{-# 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