packages feed

hs-opentelemetry-instrumentation-wai-0.0.1.4: src/OpenTelemetry/Instrumentation/Wai.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module OpenTelemetry.Instrumentation.Wai (
  newOpenTelemetryWaiMiddleware,
  newOpenTelemetryWaiMiddleware',
  requestContext,
) where

import Control.Exception (bracket)
import Control.Monad
import Data.IP (fromHostAddress, fromHostAddress6)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Vault.Lazy as Vault
import Network.HTTP.Types
import Network.Socket
import Network.Wai
import OpenTelemetry.Attributes (lookupAttribute)
import qualified OpenTelemetry.Context as Context
import OpenTelemetry.Context.ThreadLocal
import OpenTelemetry.Propagator
import OpenTelemetry.Trace.Core
import System.IO.Unsafe


newOpenTelemetryWaiMiddleware :: IO Middleware
newOpenTelemetryWaiMiddleware = getGlobalTracerProvider >>= newOpenTelemetryWaiMiddleware'


newOpenTelemetryWaiMiddleware'
  :: TracerProvider
  -> IO Middleware
newOpenTelemetryWaiMiddleware' tp = do
  let waiTracer =
        makeTracer
          tp
          "opentelemetry-instrumentation-wai"
          (TracerOptions Nothing)
  pure $ middleware waiTracer
  where
    middleware :: Tracer -> Middleware
    middleware tracer app req sendResp = do
      let propagator = getTracerProviderPropagators $ getTracerTracerProvider tracer
      let parentContextM = do
            ctx <- getContext
            ctxt <- extract propagator (requestHeaders req) ctx
            attachContext ctxt
      let path_ = T.decodeUtf8 $ rawPathInfo req
      -- peer = remoteHost req
      parentContextM
      inSpan' tracer path_ (defaultSpanArguments {kind = Server}) $ \requestSpan -> do
        ctxt <- getContext
        addAttributes
          requestSpan
          [ ("http.method", toAttribute $ T.decodeUtf8 $ requestMethod req)
          , -- , ( "http.url",
            --     toAttribute $
            --     T.decodeUtf8
            --     ((if secure req then "https://" else "http://") <> host req <> ":" <> B.pack (show $ port req) <> path req <> queryString req)
            --   )
            ("http.target", toAttribute $ T.decodeUtf8 (rawPathInfo req <> rawQueryString req))
          , -- , ( "http.host", toAttribute $ T.decodeUtf8 $ host req)
            -- , ( "http.scheme", toAttribute $ TextAttribute $ if secure req then "https" else "http")

            ( "http.flavor"
            , toAttribute $ case httpVersion req of
                (HttpVersion major minor) -> T.pack (show major <> "." <> show minor)
            )
          ,
            ( "http.user_agent"
            , toAttribute $ maybe "" T.decodeUtf8 (lookup hUserAgent $ requestHeaders req)
            )
          , -- TODO HTTP/3 will require detecting this dynamically
            ("net.transport", toAttribute ("ip_tcp" :: T.Text))
          ]

        -- TODO this is warp dependent, probably.
        -- , ( "net.host.ip")
        -- , ( "net.host.port")
        -- , ( "net.host.name")
        addAttributes requestSpan $ case remoteHost req of
          SockAddrInet port addr ->
            [ ("net.peer.port", toAttribute (fromIntegral port :: Int))
            , ("net.peer.ip", toAttribute $ T.pack $ show $ fromHostAddress addr)
            ]
          SockAddrInet6 port _ addr _ ->
            [ ("net.peer.port", toAttribute (fromIntegral port :: Int))
            , ("net.peer.ip", toAttribute $ T.pack $ show $ fromHostAddress6 addr)
            ]
          SockAddrUnix path ->
            [ ("net.peer.name", toAttribute $ T.pack path)
            ]
        let req' =
              req
                { vault =
                    Vault.insert
                      contextKey
                      ctxt
                      (vault req)
                }
        app req' $ \resp -> do
          ctxt' <- getContext
          hs <- inject propagator (Context.insertSpan requestSpan ctxt') []
          let resp' = mapResponseHeaders (hs ++) resp
          attrs <- spanGetAttributes requestSpan
          forM_ (lookupAttribute attrs "http.route") $ \case
            AttributeValue (TextAttribute route) -> updateName requestSpan route
            _ -> pure ()

          addAttributes
            requestSpan
            [ ("http.status_code", toAttribute $ statusCode $ responseStatus resp)
            ]
          when (statusCode (responseStatus resp) >= 500) $ do
            setStatus requestSpan (Error "")
          respReceived <- sendResp resp'
          ts <- getTimestamp
          endSpan requestSpan (Just ts)
          pure respReceived


contextKey :: Vault.Key Context.Context
contextKey = unsafePerformIO Vault.newKey
{-# NOINLINE contextKey #-}


requestContext :: Request -> Maybe Context.Context
requestContext =
  Vault.lookup contextKey
    . vault