packages feed

effectful-tracing-0.1.0.0: src/Effectful/Tracing/Propagation/Composite.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}

-- |
-- Module      : Effectful.Tracing.Propagation.Composite
-- Description : Combine several propagators into one inject\/extract pass.
-- Copyright   : (c) The effectful-tracing contributors
-- License     : BSD-3-Clause
-- Stability   : experimental
--
-- A real deployment rarely speaks exactly one wire format. A service may emit
-- W3C @traceparent@ for its own backend while still honouring inbound B3 headers
-- from a mesh, or run alongside legacy Jaeger clients during a migration.
-- OpenTelemetry models this with a __composite propagator__: a list of
-- single-format propagators that all run on inject (every format is written) and
-- are tried in order on extract (the first that parses wins).
--
-- This module packages each single-format propagator from
-- "Effectful.Tracing.Propagation", "Effectful.Tracing.Propagation.B3", and
-- "Effectful.Tracing.Propagation.Jaeger" as a first-class value
-- ('TraceContextPropagator' for the span context, 'BaggagePropagator' for
-- baggage), and provides the combinators that fan a list of them out on the way
-- out ('injectContextAll', 'injectBaggageAll') and collapse them on the way in
-- ('extractContextFirst', 'extractBaggageAll').
--
-- > -- write W3C and B3, accept either inbound
-- > let propagators = [w3cTraceContext, b3Single]
-- >
-- > -- server side: continue whichever format the caller used
-- > handle req = case extractContextFirst propagators (requestHeaders req) of
-- >   Just parent -> withRemoteParent parent (withSpan "handle" (serve req))
-- >   Nothing     -> withSpan "handle" (serve req)
-- >
-- > -- client side: emit every configured format
-- > call = withSpan "call.downstream" $ do
-- >   headers <- injectContextAll propagators
-- >   liftIO (httpGet url (baseHeaders <> headers))
--
-- Each standard propagator is tagged with the token name OpenTelemetry's
-- @OTEL_PROPAGATORS@ environment variable uses for it (@tracecontext@, @baggage@,
-- @b3@, @b3multi@, @jaeger@), and 'traceContextByToken' \/ 'baggageByToken' resolve
-- a token to its propagator. This is the foundation environment-variable
-- configuration builds on.
--
-- Like the underlying propagators, this works directly against the library's own
-- context under any interpreter, with no dependency on an OpenTelemetry SDK.
module Effectful.Tracing.Propagation.Composite
  ( -- * Trace-context propagators
    TraceContextPropagator (..)
  , w3cTraceContext
  , b3Single
  , b3Multi
  , jaegerTraceContext
  , traceContextByToken

    -- * Baggage propagators
  , BaggagePropagator (..)
  , w3cBaggage
  , jaegerBaggage
  , baggageByToken

    -- * Combining propagators
  , injectContextAll
  , extractContextFirst
  , injectBaggageAll
  , extractBaggageAll
  ) where

import Data.ByteString (ByteString)
import Data.Foldable (asum)
import Data.Text (Text)
import Network.HTTP.Types.Header (HeaderName)

import Effectful (Eff, (:>))

import Effectful.Tracing.Baggage
  ( Baggage
  , BaggageContext
  , baggageFromList
  , baggageToList
  )
import Effectful.Tracing.Effect (Tracer)
import Effectful.Tracing.Internal.Types (SpanContext)
import Effectful.Tracing.Propagation (extractContext, injectContext)
import Effectful.Tracing.Propagation.B3
  ( extractContextB3
  , injectContextB3
  , injectContextB3Multi
  )
import Effectful.Tracing.Propagation.Baggage (extractBaggage, injectBaggage)
import Effectful.Tracing.Propagation.Jaeger
  ( extractBaggageJaeger
  , extractContextJaeger
  , injectBaggageJaeger
  , injectContextJaeger
  )

-- | A single span-context propagator captured as a value: its
-- @OTEL_PROPAGATORS@ token name, its outbound 'inject', and its inbound
-- 'extract'. The standard ones are 'w3cTraceContext', 'b3Single', 'b3Multi', and
-- 'jaegerTraceContext'; construct your own to plug in a custom header scheme.
data TraceContextPropagator = TraceContextPropagator
  { traceContextName :: !Text
  -- ^ The @OTEL_PROPAGATORS@ token this propagator is configured by.
  , inject :: forall es. Tracer :> es => Eff es [(HeaderName, ByteString)]
  -- ^ Serialize the active span's context as outbound headers (@[]@ when there
  -- is no active span).
  , extract :: [(HeaderName, ByteString)] -> Maybe SpanContext
  -- ^ Parse a remote context from inbound headers, or 'Nothing' if this format
  -- is absent or malformed.
  }

-- | A single baggage propagator captured as a value: its @OTEL_PROPAGATORS@
-- token name, its outbound 'injectBag', and its inbound 'extractBag'. The standard
-- ones are 'w3cBaggage' and 'jaegerBaggage'.
data BaggagePropagator = BaggagePropagator
  { baggageName :: !Text
  -- ^ The @OTEL_PROPAGATORS@ token this propagator is configured by.
  , injectBag :: forall es. BaggageContext :> es => Eff es [(HeaderName, ByteString)]
  -- ^ Serialize the in-scope baggage as outbound headers (@[]@ when empty).
  , extractBag :: [(HeaderName, ByteString)] -> Baggage
  -- ^ Parse baggage from inbound headers (empty when absent).
  }

-- | The W3C Trace Context propagator (@traceparent@ \/ @tracestate@), token
-- @tracecontext@. Wraps "Effectful.Tracing.Propagation".
w3cTraceContext :: TraceContextPropagator
w3cTraceContext =
  TraceContextPropagator
    { traceContextName = "tracecontext"
    , inject = injectContext
    , extract = extractContext
    }

-- | The single-header B3 propagator (@b3@), token @b3@. Wraps
-- "Effectful.Tracing.Propagation.B3". On extract this reads either B3 form, so it
-- also accepts the multi-header encoding.
b3Single :: TraceContextPropagator
b3Single =
  TraceContextPropagator
    { traceContextName = "b3"
    , inject = injectContextB3
    , extract = extractContextB3
    }

-- | The multi-header B3 propagator (@X-B3-*@), token @b3multi@. Wraps
-- "Effectful.Tracing.Propagation.B3"; differs from 'b3Single' only in writing the
-- legacy multi-header form on inject.
b3Multi :: TraceContextPropagator
b3Multi =
  TraceContextPropagator
    { traceContextName = "b3multi"
    , inject = injectContextB3Multi
    , extract = extractContextB3
    }

-- | The Jaeger propagator (@uber-trace-id@), token @jaeger@. Wraps
-- "Effectful.Tracing.Propagation.Jaeger". Jaeger also carries baggage; that side
-- is 'jaegerBaggage'.
jaegerTraceContext :: TraceContextPropagator
jaegerTraceContext =
  TraceContextPropagator
    { traceContextName = "jaeger"
    , inject = injectContextJaeger
    , extract = extractContextJaeger
    }

-- | The W3C Baggage propagator (@baggage@ header), token @baggage@. Wraps
-- "Effectful.Tracing.Propagation.Baggage".
w3cBaggage :: BaggagePropagator
w3cBaggage =
  BaggagePropagator
    { baggageName = "baggage"
    , injectBag = injectBaggage
    , extractBag = extractBaggage
    }

-- | The Jaeger baggage propagator (@uberctx-@ headers), token @jaeger@. Wraps
-- the baggage side of "Effectful.Tracing.Propagation.Jaeger".
jaegerBaggage :: BaggagePropagator
jaegerBaggage =
  BaggagePropagator
    { baggageName = "jaeger"
    , injectBag = injectBaggageJaeger
    , extractBag = extractBaggageJaeger
    }

-- | Resolve an @OTEL_PROPAGATORS@ token to its trace-context propagator, or
-- 'Nothing' for an unknown token (or one, like @baggage@, that has no
-- trace-context side). Recognises @tracecontext@, @b3@, @b3multi@, and @jaeger@.
traceContextByToken :: Text -> Maybe TraceContextPropagator
traceContextByToken token =
  lookup token [(traceContextName p, p) | p <- standardTraceContextPropagators]

-- | Resolve an @OTEL_PROPAGATORS@ token to its baggage propagator, or 'Nothing'
-- for a token with no baggage side. Recognises @baggage@ and @jaeger@.
baggageByToken :: Text -> Maybe BaggagePropagator
baggageByToken token =
  lookup token [(baggageName p, p) | p <- standardBaggagePropagators]

-- | The standard trace-context propagators, in token order.
standardTraceContextPropagators :: [TraceContextPropagator]
standardTraceContextPropagators = [w3cTraceContext, b3Single, b3Multi, jaegerTraceContext]

-- | The standard baggage propagators, in token order.
standardBaggagePropagators :: [BaggagePropagator]
standardBaggagePropagators = [w3cBaggage, jaegerBaggage]

-- | Run every propagator's inject and concatenate the headers, so an outbound
-- request carries all configured formats at once. Returns @[]@ for an empty
-- list (or when there is no active span).
injectContextAll :: Tracer :> es => [TraceContextPropagator] -> Eff es [(HeaderName, ByteString)]
injectContextAll propagators = concat <$> traverse (\p -> inject p) propagators

-- | Try each propagator's extract in order and take the first that parses a
-- context, mirroring OpenTelemetry's composite extract. Returns 'Nothing' when
-- none of them match.
extractContextFirst :: [TraceContextPropagator] -> [(HeaderName, ByteString)] -> Maybe SpanContext
extractContextFirst propagators headers = asum [extract p headers | p <- propagators]

-- | Run every baggage propagator's inject and concatenate the headers. Returns
-- @[]@ for an empty list (or empty baggage).
injectBaggageAll :: BaggageContext :> es => [BaggagePropagator] -> Eff es [(HeaderName, ByteString)]
injectBaggageAll propagators = concat <$> traverse (\p -> injectBag p) propagators

-- | Extract baggage with every propagator and merge the results into one set.
-- Baggage is additive (unlike a single span context), so all formats
-- contribute; on a key present in more than one, the later propagator in the
-- list wins.
extractBaggageAll :: [BaggagePropagator] -> [(HeaderName, ByteString)] -> Baggage
extractBaggageAll propagators headers =
  baggageFromList (concatMap (\p -> baggageToList (extractBag p headers)) propagators)