packages feed

http-tower-hs-0.2.0.0: src/Network/HTTP/Tower/Middleware/Tracing.hs

{-# LANGUAGE OverloadedStrings #-}

module Network.HTTP.Tower.Middleware.Tracing
  ( withTracing
  , withTracingTracer
  ) where

import Control.Monad (when)
import Data.Text (Text, pack)
import Data.Text.Encoding (decodeUtf8)
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Types.Status as HTTP

import OpenTelemetry.Attributes (emptyAttributes)
import OpenTelemetry.Trace.Core
  ( Tracer
  , InstrumentationLibrary(..)
  , TracerOptions(..)
  , SpanKind(..)
  , SpanStatus(..)
  , makeTracer
  , getGlobalTracerProvider
  , addAttribute
  , setStatus
  )

import Network.HTTP.Tower.Client (HttpResponse)
import qualified Data.Version as V
import qualified Paths_http_tower_hs as Pkg

import Tower.Service (Service(..), Middleware)
import Tower.Middleware.Tracing (TracingConfig(..), defaultTracingConfig, withTracingConfig)

-- | Tracing middleware using the global TracerProvider.
-- Reads the global provider on each request (single IORef read, trivial cost).
-- If no TracerProvider is configured, this is a no-op (zero overhead).
--
-- @
-- let client' = client |> withTracing
-- @
withTracing :: Middleware HTTP.Request HttpResponse
withTracing inner = Service $ \req -> do
  tp <- getGlobalTracerProvider
  let tracer = makeTracer tp instrumentationLibrary (TracerOptions Nothing)
  runService (withTracingTracer tracer inner) req

-- | Tracing middleware using a specific Tracer.
-- Wraps each request in an OpenTelemetry span following
-- [stable HTTP semantic conventions](https://opentelemetry.io/docs/specs/semconv/http/http-spans/).
--
-- * Span name: @{method}@ (e.g., @GET@)
-- * Span kind: Client
-- * Attributes: @http.request.method@, @server.address@, @server.port@,
--   @url.full@, @http.response.status_code@, @error.type@
withTracingTracer :: Tracer -> Middleware HTTP.Request HttpResponse
withTracingTracer tracer = withTracingConfig tracer httpTracingConfig

httpTracingConfig :: TracingConfig HTTP.Request HttpResponse
httpTracingConfig = (defaultTracingConfig "")
  { tracingSpanName = decodeUtf8 . HTTP.method
  , tracingSpanKind = Client
  , tracingReqAttrs = \req s -> do
      addAttribute s "http.request.method" (decodeUtf8 (HTTP.method req))
      addAttribute s "server.address" (decodeUtf8 (HTTP.host req))
      addAttribute s "server.port" (HTTP.port req)
      addAttribute s "url.full" (buildUrl req)
  , tracingResAttrs = \resp s -> do
      let code = HTTP.statusCode (HTTP.responseStatus resp)
      addAttribute s "http.response.status_code" code
      when (code >= 400) $ do
        addAttribute s "error.type" (pack (show code))
        setStatus s (Error "HTTP error status")
  }

instrumentationLibrary :: InstrumentationLibrary
instrumentationLibrary = InstrumentationLibrary
  { libraryName = "http-tower-hs"
  , libraryVersion = pack (V.showVersion Pkg.version)
  , librarySchemaUrl = ""
  , libraryAttributes = emptyAttributes
  }

-- | Build a full URL from an http-client Request.
buildUrl :: HTTP.Request -> Text
buildUrl req =
  let scheme = if HTTP.secure req then "https" else "http" :: Text
      host = decodeUtf8 (HTTP.host req)
      port = HTTP.port req
      path = decodeUtf8 (HTTP.path req)
      query = decodeUtf8 (HTTP.queryString req)
      showPort = case (HTTP.secure req, port) of
        (True, 443)  -> ""
        (False, 80)  -> ""
        _            -> ":" <> pack (show port)
  in scheme <> "://" <> host <> showPort <> path <> query