kiroku-otel-0.1.0.0: src/Kiroku/Otel/TraceContext.hs
{- | W3C trace-context helpers that read and write @traceparent@ /
@tracestate@ header strings inside Kiroku event metadata.
The on-the-wire JSON shape inside the event's @metadata@ JSONB column is:
> {
> "traceparent": "00-<32-hex traceId>-<16-hex spanId>-<2-hex flags>",
> "tracestate": "<vendor entries, optional>"
> }
Other keys in @metadata@ are preserved by 'injectTraceContext'.
-}
module Kiroku.Otel.TraceContext (
injectTraceContext,
extractTraceContext,
) where
import Data.Aeson (Value (..))
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap (KeyMap)
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString (ByteString)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import GHC.IO (unsafePerformIO)
import Kiroku.Store.Types (EventData (..), RecordedEvent (..))
import OpenTelemetry.Propagator.W3CTraceContext (decodeSpanContext, encodeSpanContext)
import OpenTelemetry.Trace.Core (SpanContext, wrapSpanContext)
{- | Encode a 'SpanContext' as W3C @traceparent@ / @tracestate@ strings and
merge them into the @metadata@ JSON object of an 'EventData'. Existing
keys in @metadata@ are preserved; existing @traceparent@ / @tracestate@
keys (if any) are overwritten — the W3C spec mandates exactly one of
each value per propagation.
If the input @metadata@ is 'Nothing', or is a non-object JSON value,
the helper starts from an empty object before merging.
This function is pure: it uses 'unsafePerformIO' to call
'encodeSpanContext', which is observably pure on the frozen span
returned by 'wrapSpanContext' (no shared mutable state, no exceptions).
The \"unsafe\" annotation is mandatory to bridge the propagator's
@IO@-typed encoder to the pure interface this module exposes.
-}
{-# NOINLINE injectTraceContext #-}
injectTraceContext :: SpanContext -> EventData -> EventData
injectTraceContext sc ed =
case ed of
EventData{metadata = oldMeta} ->
ed{metadata = Just (Object (mergeTraceContext sc oldMeta))} :: EventData
{- | Pull a 'SpanContext' back out of a 'RecordedEvent'\'s @metadata@.
Returns 'Nothing' when @metadata@ is absent, is not a JSON object,
lacks a @traceparent@ key, or contains a @traceparent@ value that fails
W3C parsing. Never throws.
-}
extractTraceContext :: RecordedEvent -> Maybe SpanContext
extractTraceContext re = case re of
RecordedEvent{metadata = Just (Object o)} -> decodeFromObject o
_ -> Nothing
-- ---------------------------------------------------------------------------
-- Internal helpers
-- ---------------------------------------------------------------------------
mergeTraceContext :: SpanContext -> Maybe Value -> KeyMap Value
mergeTraceContext sc mMeta =
let (tp, ts) = unsafePerformIO (encodeSpanContext (wrapSpanContext sc))
tpText = TE.decodeUtf8 tp
tsText = TE.decodeUtf8 ts
existing = case mMeta of
Just (Object o) -> o
_ -> KM.empty
withTp = KM.insert (Key.fromText (T.pack "traceparent")) (String tpText) existing
in if T.null tsText
then withTp
else KM.insert (Key.fromText (T.pack "tracestate")) (String tsText) withTp
decodeFromObject :: KeyMap Value -> Maybe SpanContext
decodeFromObject o = do
String tpText <- KM.lookup (Key.fromText (T.pack "traceparent")) o
let tpBs :: ByteString
tpBs = TE.encodeUtf8 tpText
tsBs = case KM.lookup (Key.fromText (T.pack "tracestate")) o of
Just (String tsText) -> Just (TE.encodeUtf8 tsText)
_ -> Nothing
decodeSpanContext (Just tpBs) tsBs