packages feed

tower-hs-0.2.0.0: src/Tower/Middleware/Tracing.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : Tower.Middleware.Tracing
-- Description : Generic OpenTelemetry tracing middleware
-- License     : MIT
--
-- Wraps each service call in an OpenTelemetry span. Users provide a
-- 'TracingConfig' to control the span name, kind, and attribute extraction.
--
-- @
-- import Data.Function (('&'))
-- let config = ('defaultTracingConfig' "my-service")
--       { 'tracingReqAttrs' = \\req s -> addAttribute s "my.attr" (show req) }
-- svc '&' 'withTracingConfig' tracer config
-- @
module Tower.Middleware.Tracing
  ( TracingConfig(..)
  , defaultTracingConfig
  , withTracingConfig
  , withTracingGlobal
  ) where

import Data.Text (Text)

import OpenTelemetry.Attributes (emptyAttributes)
import OpenTelemetry.Trace.Core
  ( Tracer
  , Span
  , InstrumentationLibrary(..)
  , TracerOptions(..)
  , SpanArguments(..)
  , SpanKind(..)
  , SpanStatus(..)
  , makeTracer
  , getGlobalTracerProvider
  , defaultSpanArguments
  , addAttribute
  , setStatus
  , inSpan'
  )

import Tower.Service (Service(..), Middleware)
import Tower.Error (ServiceError, displayError)

-- | Configuration for the generic tracing middleware.
data TracingConfig req res = TracingConfig
  { tracingSpanName :: req -> Text
    -- ^ Derive the span name from the request.
  , tracingSpanKind :: SpanKind
    -- ^ The span kind (e.g., 'Client', 'Server', 'Internal').
  , tracingReqAttrs :: req -> Span -> IO ()
    -- ^ Add request attributes to the span before the call.
  , tracingResAttrs :: res -> Span -> IO ()
    -- ^ Add response attributes to the span after a successful call.
  , tracingErrAttrs :: ServiceError -> Span -> IO ()
    -- ^ Add error attributes to the span on failure (in addition to setting error status).
  }

-- | Default tracing config: fixed span name, 'Client' kind, no attributes.
-- On error, adds @error.type@ attribute with the error description.
defaultTracingConfig :: Text -> TracingConfig req res
defaultTracingConfig name = TracingConfig
  { tracingSpanName = const name
  , tracingSpanKind = Client
  , tracingReqAttrs = \_ _ -> pure ()
  , tracingResAttrs = \_ _ -> pure ()
  , tracingErrAttrs = \err s -> addAttribute s "error.type" (displayError err)
  }

-- | Tracing middleware using a specific 'Tracer' and 'TracingConfig'.
--
-- Wraps each service call in an OpenTelemetry span. On success, calls
-- 'tracingResAttrs' to record response attributes. On failure, sets
-- the span status to error with the 'ServiceError' description.
withTracingConfig :: Tracer -> TracingConfig req res -> Middleware req res
withTracingConfig tracer config inner = Service $ \req -> do
  let spanName = tracingSpanName config req
      spanArgs = defaultSpanArguments { kind = tracingSpanKind config }
  inSpan' tracer spanName spanArgs $ \s -> do
    tracingReqAttrs config req s
    result <- runService inner req
    case result of
      Right res -> do
        tracingResAttrs config res s
        pure (Right res)
      Left err -> do
        tracingErrAttrs config err s
        setStatus s (Error (displayError err))
        pure (Left err)

-- | 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.
withTracingGlobal :: InstrumentationLibrary -> TracingConfig req res -> Middleware req res
withTracingGlobal lib config inner = Service $ \req -> do
  tp <- getGlobalTracerProvider
  let tracer = makeTracer tp lib (TracerOptions Nothing)
  runService (withTracingConfig tracer config inner) req