packages feed

hs-opentelemetry-instrumentation-http-client 0.1.0.2 → 1.0.0.0

raw patch · 8 files changed

+870/−209 lines, 8 filesdep +hs-opentelemetry-exporter-in-memorydep +hs-opentelemetry-instrumentation-http-clientdep +hs-opentelemetry-sdkdep ~hs-opentelemetry-apidep ~hs-opentelemetry-instrumentation-conduitPVP ok

version bump matches the API change (PVP)

Dependencies added: hs-opentelemetry-exporter-in-memory, hs-opentelemetry-instrumentation-http-client, hs-opentelemetry-sdk, hs-opentelemetry-semantic-conventions, hspec

Dependency ranges changed: hs-opentelemetry-api, hs-opentelemetry-instrumentation-conduit

API changes (from Hackage documentation)

- OpenTelemetry.Instrumentation.HttpClient.Raw: instance GHC.Base.Monoid OpenTelemetry.Instrumentation.HttpClient.Raw.HttpClientInstrumentationConfig
- OpenTelemetry.Instrumentation.HttpClient.Raw: instance GHC.Base.Semigroup OpenTelemetry.Instrumentation.HttpClient.Raw.HttpClientInstrumentationConfig
+ OpenTelemetry.Instrumentation.HttpClient: [httpClientMetrics] :: HttpClientInstrumentationConfig -> Maybe HttpClientMetrics
+ OpenTelemetry.Instrumentation.HttpClient: httpClientPropagateHeaders :: ManagerSettings -> IO ManagerSettings
+ OpenTelemetry.Instrumentation.HttpClient: httpLbs' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> Manager -> m (Response ByteString)
+ OpenTelemetry.Instrumentation.HttpClient: httpNoBody' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> Manager -> m (Response ())
+ OpenTelemetry.Instrumentation.HttpClient: instrumentManagerSettings :: HttpClientInstrumentationConfig -> ManagerSettings -> IO ManagerSettings
+ OpenTelemetry.Instrumentation.HttpClient: newTracedManager :: HttpClientInstrumentationConfig -> ManagerSettings -> IO Manager
+ OpenTelemetry.Instrumentation.HttpClient: responseOpen' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> Manager -> m (Response BodyReader)
+ OpenTelemetry.Instrumentation.HttpClient: tracedHttpRequest :: HttpClientInstrumentationConfig -> Request -> (Request -> IO (Response a)) -> IO (Response a)
+ OpenTelemetry.Instrumentation.HttpClient: withResponse' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> Manager -> (Response BodyReader -> m a) -> m a
+ OpenTelemetry.Instrumentation.HttpClient.Raw: HttpClientMetrics :: !Histogram -> !UpDownCounter Int64 -> !Counter Int64 -> HttpClientMetrics
+ OpenTelemetry.Instrumentation.HttpClient.Raw: [hcmActive] :: HttpClientMetrics -> !UpDownCounter Int64
+ OpenTelemetry.Instrumentation.HttpClient.Raw: [hcmCount] :: HttpClientMetrics -> !Counter Int64
+ OpenTelemetry.Instrumentation.HttpClient.Raw: [hcmDuration] :: HttpClientMetrics -> !Histogram
+ OpenTelemetry.Instrumentation.HttpClient.Raw: [httpClientMetrics] :: HttpClientInstrumentationConfig -> Maybe HttpClientMetrics
+ OpenTelemetry.Instrumentation.HttpClient.Raw: createHttpClientMetrics :: IO HttpClientMetrics
+ OpenTelemetry.Instrumentation.HttpClient.Raw: createHttpClientMetricsFromMeter :: Meter -> IO HttpClientMetrics
+ OpenTelemetry.Instrumentation.HttpClient.Raw: data HttpClientMetrics
+ OpenTelemetry.Instrumentation.HttpClient.Raw: httpClientPropagateHeaders :: ManagerSettings -> IO ManagerSettings
+ OpenTelemetry.Instrumentation.HttpClient.Raw: httpVersionText :: HttpVersion -> Text
+ OpenTelemetry.Instrumentation.HttpClient.Raw: instance GHC.Internal.Base.Monoid OpenTelemetry.Instrumentation.HttpClient.Raw.HttpClientInstrumentationConfig
+ OpenTelemetry.Instrumentation.HttpClient.Raw: instance GHC.Internal.Base.Semigroup OpenTelemetry.Instrumentation.HttpClient.Raw.HttpClientInstrumentationConfig
+ OpenTelemetry.Instrumentation.HttpClient.Raw: instrumentManagerSettings :: HttpClientInstrumentationConfig -> ManagerSettings -> IO ManagerSettings
+ OpenTelemetry.Instrumentation.HttpClient.Raw: instrumentResponseOnSpan :: MonadIO m => HttpClientInstrumentationConfig -> Span -> Response a -> m ()
+ OpenTelemetry.Instrumentation.HttpClient.Raw: newTracedManager :: HttpClientInstrumentationConfig -> ManagerSettings -> IO Manager
+ OpenTelemetry.Instrumentation.HttpClient.Raw: tracedHttpRequest :: HttpClientInstrumentationConfig -> Request -> (Request -> IO (Response a)) -> IO (Response a)
+ OpenTelemetry.Instrumentation.HttpClient.Simple: [httpClientMetrics] :: HttpClientInstrumentationConfig -> Maybe HttpClientMetrics
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpBS' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> m (Response ByteString)
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpJSON' :: (MonadUnliftIO m, FromJSON a, HasCallStack) => HttpClientInstrumentationConfig -> Request -> m (Response a)
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpJSONEither' :: (FromJSON a, MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> m (Response (Either JSONException a))
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpLBS' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> m (Response ByteString)
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpNoBody' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> m (Response ())
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpSink' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> (Response () -> ConduitM ByteString Void m a) -> m a
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpSource' :: forall (m :: Type -> Type) i o r. (MonadUnliftIO m, MonadResource m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> (Response (ConduitM i ByteString m ()) -> ConduitM i o m r) -> ConduitM i o m r
+ OpenTelemetry.Instrumentation.HttpClient.Simple: withResponse' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> (Response (ConduitM i ByteString m ()) -> m a) -> m a
- OpenTelemetry.Instrumentation.HttpClient: HttpClientInstrumentationConfig :: Maybe Text -> [HeaderName] -> [HeaderName] -> HttpClientInstrumentationConfig
+ OpenTelemetry.Instrumentation.HttpClient: HttpClientInstrumentationConfig :: Maybe Text -> [HeaderName] -> [HeaderName] -> Maybe HttpClientMetrics -> HttpClientInstrumentationConfig
- OpenTelemetry.Instrumentation.HttpClient: httpLbs :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> Manager -> m (Response ByteString)
+ OpenTelemetry.Instrumentation.HttpClient: httpLbs :: (MonadUnliftIO m, HasCallStack) => Request -> Manager -> m (Response ByteString)
- OpenTelemetry.Instrumentation.HttpClient: httpNoBody :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> Manager -> m (Response ())
+ OpenTelemetry.Instrumentation.HttpClient: httpNoBody :: (MonadUnliftIO m, HasCallStack) => Request -> Manager -> m (Response ())
- OpenTelemetry.Instrumentation.HttpClient: responseOpen :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> Manager -> m (Response BodyReader)
+ OpenTelemetry.Instrumentation.HttpClient: responseOpen :: (MonadUnliftIO m, HasCallStack) => Request -> Manager -> m (Response BodyReader)
- OpenTelemetry.Instrumentation.HttpClient: withResponse :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> Manager -> (Response BodyReader -> m a) -> m a
+ OpenTelemetry.Instrumentation.HttpClient: withResponse :: (MonadUnliftIO m, HasCallStack) => Request -> Manager -> (Response BodyReader -> m a) -> m a
- OpenTelemetry.Instrumentation.HttpClient.Raw: HttpClientInstrumentationConfig :: Maybe Text -> [HeaderName] -> [HeaderName] -> HttpClientInstrumentationConfig
+ OpenTelemetry.Instrumentation.HttpClient.Raw: HttpClientInstrumentationConfig :: Maybe Text -> [HeaderName] -> [HeaderName] -> Maybe HttpClientMetrics -> HttpClientInstrumentationConfig
- OpenTelemetry.Instrumentation.HttpClient.Simple: HttpClientInstrumentationConfig :: Maybe Text -> [HeaderName] -> [HeaderName] -> HttpClientInstrumentationConfig
+ OpenTelemetry.Instrumentation.HttpClient.Simple: HttpClientInstrumentationConfig :: Maybe Text -> [HeaderName] -> [HeaderName] -> Maybe HttpClientMetrics -> HttpClientInstrumentationConfig
- OpenTelemetry.Instrumentation.HttpClient.Simple: httpBS :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> m (Response ByteString)
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpBS :: (MonadUnliftIO m, HasCallStack) => Request -> m (Response ByteString)
- OpenTelemetry.Instrumentation.HttpClient.Simple: httpJSON :: (MonadUnliftIO m, FromJSON a, HasCallStack) => HttpClientInstrumentationConfig -> Request -> m (Response a)
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpJSON :: (MonadUnliftIO m, FromJSON a, HasCallStack) => Request -> m (Response a)
- OpenTelemetry.Instrumentation.HttpClient.Simple: httpJSONEither :: (FromJSON a, MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> m (Response (Either JSONException a))
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpJSONEither :: (FromJSON a, MonadUnliftIO m, HasCallStack) => Request -> m (Response (Either JSONException a))
- OpenTelemetry.Instrumentation.HttpClient.Simple: httpLBS :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> m (Response ByteString)
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpLBS :: (MonadUnliftIO m, HasCallStack) => Request -> m (Response ByteString)
- OpenTelemetry.Instrumentation.HttpClient.Simple: httpNoBody :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> m (Response ())
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpNoBody :: (MonadUnliftIO m, HasCallStack) => Request -> m (Response ())
- OpenTelemetry.Instrumentation.HttpClient.Simple: httpSink :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> (Response () -> ConduitM ByteString Void m a) -> m a
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpSink :: (MonadUnliftIO m, HasCallStack) => Request -> (Response () -> ConduitM ByteString Void m a) -> m a
- OpenTelemetry.Instrumentation.HttpClient.Simple: httpSource :: forall (m :: Type -> Type) i o r. (MonadUnliftIO m, MonadResource m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> (Response (ConduitM i ByteString m ()) -> ConduitM i o m r) -> ConduitM i o m r
+ OpenTelemetry.Instrumentation.HttpClient.Simple: httpSource :: forall (m :: Type -> Type) i o r. (MonadUnliftIO m, MonadResource m, HasCallStack) => Request -> (Response (ConduitM i ByteString m ()) -> ConduitM i o m r) -> ConduitM i o m r
- OpenTelemetry.Instrumentation.HttpClient.Simple: withResponse :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Request -> (Response (ConduitM i ByteString m ()) -> m a) -> m a
+ OpenTelemetry.Instrumentation.HttpClient.Simple: withResponse :: (MonadUnliftIO m, HasCallStack) => Request -> (Response (ConduitM i ByteString m ()) -> m a) -> m a

Files

ChangeLog.md view
@@ -1,5 +1,15 @@ # Changelog for hs-opentelemetry-instrumentation-http-client +## Unreleased++## 1.0.0.0 - 2026-05-29++- **Fix: stable attribute `http.host` → `server.address`.** The stable+  HTTP semantic conventions use `server.address` and `server.port` instead+  of the legacy `http.host`.+- Add `error.type` attribute on HTTP error responses (status >= 400).+- Fix span name to use HTTP method (low-cardinality) instead of full URL.+ ## 0.1.0.2  - Relax `hs-opentelemetry-api` bounds to support 0.3.x
LICENSE view
@@ -1,4 +1,4 @@-Copyright Ian Duncan (c) 2021+Copyright Ian Duncan (c) 2021-2026  All rights reserved. 
README.md view
@@ -1,1 +1,3 @@ # hs-opentelemetry-instrumentation-http-client++[![hs-opentelemetry-instrumentation-http-client](https://img.shields.io/hackage/v/hs-opentelemetry-instrumentation-http-client?style=flat-square&logo=haskell&label=hs-opentelemetry-instrumentation-http-client&labelColor=5D4F85)](https://hackage.haskell.org/package/hs-opentelemetry-instrumentation-http-client)
hs-opentelemetry-instrumentation-http-client.cabal view
@@ -1,20 +1,22 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.37.0.+-- This file has been generated from package.yaml by hpack version 0.38.3. -- -- see: https://github.com/sol/hpack -name:               hs-opentelemetry-instrumentation-http-client-version:            0.1.0.2-description:        Please see the README on GitHub at <https://github.com/iand675/hs-opentelemetry/tree/main/instrumentation/http-client#readme>-homepage:           https://github.com/iand675/hs-opentelemetry#readme-bug-reports:        https://github.com/iand675/hs-opentelemetry/issues-author:             Ian Duncan, Jade Lovelace-maintainer:         ian@iankduncan.com-copyright:          2024 Ian Duncan, Mercury Technologies-license:            BSD3-license-file:       LICENSE-build-type:         Simple+name:           hs-opentelemetry-instrumentation-http-client+version:        1.0.0.0+synopsis:       OpenTelemetry instrumentation for http-client+description:    Please see the README on GitHub at <https://github.com/iand675/hs-opentelemetry/tree/main/instrumentation/http-client#readme>+category:       OpenTelemetry, Telemetry, HTTP, Web+homepage:       https://github.com/iand675/hs-opentelemetry#readme+bug-reports:    https://github.com/iand675/hs-opentelemetry/issues+author:         Ian Duncan, Jade Lovelace+maintainer:     ian@iankduncan.com+copyright:      2024 Ian Duncan, Mercury Technologies+license:        BSD3+license-file:   LICENSE+build-type:     Simple extra-source-files:     README.md     ChangeLog.md@@ -39,12 +41,35 @@     , bytestring     , case-insensitive     , conduit-    , hs-opentelemetry-api ==0.3.*-    , hs-opentelemetry-instrumentation-conduit >=0.0.1 && <0.2+    , hs-opentelemetry-api ==1.0.*+    , hs-opentelemetry-instrumentation-conduit ==1.0.*+    , hs-opentelemetry-semantic-conventions >=1.40 && <2     , http-client     , http-conduit     , http-types     , text     , unliftio+    , unordered-containers+  default-language: Haskell2010++test-suite hs-opentelemetry-instrumentation-http-client-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_hs_opentelemetry_instrumentation_http_client+  hs-source-dirs:+      test+  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.7 && <5+    , hs-opentelemetry-api ==1.0.*+    , hs-opentelemetry-exporter-in-memory ==1.0.*+    , hs-opentelemetry-instrumentation-http-client ==1.0.*+    , hs-opentelemetry-sdk ==1.0.*+    , hs-opentelemetry-semantic-conventions >=1.40 && <2+    , hspec+    , http-client+    , http-types+    , text     , unordered-containers   default-language: Haskell2010
src/OpenTelemetry/Instrumentation/HttpClient.hs view
@@ -1,24 +1,81 @@ {-# LANGUAGE OverloadedStrings #-} -{- | Offer a few options for HTTP instrumentation+{- |+Module      : OpenTelemetry.Instrumentation.HttpClient+Copyright   : (c) Ian Duncan, 2021-2026+License     : BSD-3+Description : Automatic tracing for http-client requests+Stability   : experimental -- Add attributes via 'Request' and 'Response' to an existing span (Best)-- Use internals to instrument a particular callsite using modifyRequest, modifyResponse (Next best)-- Provide a middleware to pull from the thread-local state (okay)-- Modify the global manager to pull from the thread-local state (least good, can't be helped sometimes)+= Overview -[New HTTP semantic conventions have been declared stable.](https://opentelemetry.io/blog/2023/http-conventions-declared-stable/#migration-plan) Opt-in by setting the environment variable OTEL_SEMCONV_STABILITY_OPT_IN to-- "http" - to use the stable conventions-- "http/dup" - to emit both the old and the stable conventions-Otherwise, the old conventions will be used. The stable conventions will replace the old conventions in the next major release of this library.+Instruments outbound HTTP requests made with the @http-client@ library.+Creates a @Client@ span for each request and injects trace context into+request headers so downstream services can continue the trace.++= Quick example++The usual approach is to build a 'Manager' with traced settings once; spans+use the tracer from 'OpenTelemetry.Instrumentation.HttpClient.Raw.httpTracerProvider'+(typically the global tracer provider):++@+import Network.HTTP.Client.TLS (tlsManagerSettings)+import OpenTelemetry.Instrumentation.HttpClient+  ( httpLbs+  , httpClientInstrumentationConfig+  , newTracedManager+  )++main :: IO ()+main = do+  manager <- newTracedManager httpClientInstrumentationConfig tlsManagerSettings+  response <- httpLbs request manager+  ...+@++For custom manager settings, apply 'OpenTelemetry.Instrumentation.HttpClient.Raw.instrumentManagerSettings'+before @newManager@. You can also use 'tracedHttpRequest' or the prime-suffixed+variants (e.g. 'httpLbs'') for per-call configuration; see the export list below.++= What gets traced++Each outbound request creates a @Client@ span with:++* Span name: @METHOD host@ (e.g. @GET api.example.com@)+* @http.request.method@, @url.full@, @server.address@, @server.port@+* @http.response.status_code@, @http.response.body.size@+* Trace context injected into request headers via the global propagator++[HTTP semantic conventions migration:](https://opentelemetry.io/blog/2023/http-conventions-declared-stable/#migration-plan)+opt in via @OTEL_SEMCONV_STABILITY_OPT_IN@ (@http@, @http/dup@, or default legacy). -} module OpenTelemetry.Instrumentation.HttpClient (+  -- * Manager-level instrumentation (recommended)+  instrumentManagerSettings,+  newTracedManager,+  httpClientPropagateHeaders,++  -- * Per-request combinator+  tracedHttpRequest,++  -- * Drop-in replacements (zero-config)   withResponse,   httpLbs,   httpNoBody,   responseOpen,++  -- * Drop-in replacements (with config)+  withResponse',+  httpLbs',+  httpNoBody',+  responseOpen',++  -- * Configuration   httpClientInstrumentationConfig,   HttpClientInstrumentationConfig (..),++  -- * Re-exports   module X, ) where @@ -31,9 +88,13 @@ import OpenTelemetry.Instrumentation.HttpClient.Raw (   HttpClientInstrumentationConfig (..),   httpClientInstrumentationConfig,+  httpClientPropagateHeaders,   httpTracerProvider,+  instrumentManagerSettings,   instrumentRequest,   instrumentResponse,+  newTracedManager,+  tracedHttpRequest,  ) import OpenTelemetry.Trace.Core (   SpanArguments (kind),@@ -50,34 +111,70 @@ spanArgs = defaultSpanArguments {kind = Client}  -{- | Instrumented variant of @Network.HTTP.Client.withResponse@+{- | 'withResponse' with default instrumentation config. - Perform a @Request@ using a connection acquired from the given @Manager@,- and then provide the @Response@ to the given function. This function is- fully exception safe, guaranteeing that the response will be closed when the- inner function exits. It is defined as:+@since 0.2.0.0+-}+withResponse+  :: (MonadUnliftIO m, HasCallStack)+  => Client.Request+  -> Client.Manager+  -> (Client.Response Client.BodyReader -> m a)+  -> m a+withResponse = withResponse' mempty - > withResponse req man f = bracket (responseOpen req man) responseClose f - It is recommended that you use this function in place of explicit calls to- 'responseOpen' and 'responseClose'.+{- | 'httpLbs' with default instrumentation config. - You will need to use functions such as 'brRead' to consume the response- body.+@since 0.2.0.0 -}-withResponse+httpLbs   :: (MonadUnliftIO m, HasCallStack)+  => Client.Request+  -> Client.Manager+  -> m (Client.Response L.ByteString)+httpLbs = httpLbs' mempty+++{- | 'httpNoBody' with default instrumentation config.++@since 0.2.0.0+-}+httpNoBody+  :: (MonadUnliftIO m, HasCallStack)+  => Client.Request+  -> Client.Manager+  -> m (Client.Response ())+httpNoBody = httpNoBody' mempty+++{- | 'responseOpen' with default instrumentation config.++@since 0.2.0.0+-}+responseOpen+  :: (MonadUnliftIO m, HasCallStack)+  => Client.Request+  -> Client.Manager+  -> m (Client.Response Client.BodyReader)+responseOpen = responseOpen' mempty+++{- | Instrumented 'Client.withResponse' with explicit config.++@since 0.2.0.0+-}+withResponse'+  :: (MonadUnliftIO m, HasCallStack)   => HttpClientInstrumentationConfig   -> Client.Request   -> Client.Manager   -> (Client.Response Client.BodyReader -> m a)   -> m a-withResponse httpConf req man f = do+withResponse' httpConf req man f = do   tracer <- httpTracerProvider   inSpan'' tracer "withResponse" (addAttributesToSpanArguments callerAttributes spanArgs) $ \_wrSpan -> do     ctxt <- getContext-    -- TODO would like to capture the req/resp time specifically-    -- inSpan "http.request" (defaultSpanArguments { startingKind = Client }) $ \httpReqSpan -> do     req' <- instrumentRequest httpConf ctxt req     runInIO <- askRunInIO     liftIO $ Client.withResponse req' man $ \resp -> do@@ -85,14 +182,17 @@       runInIO $ f resp  -{- | A convenience wrapper around 'withResponse' which reads in the entire- response body and immediately closes the connection. Note that this function- performs fully strict I\/O, and only uses a lazy ByteString in its response- for memory efficiency. If you are anticipating a large response body, you- are encouraged to use 'withResponse' and 'brRead' instead.+{- | Instrumented 'Client.httpLbs' with explicit config.++@since 0.2.0.0 -}-httpLbs :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Client.Request -> Client.Manager -> m (Client.Response L.ByteString)-httpLbs httpConf req man = do+httpLbs'+  :: (MonadUnliftIO m, HasCallStack)+  => HttpClientInstrumentationConfig+  -> Client.Request+  -> Client.Manager+  -> m (Client.Response L.ByteString)+httpLbs' httpConf req man = do   tracer <- httpTracerProvider   inSpan'' tracer "httpLbs" (addAttributesToSpanArguments callerAttributes spanArgs) $ \_ -> do     ctxt <- getContext@@ -102,11 +202,17 @@     pure resp  -{- | A convenient wrapper around 'withResponse' which ignores the response- body. This is useful, for example, when performing a HEAD request.+{- | Instrumented 'Client.httpNoBody' with explicit config.++@since 0.2.0.0 -}-httpNoBody :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Client.Request -> Client.Manager -> m (Client.Response ())-httpNoBody httpConf req man = do+httpNoBody'+  :: (MonadUnliftIO m, HasCallStack)+  => HttpClientInstrumentationConfig+  -> Client.Request+  -> Client.Manager+  -> m (Client.Response ())+httpNoBody' httpConf req man = do   tracer <- httpTracerProvider   inSpan'' tracer "httpNoBody" (addAttributesToSpanArguments callerAttributes spanArgs) $ \_ -> do     ctxt <- getContext@@ -116,36 +222,17 @@     pure resp  -{- | The most low-level function for initiating an HTTP request.-- The first argument to this function gives a full specification- on the request: the host to connect to, whether to use SSL,- headers, etc. Please see 'Request' for full details.  The- second argument specifies which 'Manager' should be used.-- This function then returns a 'Response' with a- 'BodyReader'.  The 'Response' contains the status code- and headers that were sent back to us, and the- 'BodyReader' contains the body of the request.  Note- that this 'BodyReader' allows you to have fully- interleaved IO actions during your HTTP download, making it- possible to download very large responses in constant memory.-- An important note: the response body returned by this function represents a- live HTTP connection. As such, if you do not use the response body, an open- socket will be retained indefinitely. You must be certain to call- 'responseClose' on this response to free up resources.-- This function automatically performs any necessary redirects, as specified- by the 'redirectCount' setting.+{- | Instrumented 'Client.responseOpen' with explicit config. - When implementing a (reverse) proxy using this function or relating- functions, it's wise to remove Transfer-Encoding:, Content-Length:,- Content-Encoding: and Accept-Encoding: from request and response- headers to be relayed.+@since 0.2.0.0 -}-responseOpen :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Client.Request -> Client.Manager -> m (Client.Response Client.BodyReader)-responseOpen httpConf req man = do+responseOpen'+  :: (MonadUnliftIO m, HasCallStack)+  => HttpClientInstrumentationConfig+  -> Client.Request+  -> Client.Manager+  -> m (Client.Response Client.BodyReader)+responseOpen' httpConf req man = do   tracer <- httpTracerProvider   inSpan'' tracer "responseOpen" (addAttributesToSpanArguments callerAttributes spanArgs) $ \_ -> do     ctxt <- getContext
src/OpenTelemetry/Instrumentation/HttpClient/Raw.hs view
@@ -1,40 +1,91 @@+{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -module OpenTelemetry.Instrumentation.HttpClient.Raw where+{- |+Module      : OpenTelemetry.Instrumentation.HttpClient.Raw+Description : Low-level HTTP client instrumentation.+Stability   : experimental +Provides raw request/response hooks for creating spans around HTTP calls.+-}+module OpenTelemetry.Instrumentation.HttpClient.Raw (+  -- * Manager-level instrumentation+  instrumentManagerSettings,+  newTracedManager,+  httpClientPropagateHeaders,++  -- * Per-request combinator+  tracedHttpRequest,++  -- * Low-level building blocks+  instrumentRequest,+  instrumentResponse,+  instrumentResponseOnSpan,++  -- * Configuration+  HttpClientInstrumentationConfig (..),+  httpClientInstrumentationConfig,++  -- * Metrics+  HttpClientMetrics (..),+  createHttpClientMetrics,+  createHttpClientMetricsFromMeter,++  -- * Internal+  httpTracerProvider,+  httpVersionText,+) where+ import Control.Applicative ((<|>))+import Control.Exception (SomeException, catch, throwIO) import Control.Monad (forM_, when) import Control.Monad.IO.Class import qualified Data.ByteString.Char8 as B import Data.CaseInsensitive (foldedCase)+import qualified Data.CaseInsensitive as CI import qualified Data.HashMap.Strict as H+import Data.Int (Int64) import Data.Maybe import qualified Data.Text as T import qualified Data.Text.Encoding as T+import qualified Data.Text.Lazy as TL+import Data.Text.Lazy.Builder (toLazyText)+import Data.Text.Lazy.Builder.Int (decimal)+import Data.Typeable (tyConName, typeOf, typeRepTyCon)+import Data.Word (Word64) import Network.HTTP.Client import Network.HTTP.Types-import OpenTelemetry.Context (Context, lookupSpan)+import qualified OpenTelemetry.Attributes as A+import OpenTelemetry.Attributes.Key (unkey)+import OpenTelemetry.Context (insertSpan, lookupSpan)+import qualified OpenTelemetry.Context as Ctx import OpenTelemetry.Context.ThreadLocal-import OpenTelemetry.Propagator+import OpenTelemetry.Metric.Core+import OpenTelemetry.Propagator (TextMap, TextMapPropagator, extract, getGlobalTextMapPropagator, inject, textMapFromList, textMapToList)+import qualified OpenTelemetry.SemanticConventions as SC import OpenTelemetry.SemanticsConfig import OpenTelemetry.Trace.Core+import System.IO.Unsafe (unsafePerformIO)   data HttpClientInstrumentationConfig = HttpClientInstrumentationConfig   { requestName :: Maybe T.Text   , requestHeadersToRecord :: [HeaderName]   , responseHeadersToRecord :: [HeaderName]+  , httpClientMetrics :: Maybe HttpClientMetrics   }   instance Semigroup HttpClientInstrumentationConfig where   l <> r =     HttpClientInstrumentationConfig-      { requestName = requestName r <|> requestName l -- flipped on purpose: last writer wins+      { requestName = requestName r <|> requestName l       , requestHeadersToRecord = requestHeadersToRecord l <> requestHeadersToRecord r       , responseHeadersToRecord = responseHeadersToRecord l <> responseHeadersToRecord r+      , httpClientMetrics = httpClientMetrics r <|> httpClientMetrics l       }  @@ -44,6 +95,7 @@       { requestName = Nothing       , requestHeadersToRecord = mempty       , responseHeadersToRecord = mempty+      , httpClientMetrics = Nothing       }  @@ -51,142 +103,461 @@ httpClientInstrumentationConfig = mempty  --- TODO see if we can avoid recreating this on each request without being more invasive with the interface+-- Context key for the span created by Manager-level hooks, so we can+-- retrieve "our" span in managerModifyResponse without confusing it with+-- a caller's span.+managedSpanKey :: Ctx.Key Span+managedSpanKey = unsafePerformIO $ Ctx.newKey "http-client.managed-span"+{-# NOINLINE managedSpanKey #-}+++-- Context key to stash the attach token so we can restore context+-- after managerModifyResponse completes.+parentTokenKey :: Ctx.Key Token+parentTokenKey = unsafePerformIO $ Ctx.newKey "http-client.parent-token"+{-# NOINLINE parentTokenKey #-}+++metricStartKey :: Ctx.Key (Word64, Request)+metricStartKey = unsafePerformIO $ Ctx.newKey "http-client.metric-start"+{-# NOINLINE metricStartKey #-}++ httpTracerProvider :: (MonadIO m) => m Tracer httpTracerProvider = do   tp <- getGlobalTracerProvider   pure $ makeTracer tp $detectInstrumentationLibrary tracerOptions  -instrumentRequest+data HttpClientMetrics = HttpClientMetrics+  { hcmDuration :: !Histogram+  , hcmActive :: !(UpDownCounter Int64)+  , hcmCount :: !(Counter Int64)+  }+++createHttpClientMetrics :: IO HttpClientMetrics+createHttpClientMetrics = do+  mp <- getGlobalMeterProvider+  meter <- getMeter mp "hs-opentelemetry-instrumentation-http-client"+  createHttpClientMetricsFromMeter meter+++createHttpClientMetricsFromMeter :: Meter -> IO HttpClientMetrics+createHttpClientMetricsFromMeter meter = do+  dur <-+    meterCreateHistogram+      meter+      "http.client.request.duration"+      (Just "s")+      (Just "Duration of outbound HTTP requests")+      defaultAdvisoryParameters+        { advisoryExplicitBucketBoundaries =+            Just [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0]+        }+  active <- meterCreateUpDownCounterInt64 meter "http.client.active_requests" (Just "{request}") (Just "Number of active outbound HTTP requests") defaultAdvisoryParameters+  cnt <- meterCreateCounterInt64 meter "http.client.request.count" (Just "{request}") (Just "Total number of outbound HTTP requests") defaultAdvisoryParameters+  pure HttpClientMetrics {hcmDuration = dur, hcmActive = active, hcmCount = cnt}+++resolveMetrics :: HttpClientInstrumentationConfig -> IO HttpClientMetrics+resolveMetrics conf = maybe createHttpClientMetrics pure (httpClientMetrics conf)+++metricRequestAttrs :: Request -> A.Attributes+metricRequestAttrs req =+  let a1 = A.addAttribute A.defaultAttributeLimits A.emptyAttributes (unkey SC.http_request_method) (T.decodeUtf8 (method req))+      a2 = A.addAttribute A.defaultAttributeLimits a1 (unkey SC.server_address) (T.decodeUtf8 (host req))+      a3 = A.addAttribute A.defaultAttributeLimits a2 (unkey SC.server_port) (port req)+  in a3+++recordResponseMetrics :: HttpClientMetrics -> Request -> Word64 -> Word64 -> Response a -> IO ()+recordResponseMetrics hcm req startNs endNs resp = do+  let sc = statusCode (responseStatus resp)+      durationSec = fromIntegral (endNs - startNs) / 1_000_000_000 :: Double+      base = metricRequestAttrs req+      a1 = A.addAttribute A.defaultAttributeLimits base (unkey SC.http_response_statusCode) sc+      a2 = A.addAttribute A.defaultAttributeLimits a1 (unkey SC.network_protocol_version) (httpVersionText (responseVersion resp))+      respAttrs+        | sc >= 400 = A.addAttribute A.defaultAttributeLimits a2 (unkey SC.error_type) (T.pack (show sc))+        | otherwise = a2+  histogramRecord (hcmDuration hcm) durationSec respAttrs+  counterAdd (hcmCount hcm) 1 respAttrs+++recordErrorMetrics :: HttpClientMetrics -> Request -> Word64 -> Word64 -> SomeException -> IO ()+recordErrorMetrics hcm req startNs endNs ex = do+  let durationSec = fromIntegral (endNs - startNs) / 1_000_000_000 :: Double+      base = metricRequestAttrs req+      errType = T.pack $ tyConName $ typeRepTyCon $ typeOf ex+      errAttrs = A.addAttribute A.defaultAttributeLimits base (unkey SC.error_type) errType+  histogramRecord (hcmDuration hcm) durationSec errAttrs+  counterAdd (hcmCount hcm) 1 errAttrs+++{- | Instrument a 'ManagerSettings' to automatically trace all HTTP requests.++Each request through the resulting 'Manager' will:++1. Create a client span named after the HTTP method (e.g., @\"GET\"@, @\"POST\"@)+2. Add request\/response attributes per the OTel HTTP semantic conventions+3. Inject context propagation headers (e.g., @traceparent@, @tracestate@)+4. Record response status code and set error status for 4xx\/5xx+5. End the span when the response is received (or on exception)++This is the recommended way to instrument http-client. Set it up once+at 'Manager' creation time and all requests — including those from+third-party libraries — are automatically traced.++@+settings <- 'instrumentManagerSettings' 'httpClientInstrumentationConfig' tlsManagerSettings+manager <- 'newManager' settings+-- All requests through this manager are now traced:+resp <- Client.httpLbs req manager+@++@since 0.2.0.0+-}+instrumentManagerSettings+  :: HttpClientInstrumentationConfig+  -> ManagerSettings+  -> IO ManagerSettings+instrumentManagerSettings conf settings = do+  tracer <- httpTracerProvider+  hcm <- resolveMetrics conf+  pure $+    settings+      { managerModifyRequest = \req -> do+          req' <- managerModifyRequest settings req+          parentCtx <- getContext++          let methodText = T.decodeUtf8 (method req')+              reqSpanName = fromMaybe methodText (requestName conf)++          s <-+            createSpanWithoutCallStack+              tracer+              parentCtx+              reqSpanName+              defaultSpanArguments {kind = Client}++          let newCtx =+                Ctx.insert managedSpanKey s+                  . insertSpan s+                  $ parentCtx++          tok <- attachContext newCtx+          adjustContext (Ctx.insert parentTokenKey tok)++          upDownCounterAdd (hcmActive hcm) 1 (metricRequestAttrs req')++          addRequestAttributes conf s req'++          propagator <- getGlobalTextMapPropagator+          ctx' <- getContext+          hdrs <- injectToHeaders propagator ctx' (requestHeaders req')+          startTs <- getTimestamp+          adjustContext (Ctx.insert metricStartKey (timestampNanoseconds startTs, req'))+          pure req' {requestHeaders = hdrs}+      , managerModifyResponse = \resp -> do+          resp' <- managerModifyResponse settings resp+          ctx <- getContext+          forM_ (Ctx.lookup managedSpanKey ctx) $ \s -> do+            instrumentResponseOnSpan conf s resp'+            endSpan s Nothing+          forM_ (Ctx.lookup metricStartKey ctx) $ \(startNs, origReq) -> do+            endTs <- getTimestamp+            upDownCounterAdd (hcmActive hcm) (-1) (metricRequestAttrs origReq)+            recordResponseMetrics hcm origReq startNs (timestampNanoseconds endTs) resp'+          forM_ (Ctx.lookup parentTokenKey ctx) detachContext+          pure resp'+      , managerWrapException = \req action ->+          managerWrapException settings req action `catch` \(e :: SomeException) -> do+            ctx <- getContext+            forM_ (Ctx.lookup managedSpanKey ctx) $ \s -> do+              let errText = T.pack (show e)+              addAttributes s [(unkey SC.error_type, toAttribute errText)]+              setStatus s (Error errText)+              endSpan s Nothing+            forM_ (Ctx.lookup metricStartKey ctx) $ \(startNs, origReq) -> do+              endTs <- getTimestamp+              upDownCounterAdd (hcmActive hcm) (-1) (metricRequestAttrs origReq)+              recordErrorMetrics hcm origReq startNs (timestampNanoseconds endTs) e+            forM_ (Ctx.lookup parentTokenKey ctx) detachContext+            throwIO e+      }+++{- | Create a new 'Manager' with automatic tracing. Convenience wrapper+around 'instrumentManagerSettings' and 'newManager'.++@+manager <- 'newTracedManager' 'httpClientInstrumentationConfig' defaultManagerSettings+@++@since 0.2.0.0+-}+newTracedManager :: HttpClientInstrumentationConfig -> ManagerSettings -> IO Manager+newTracedManager conf settings = do+  settings' <- instrumentManagerSettings conf settings+  newManager settings'+++{- | Lightweight variant that only injects context propagation headers+(e.g., @traceparent@, @tracestate@) without creating spans. Useful when+spans are managed elsewhere (e.g., via @inSpan@) but you still want+outgoing requests to carry trace context.++@+settings <- 'httpClientPropagateHeaders' defaultManagerSettings+manager <- 'newManager' settings+@++@since 0.2.0.0+-}+httpClientPropagateHeaders :: ManagerSettings -> IO ManagerSettings+httpClientPropagateHeaders settings =+  pure $+    settings+      { managerModifyRequest = \req -> do+          req' <- managerModifyRequest settings req+          ctx <- getContext+          propagator <- getGlobalTextMapPropagator+          hdrs <- injectToHeaders propagator ctx (requestHeaders req')+          pure req' {requestHeaders = hdrs}+      }+++{- | Wrap any HTTP request action in a client span. The request is used+to derive the span name and attributes; the continuation receives the+modified request with propagation headers injected.++@+resp <- 'tracedHttpRequest' conf req $ \\req' ->+  Client.httpLbs req' manager+@++Exception-safe: the span is always ended, even if the action throws.++@since 0.2.0.0+-}+tracedHttpRequest+  :: HttpClientInstrumentationConfig+  -> Request+  -> (Request -> IO (Response a))+  -> IO (Response a)+tracedHttpRequest conf req action = do+  tracer <- httpTracerProvider+  hcm <- resolveMetrics conf+  let methodText = T.decodeUtf8 (method req)+      reqSpanName = fromMaybe methodText (requestName conf)+  ctx <- getContext+  s <-+    createSpanWithoutCallStack+      tracer+      ctx+      reqSpanName+      defaultSpanArguments {kind = Client}+  let ctx' = insertSpan s ctx+  tok <- attachContext ctx'++  addRequestAttributes conf s req+  upDownCounterAdd (hcmActive hcm) 1 (metricRequestAttrs req)+  startTs <- getTimestamp++  propagator <- getGlobalTextMapPropagator+  hdrs <- injectToHeaders propagator ctx' (requestHeaders req)+  let req' = req {requestHeaders = hdrs}++  let onError (e :: SomeException) = do+        endTs <- getTimestamp+        upDownCounterAdd (hcmActive hcm) (-1) (metricRequestAttrs req)+        recordErrorMetrics hcm req (timestampNanoseconds startTs) (timestampNanoseconds endTs) e+        let errText = T.pack (show e)+        addAttributes s [(unkey SC.error_type, toAttribute errText)]+        setStatus s (Error errText)+        endSpan s Nothing+        detachContext tok+        throwIO e++  resp <-+    action req' `catch` onError++  endTs <- getTimestamp+  upDownCounterAdd (hcmActive hcm) (-1) (metricRequestAttrs req)+  recordResponseMetrics hcm req (timestampNanoseconds startTs) (timestampNanoseconds endTs) resp+  instrumentResponseOnSpan conf s resp+  endSpan s Nothing+  detachContext tok+  pure resp+++-- | Add request attributes to a span. Does not inject propagation headers.+addRequestAttributes   :: (MonadIO m)   => HttpClientInstrumentationConfig-  -> Context+  -> Span   -> Request-  -> m Request-instrumentRequest conf ctxt req = do-  tracer <- httpTracerProvider-  forM_ (lookupSpan ctxt) $ \s -> do-    let url =-          T.decodeUtf8-            ((if secure req then "https://" else "http://") <> host req <> ":" <> B.pack (show $ port req) <> path req <> queryString req)-    updateName s $ fromMaybe url $ requestName conf+  -> m ()+addRequestAttributes conf s req = do+  let url =+        T.decodeUtf8+          ( (if secure req then "https://" else "http://")+              <> host req+              <> ":"+              <> B.pack (show $ port req)+              <> path req+              <> queryString req+          )+      methodText = T.decodeUtf8 $ method req -    let addStableAttributes = do-          addAttributes-            s-            [ ("http.request.method", toAttribute $ T.decodeUtf8 $ method req)-            , ("url.full", toAttribute url)-            , ("url.path", toAttribute $ T.decodeUtf8 $ path req)-            , ("url.query", toAttribute $ T.decodeUtf8 $ queryString req)-            , ("http.host", toAttribute $ T.decodeUtf8 $ host req)-            , ("url.scheme", toAttribute $ TextAttribute $ if secure req then "https" else "http")-            ,-              ( "network.protocol.version"-              , toAttribute $ case requestVersion req of-                  (HttpVersion major minor) -> T.pack (show major <> "." <> show minor)-              )-            ,-              ( "user_agent.original"-              , toAttribute $ maybe "" T.decodeUtf8 (lookup hUserAgent $ requestHeaders req)-              )-            ]-          addAttributes s-            $ H.fromList-            $ mapMaybe-              (\h -> (\v -> ("http.request.header." <> T.decodeUtf8 (foldedCase h), toAttribute (T.decodeUtf8 v))) <$> lookup h (requestHeaders req))-            $ requestHeadersToRecord conf+  updateName s $ fromMaybe methodText $ requestName conf -        addOldAttributes = do-          addAttributes-            s-            [ ("http.method", toAttribute $ T.decodeUtf8 $ method req)-            , ("http.url", toAttribute url)-            , ("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)-              )+  let addStableAttributes = do+        addAttributes+          s+          [ (unkey SC.http_request_method, toAttribute $ T.decodeUtf8 $ method req)+          , (unkey SC.url_full, toAttribute url)+          , (unkey SC.url_path, toAttribute $ T.decodeUtf8 $ path req)+          , (unkey SC.url_query, toAttribute $ T.decodeUtf8 $ queryString req)+          , (unkey SC.server_address, toAttribute $ T.decodeUtf8 $ host req)+          , (unkey SC.server_port, toAttribute $ port req)+          , (unkey SC.url_scheme, toAttribute $ TextAttribute $ if secure req then "https" else "http")+          , (unkey SC.network_protocol_version, toAttribute $ httpVersionText (requestVersion req))+          , (unkey SC.userAgent_original, toAttribute $ maybe "" T.decodeUtf8 (lookup hUserAgent $ requestHeaders req))+          ]+        addAttributes s+          $ H.fromList+          $ mapMaybe+            (\h -> (\v -> ("http.request.header." <> T.decodeUtf8 (foldedCase h), toAttribute (T.decodeUtf8 v))) <$> lookup h (requestHeaders req))+          $ requestHeadersToRecord conf++      addOldAttributes = do+        addAttributes+          s+          [ (unkey SC.http_method, toAttribute $ T.decodeUtf8 $ method req)+          , (unkey SC.http_url, toAttribute url)+          , (unkey SC.http_target, toAttribute $ T.decodeUtf8 (path req <> queryString req))+          , (unkey SC.http_host, toAttribute $ T.decodeUtf8 $ host req)+          , (unkey SC.http_scheme, toAttribute $ TextAttribute $ if secure req then "https" else "http")+          , (unkey SC.http_flavor, toAttribute $ httpVersionText (requestVersion req))+          , (unkey SC.http_userAgent, toAttribute $ maybe "" T.decodeUtf8 (lookup hUserAgent $ requestHeaders req))+          ]+        addAttributes s+          $ H.fromList+          $ mapMaybe+            (\h -> (\v -> ("http.request.header." <> T.decodeUtf8 (foldedCase h), toAttribute (T.decodeUtf8 v))) <$> lookup h (requestHeaders req))+          $ requestHeadersToRecord conf++  semanticsOptions <- liftIO getSemanticsOptions+  case httpOption semanticsOptions of+    Stable -> addStableAttributes+    StableAndOld -> addStableAttributes >> addOldAttributes+    Old -> addOldAttributes+++-- | Record response attributes on a specific span.+instrumentResponseOnSpan+  :: (MonadIO m)+  => HttpClientInstrumentationConfig+  -> Span+  -> Response a+  -> m ()+instrumentResponseOnSpan conf s resp = do+  let sc = statusCode (responseStatus resp)+      errorAttrs+        | sc >= 400 = [(unkey SC.error_type, toAttribute (T.pack $ show sc))]+        | otherwise = []+      headerAttrs =+        H.fromList+          $ mapMaybe+            (\h -> (\v -> ("http.response.header." <> T.decodeUtf8 (foldedCase h), toAttribute (T.decodeUtf8 v))) <$> lookup h (responseHeaders resp))+          $ responseHeadersToRecord conf++  semanticsOptions <- liftIO getSemanticsOptions+  case httpOption semanticsOptions of+    Stable ->+      addAttributes s $+        H.fromList ((unkey SC.http_response_statusCode, toAttribute sc) : errorAttrs)+          `H.union` headerAttrs+    StableAndOld ->+      addAttributes s $+        H.fromList+          ( [ (unkey SC.http_response_statusCode, toAttribute sc)+            , (unkey SC.http_statusCode, toAttribute sc)             ]-          addAttributes s-            $ H.fromList-            $ mapMaybe-              (\h -> (\v -> ("http.request.header." <> T.decodeUtf8 (foldedCase h), toAttribute (T.decodeUtf8 v))) <$> lookup h (requestHeaders req))-            $ requestHeadersToRecord conf+              <> errorAttrs+          )+          `H.union` headerAttrs+    Old ->+      addAttributes s $+        H.fromList ((unkey SC.http_statusCode, toAttribute sc) : errorAttrs)+          `H.union` headerAttrs -    semanticsOptions <- liftIO getSemanticsOptions-    case httpOption semanticsOptions of-      Stable -> addStableAttributes-      StableAndOld -> addStableAttributes >> addOldAttributes-      Old -> addOldAttributes+  when (sc >= 400) $+    setStatus s (Error "") -  hdrs <- inject (getTracerProviderPropagators $ getTracerTracerProvider tracer) ctxt $ requestHeaders req++{- | Add request attributes and inject propagation headers.++This is the original low-level function. For most use cases, prefer+'instrumentManagerSettings' or 'tracedHttpRequest'.++@since 0.1.0.0+-}+instrumentRequest+  :: (MonadIO m)+  => HttpClientInstrumentationConfig+  -> Ctx.Context+  -> Request+  -> m Request+instrumentRequest conf ctxt req = do+  forM_ (lookupSpan ctxt) $ \s ->+    addRequestAttributes conf s req++  propagator <- liftIO getGlobalTextMapPropagator+  hdrs <- injectToHeaders propagator ctxt $ requestHeaders req   pure $     req       { requestHeaders = hdrs       }  +{- | Record response attributes on the active span in the given context.++This is the original low-level function. For most use cases, prefer+'instrumentManagerSettings' or 'tracedHttpRequest'.++@since 0.1.0.0+-} instrumentResponse   :: (MonadIO m)   => HttpClientInstrumentationConfig-  -> Context+  -> Ctx.Context   -> Response a   -> m () instrumentResponse conf ctxt resp = do-  tracer <- httpTracerProvider-  ctxt' <- extract (getTracerProviderPropagators $ getTracerTracerProvider tracer) (responseHeaders resp) ctxt-  _ <- attachContext ctxt'-  forM_ (lookupSpan ctxt') $ \s -> do-    when (statusCode (responseStatus resp) >= 400) $ do-      setStatus s (Error "")-    let addStableAttributes = do-          addAttributes-            s-            [ ("http.response.statusCode", toAttribute $ statusCode $ responseStatus resp)-            -- TODO-            -- , ("http.request.body.size",	_)-            -- , ("http.request_content_length_uncompressed",	_)-            -- , ("http.response.body.size", _)-            -- , ("http.response_content_length_uncompressed", _)-            -- , ("net.transport")-            -- , ("server.address")-            -- , ("server.port")-            ]-          addAttributes s-            $ H.fromList-            $ mapMaybe-              (\h -> (\v -> ("http.response.header." <> T.decodeUtf8 (foldedCase h), toAttribute (T.decodeUtf8 v))) <$> lookup h (responseHeaders resp))-            $ responseHeadersToRecord conf-        addOldAttributes = do-          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-            $ H.fromList-            $ mapMaybe-              (\h -> (\v -> ("http.response.header." <> T.decodeUtf8 (foldedCase h), toAttribute (T.decodeUtf8 v))) <$> lookup h (responseHeaders resp))-            $ responseHeadersToRecord conf+  propagator <- liftIO getGlobalTextMapPropagator+  ctxt' <- extractFromHeaders propagator (responseHeaders resp) ctxt+  forM_ (lookupSpan ctxt') $ \s ->+    instrumentResponseOnSpan conf s resp -    semanticsOptions <- liftIO getSemanticsOptions-    case httpOption semanticsOptions of-      Stable -> addStableAttributes-      StableAndOld -> addStableAttributes >> addOldAttributes-      Old -> addOldAttributes++httpVersionText :: HttpVersion -> T.Text+httpVersionText (HttpVersion major minor) =+  TL.toStrict $ toLazyText $ decimal major <> "." <> decimal minor+++headersToTextMap :: RequestHeaders -> TextMap+headersToTextMap = textMapFromList . map (\(k, v) -> (T.decodeUtf8 (foldedCase k), T.decodeUtf8 v))+++injectToHeaders :: (MonadIO m) => TextMapPropagator -> Ctx.Context -> RequestHeaders -> m RequestHeaders+injectToHeaders propagator ctx existingHeaders = do+  tm <- inject propagator ctx (headersToTextMap existingHeaders)+  pure $ map (\(k, v) -> (CI.mk (T.encodeUtf8 k), T.encodeUtf8 v)) (textMapToList tm)+++extractFromHeaders :: (MonadIO m) => TextMapPropagator -> ResponseHeaders -> Ctx.Context -> m Ctx.Context+extractFromHeaders propagator hdrs = extract propagator (headersToTextMap hdrs)
src/OpenTelemetry/Instrumentation/HttpClient/Simple.hs view
@@ -1,7 +1,17 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} +{- | OpenTelemetry instrumentation for @http-conduit@ \/ @Network.HTTP.Simple@.++Provides drop-in replacements for the @http-conduit@ convenience functions.+Zero-config versions use default settings; primed variants accept+'HttpClientInstrumentationConfig'.++For Manager-level instrumentation (recommended for most applications), see+"OpenTelemetry.Instrumentation.HttpClient".+-} module OpenTelemetry.Instrumentation.HttpClient.Simple (+  -- * Zero-config drop-in replacements   httpBS,   httpLBS,   httpNoBody,@@ -10,8 +20,22 @@   httpSink,   httpSource,   withResponse,++  -- * Explicit-config variants+  httpBS',+  httpLBS',+  httpNoBody',+  httpJSON',+  httpJSONEither',+  httpSink',+  httpSource',+  withResponse',++  -- * Configuration   httpClientInstrumentationConfig,   HttpClientInstrumentationConfig (..),++  -- * Re-exports   module X, ) where @@ -34,8 +58,44 @@ spanArgs = defaultSpanArguments {kind = Client}  -httpBS :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> m (Simple.Response B.ByteString)-httpBS httpConf req = do+-- Zero-config versions++httpBS :: (MonadUnliftIO m, HasCallStack) => Simple.Request -> m (Simple.Response B.ByteString)+httpBS = httpBS' mempty+++httpLBS :: (MonadUnliftIO m, HasCallStack) => Simple.Request -> m (Simple.Response L.ByteString)+httpLBS = httpLBS' mempty+++httpNoBody :: (MonadUnliftIO m, HasCallStack) => Simple.Request -> m (Simple.Response ())+httpNoBody = httpNoBody' mempty+++httpJSON :: (MonadUnliftIO m, FromJSON a, HasCallStack) => Simple.Request -> m (Simple.Response a)+httpJSON = httpJSON' mempty+++httpJSONEither :: (FromJSON a, MonadUnliftIO m, HasCallStack) => Simple.Request -> m (Simple.Response (Either Simple.JSONException a))+httpJSONEither = httpJSONEither' mempty+++httpSink :: (MonadUnliftIO m, HasCallStack) => Simple.Request -> (Simple.Response () -> ConduitM B.ByteString Void m a) -> m a+httpSink = httpSink' mempty+++httpSource :: (MonadUnliftIO m, MonadResource m, HasCallStack) => Simple.Request -> (Simple.Response (ConduitM i B.ByteString m ()) -> ConduitM i o m r) -> ConduitM i o m r+httpSource = httpSource' mempty+++withResponse :: (MonadUnliftIO m, HasCallStack) => Simple.Request -> (Simple.Response (ConduitM i B.ByteString m ()) -> m a) -> m a+withResponse = withResponse' mempty+++-- Explicit-config variants++httpBS' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> m (Simple.Response B.ByteString)+httpBS' httpConf req = do   tracer <- httpTracerProvider   inSpan' tracer "httpBS" (addAttributesToSpanArguments callerAttributes spanArgs) $ \_s -> do     ctxt <- getContext@@ -45,8 +105,8 @@     pure resp  -httpLBS :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> m (Simple.Response L.ByteString)-httpLBS httpConf req = do+httpLBS' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> m (Simple.Response L.ByteString)+httpLBS' httpConf req = do   tracer <- httpTracerProvider   inSpan' tracer "httpLBS" (addAttributesToSpanArguments callerAttributes spanArgs) $ \_s -> do     ctxt <- getContext@@ -56,8 +116,8 @@     pure resp  -httpNoBody :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> m (Simple.Response ())-httpNoBody httpConf req = do+httpNoBody' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> m (Simple.Response ())+httpNoBody' httpConf req = do   tracer <- httpTracerProvider   inSpan' tracer "httpNoBody" (addAttributesToSpanArguments callerAttributes spanArgs) $ \_s -> do     ctxt <- getContext@@ -67,8 +127,8 @@     pure resp  -httpJSON :: (MonadUnliftIO m, FromJSON a, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> m (Simple.Response a)-httpJSON httpConf req = do+httpJSON' :: (MonadUnliftIO m, FromJSON a, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> m (Simple.Response a)+httpJSON' httpConf req = do   tracer <- httpTracerProvider   inSpan' tracer "httpJSON" (addAttributesToSpanArguments callerAttributes spanArgs) $ \_s -> do     ctxt <- getContext@@ -78,8 +138,8 @@     pure resp  -httpJSONEither :: (FromJSON a, MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> m (Simple.Response (Either Simple.JSONException a))-httpJSONEither httpConf req = do+httpJSONEither' :: (FromJSON a, MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> m (Simple.Response (Either Simple.JSONException a))+httpJSONEither' httpConf req = do   tracer <- httpTracerProvider   inSpan' tracer "httpJSONEither" (addAttributesToSpanArguments callerAttributes spanArgs) $ \_s -> do     ctxt <- getContext@@ -89,8 +149,8 @@     pure resp  -httpSink :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> (Simple.Response () -> ConduitM B.ByteString Void m a) -> m a-httpSink httpConf req f = do+httpSink' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> (Simple.Response () -> ConduitM B.ByteString Void m a) -> m a+httpSink' httpConf req f = do   tracer <- httpTracerProvider   inSpan' tracer "httpSink" (addAttributesToSpanArguments callerAttributes spanArgs) $ \_s -> do     ctxt <- getContext@@ -100,8 +160,8 @@       f resp  -httpSource :: (MonadUnliftIO m, MonadResource m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> (Simple.Response (ConduitM i B.ByteString m ()) -> ConduitM i o m r) -> ConduitM i o m r-httpSource httpConf req f = do+httpSource' :: (MonadUnliftIO m, MonadResource m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> (Simple.Response (ConduitM i B.ByteString m ()) -> ConduitM i o m r) -> ConduitM i o m r+httpSource' httpConf req f = do   tracer <- httpTracerProvider   Conduit.inSpan tracer "httpSource" (addAttributesToSpanArguments callerAttributes spanArgs) $ \_s -> do     ctxt <- lift getContext@@ -111,8 +171,8 @@       f resp  -withResponse :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> (Simple.Response (ConduitM i B.ByteString m ()) -> m a) -> m a-withResponse httpConf req f = do+withResponse' :: (MonadUnliftIO m, HasCallStack) => HttpClientInstrumentationConfig -> Simple.Request -> (Simple.Response (ConduitM i B.ByteString m ()) -> m a) -> m a+withResponse' httpConf req f = do   tracer <- httpTracerProvider   inSpan' tracer "withResponse" (addAttributesToSpanArguments callerAttributes spanArgs) $ \_s -> do     ctxt <- getContext
+ test/Spec.hs view
@@ -0,0 +1,106 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Data.IORef+import qualified Data.Text as T+import Network.HTTP.Client (parseRequest)+import OpenTelemetry.Attributes (lookupAttribute)+import OpenTelemetry.Attributes.Attribute (Attribute (..), PrimitiveAttribute (..))+import OpenTelemetry.Attributes.Key (unkey)+import qualified OpenTelemetry.Context as Context+import OpenTelemetry.Exporter.InMemory.Span (inMemoryListExporter)+import OpenTelemetry.Instrumentation.HttpClient.Raw+import qualified OpenTelemetry.SemanticConventions as SC+import OpenTelemetry.Trace.Core+import System.Environment (setEnv)+import Test.Hspec+++main :: IO ()+main = do+  setEnv "OTEL_SEMCONV_STABILITY_OPT_IN" "http"+  hspec spec+++withTestSpan :: T.Text -> (Context.Context -> Span -> IO a) -> IO (ImmutableSpan, a)+withTestSpan name action = do+  (processor, ref) <- inMemoryListExporter+  tp <- createTracerProvider [processor] emptyTracerProviderOptions+  setGlobalTracerProvider tp+  let tracer = makeTracer tp "test" tracerOptions+  s <- createSpan tracer Context.empty name (defaultSpanArguments {kind = Client})+  let ctx = Context.insertSpan s Context.empty+  result <- action ctx s+  endSpan s Nothing+  _ <- shutdownTracerProvider tp Nothing+  spans <- readIORef ref+  case spans of+    (clientSpan : _) -> pure (clientSpan, result)+    [] -> error "No spans recorded"+++spec :: Spec+spec = describe "HTTP client instrumentation" $ do+  describe "instrumentRequest" $ do+    it "sets span name to HTTP method (low cardinality)" $ do+      req <- parseRequest "http://example.com/users/123?q=test"+      (clientSpan, _) <- withTestSpan "HTTP" $ \ctx _s -> do+        _ <- instrumentRequest httpClientInstrumentationConfig ctx req+        pure ()+      hot <- readIORef (spanHot clientSpan)+      hotName hot `shouldBe` "GET"++    it "uses requestName override when provided" $ do+      req <- parseRequest "http://example.com/users/123"+      let conf = httpClientInstrumentationConfig {requestName = Just "custom-op"}+      (clientSpan, _) <- withTestSpan "HTTP" $ \ctx _s -> do+        _ <- instrumentRequest conf ctx req+        pure ()+      hot <- readIORef (spanHot clientSpan)+      hotName hot `shouldBe` "custom-op"++    it "sets http.request.method" $ do+      req <- parseRequest "POST http://example.com/api/data"+      (clientSpan, _) <- withTestSpan "HTTP" $ \ctx _s -> do+        _ <- instrumentRequest httpClientInstrumentationConfig ctx req+        pure ()+      hot <- readIORef (spanHot clientSpan)+      lookupAttribute (hotAttributes hot) (unkey SC.http_request_method)+        `shouldBe` Just (AttributeValue (TextAttribute "POST"))++    it "sets url.path" $ do+      req <- parseRequest "http://example.com/api/data"+      (clientSpan, _) <- withTestSpan "HTTP" $ \ctx _s -> do+        _ <- instrumentRequest httpClientInstrumentationConfig ctx req+        pure ()+      hot <- readIORef (spanHot clientSpan)+      lookupAttribute (hotAttributes hot) (unkey SC.url_path)+        `shouldBe` Just (AttributeValue (TextAttribute "/api/data"))++    it "sets server.address" $ do+      req <- parseRequest "http://example.com/test"+      (clientSpan, _) <- withTestSpan "HTTP" $ \ctx _s -> do+        _ <- instrumentRequest httpClientInstrumentationConfig ctx req+        pure ()+      hot <- readIORef (spanHot clientSpan)+      lookupAttribute (hotAttributes hot) (unkey SC.server_address)+        `shouldBe` Just (AttributeValue (TextAttribute "example.com"))++    it "sets server.port" $ do+      req <- parseRequest "http://example.com:8080/test"+      (clientSpan, _) <- withTestSpan "HTTP" $ \ctx _s -> do+        _ <- instrumentRequest httpClientInstrumentationConfig ctx req+        pure ()+      hot <- readIORef (spanHot clientSpan)+      lookupAttribute (hotAttributes hot) (unkey SC.server_port)+        `shouldBe` Just (AttributeValue (IntAttribute 8080))++    it "sets url.scheme to http" $ do+      req <- parseRequest "http://example.com/test"+      (clientSpan, _) <- withTestSpan "HTTP" $ \ctx _s -> do+        _ <- instrumentRequest httpClientInstrumentationConfig ctx req+        pure ()+      hot <- readIORef (spanHot clientSpan)+      lookupAttribute (hotAttributes hot) (unkey SC.url_scheme)+        `shouldBe` Just (AttributeValue (TextAttribute "http"))