packages feed

hs-opentelemetry-propagator-xray-1.0.0.0: src/OpenTelemetry/Propagator/XRay.hs

{-# LANGUAGE OverloadedStrings #-}

{- | AWS X-Ray Propagation Format.

The @X-Amzn-Trace-Id@ header is used by AWS services (ALB, API Gateway,
Lambda, etc.) to propagate trace context. The trace ID embeds a Unix
epoch timestamp in its first 4 bytes, but is otherwise a standard 128-bit
identifier that maps directly to an OpenTelemetry trace ID.

== Header format

@
X-Amzn-Trace-Id: Root=1-{epoch8hex}-{unique24hex};Parent={spanid16hex};Sampled={0|1}
@

== Interoperability with W3C Trace Context

Use alongside W3C propagators for mixed AWS / non-AWS environments:

@
OTEL_PROPAGATORS=tracecontext,baggage,xray
@

Both propagators will extract context from their respective headers.
On injection both headers are emitted, so downstream services can
consume whichever format they understand.

See <https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader>.
-}
module OpenTelemetry.Propagator.XRay (
  xrayPropagator,

  -- * Registry integration
  registerXRayPropagator,
) where

import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text.Encoding as TE
import OpenTelemetry.Common (TraceFlags (..))
import OpenTelemetry.Context (Context)
import qualified OpenTelemetry.Context as Context
import OpenTelemetry.Propagator (
  Propagator (..),
  TextMap,
  textMapInsert,
  textMapLookup,
 )
import OpenTelemetry.Propagator.XRay.Internal
import OpenTelemetry.Registry (registerTextMapPropagator)
import qualified OpenTelemetry.Trace.Core as Core
import OpenTelemetry.Trace.Id (Base (..), spanIdBaseEncodedBuilder)
import OpenTelemetry.Trace.TraceState (TraceState (..))


{- | Propagator for the AWS X-Ray @X-Amzn-Trace-Id@ header.

Extracts @Root@, @Parent@, and @Sampled@ fields from incoming headers
and injects them on outgoing headers.

@since 0.0.1.0
-}
xrayPropagator :: Propagator Context TextMap TextMap
xrayPropagator =
  Propagator
    { propagatorFields = [xrayTraceIdHeader]
    , extractor = \tm c ->
        case textMapLookup xrayTraceIdHeader tm of
          Nothing -> pure c
          Just val ->
            case decodeXRayHeader (TE.encodeUtf8 val) of
              Nothing -> pure c
              Just xh ->
                let sc =
                      Core.SpanContext
                        { Core.traceId = xhTraceId xh
                        , Core.spanId = xhSpanId xh
                        , Core.isRemote = True
                        , Core.traceFlags = case xhSampled xh of
                            Just True -> TraceFlags 1
                            _ -> TraceFlags 0 -- absent or explicitly unsampled
                        , Core.traceState = TraceState []
                        }
                in pure $ Context.insertSpan (Core.wrapSpanContext sc) c
    , injector = \c tm ->
        case Context.lookupSpan c of
          Nothing -> pure tm
          Just span' -> do
            sc <- Core.getSpanContext span'
            let root = TE.decodeUtf8 $ otelTraceIdToXRay (Core.traceId sc)
                parent =
                  TE.decodeUtf8 $
                    BL.toStrict $
                      BB.toLazyByteString $
                        spanIdBaseEncodedBuilder Base16 (Core.spanId sc)
                sampled = if Core.isSampled (Core.traceFlags sc) then "1" else "0"
                headerValue = "Root=" <> root <> ";Parent=" <> parent <> ";Sampled=" <> sampled
            pure $ textMapInsert xrayTraceIdHeader headerValue tm
    }


{- | Register the X-Ray propagator under the name @\"xray\"@ in the
global registry.

@since 0.0.1.0
-}
registerXRayPropagator :: IO ()
registerXRayPropagator =
  registerTextMapPropagator "xray" xrayPropagator