{-# LANGUAGE OverloadedStrings #-} module OpenTelemetry.Instrumentation.HttpClient.Raw where import Control.Monad.IO.Class import OpenTelemetry.Context (Context, lookupSpan) import OpenTelemetry.Context.ThreadLocal import OpenTelemetry.Trace.Core import OpenTelemetry.Propagator import Network.HTTP.Client import Network.HTTP.Types import Control.Monad (forM_, when) import qualified Data.ByteString.Char8 as B import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Foldable (Foldable(toList)) import Data.CaseInsensitive (foldedCase) data HttpClientInstrumentationConfig = HttpClientInstrumentationConfig { requestHeadersToRecord :: [HeaderName] , responseHeadersToRecord :: [HeaderName] } instance Semigroup HttpClientInstrumentationConfig where l <> r = HttpClientInstrumentationConfig { requestHeadersToRecord = requestHeadersToRecord l <> requestHeadersToRecord r , responseHeadersToRecord = responseHeadersToRecord l <> responseHeadersToRecord r } instance Monoid HttpClientInstrumentationConfig where mempty = HttpClientInstrumentationConfig { requestHeadersToRecord = mempty , responseHeadersToRecord = mempty } httpClientInstrumentationConfig :: HttpClientInstrumentationConfig httpClientInstrumentationConfig = mempty -- TODO see if we can avoid recreating this on each request without being more invasive with the interface httpTracerProvider :: MonadIO m => m Tracer httpTracerProvider = do tp <- getGlobalTracerProvider pure $ makeTracer tp "hs-opentelemetry-instrumentation-http-client" tracerOptions instrumentRequest :: MonadIO m => HttpClientInstrumentationConfig -> Context -> Request -> m Request instrumentRequest conf ctxt req = do tp <- httpTracerProvider forM_ (lookupSpan ctxt) $ \s -> do addAttributes s [ ( "http.method", toAttribute $ T.decodeUtf8 $ method 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 (path req <> queryString req)) , ( "http.host", toAttribute $ T.decodeUtf8 $ host req) , ( "http.scheme", toAttribute $ TextAttribute $ if secure req then "https" else "http") , ( "http.flavor" , toAttribute $ case requestVersion req of (HttpVersion major minor) -> T.pack (show major <> "." <> show minor) ) , ( "http.user_agent" , toAttribute $ maybe "" T.decodeUtf8 (lookup hUserAgent $ requestHeaders req) ) ] addAttributes s $ concatMap (\h -> toList $ (\v -> ("http.request.header." <> T.decodeUtf8 (foldedCase h), toAttribute (T.decodeUtf8 v))) <$> lookup h (requestHeaders req)) $ requestHeadersToRecord conf hdrs <- inject (getTracerProviderPropagators $ getTracerTracerProvider tp) ctxt $ requestHeaders req pure $ req { requestHeaders = hdrs } instrumentResponse :: MonadIO m => HttpClientInstrumentationConfig -> Context -> Response a -> m () instrumentResponse conf ctxt resp = do tp <- httpTracerProvider ctxt' <- extract (getTracerProviderPropagators $ getTracerTracerProvider tp) (responseHeaders resp) ctxt _ <- attachContext ctxt' forM_ (lookupSpan ctxt') $ \s -> do when (statusCode (responseStatus resp) >= 400) $ do setStatus s (Error "") addAttributes s [ ("http.status_code", toAttribute $ statusCode $ responseStatus resp) -- TODO -- , ("http.request_content_length", _) -- , ("http.request_content_length_uncompressed", _) -- , ("http.response_content_length", _) -- , ("http.response_content_length_uncompressed", _) -- , ("net.transport") -- , ("net.peer.name") -- , ("net.peer.ip") -- , ("net.peer.port") ] addAttributes s $ concatMap (\h -> toList $ (\v -> ("http.response.header." <> T.decodeUtf8 (foldedCase h), toAttribute (T.decodeUtf8 v))) <$> lookup h (responseHeaders resp)) $ responseHeadersToRecord conf