pgmq-effectful-0.2.0.0: src/Pgmq/Effectful/Traced.hs
-- | Traced operations for PGMQ with automatic trace-context propagation.
--
-- Higher-level helpers layered on top of the Pgmq effect:
--
-- * 'sendMessageTraced' writes the current trace context onto the
-- outgoing message headers using the tracer provider's /configured/
-- propagator (W3C Trace Context by default; swap in B3, Datadog, or
-- any other propagator by configuring the provider).
-- * 'readMessageWithContext' extracts the trace context from each
-- received message and returns an 'OTel.Context' suitable for
-- starting a child @process@ span that links back to the producer's
-- trace.
--
-- == Usage
--
-- @
-- -- Producer
-- provider <- OTel.getGlobalTracerProvider
-- sendMessageTraced provider queueName body Nothing
--
-- -- Consumer
-- messagesWithCtx <- readMessageWithContext provider readQuery
-- forM_ messagesWithCtx $ \\(msg, parentCtx) ->
-- -- parentCtx carries the propagated parent context
-- OTel.inSpan tracer \"process\" OTel.defaultSpanArguments
-- (processMessage msg)
-- @
module Pgmq.Effectful.Traced
( -- * Traced Send Operations
sendMessageTraced,
-- * Context-aware Read Operations
readMessageWithContext,
MessageWithContext,
)
where
import Data.Aeson (Value (..))
import Data.Vector (Vector)
import Data.Vector qualified as V
import Effectful (Eff, IOE, (:>))
import Effectful qualified
import OpenTelemetry.Context qualified as Ctxt
import OpenTelemetry.Context.ThreadLocal qualified as CtxtLocal
import OpenTelemetry.Trace.Core qualified as OTel
import Pgmq.Effectful.Effect (Pgmq, readMessage, sendMessageWithHeaders)
import Pgmq.Effectful.Telemetry
( extractTraceContext,
injectTraceContext,
jsonToTraceHeaders,
mergeTraceHeaders,
)
import Pgmq.Hasql.Statements.Types qualified as Types
import Pgmq.Types
-- | A message paired with the trace context extracted from its
-- headers. Use 'OpenTelemetry.Context.ThreadLocal.attachContext' (or
-- pass it to an @inSpan''@-shaped primitive that lets you set the
-- parent) to start a @process@ span linked to the producer's trace.
type MessageWithContext = (Message, Ctxt.Context)
-- | Send a message with trace context injected into its headers.
--
-- The current 'Ctxt.Context' is fetched via
-- 'OpenTelemetry.Context.ThreadLocal.getContext' and handed to the
-- tracer provider's configured propagator, which writes the propagated
-- fields (traceparent/tracestate for W3C, x-b3-* for B3, etc.) onto
-- the message headers. Any user-supplied headers win against
-- propagator-written ones (the merge is additive).
sendMessageTraced ::
(Pgmq :> es, IOE :> es) =>
OTel.TracerProvider ->
QueueName ->
MessageBody ->
Maybe Value ->
Eff es MessageId
sendMessageTraced provider queueName body existingHeaders = do
ctx <- Effectful.liftIO CtxtLocal.getContext
traceHeaders <- injectTraceContext provider ctx
let mergedHeaders = MessageHeaders $ mergeTraceHeaders traceHeaders existingHeaders
sendMessageWithHeaders $
Types.SendMessageWithHeaders
{ queueName = queueName,
messageBody = body,
messageHeaders = mergedHeaders,
delay = Nothing
}
-- | Read messages and extract trace context from their headers.
--
-- The returned 'Ctxt.Context' for each message is the
-- 'Ctxt.empty' context updated with whatever fields the tracer
-- provider's propagator was able to parse from the headers — so a
-- caller can open a child @process@ span linked to the producer's
-- trace without knowing which propagator is in use.
readMessageWithContext ::
(Pgmq :> es, IOE :> es) =>
OTel.TracerProvider ->
Types.ReadMessage ->
Eff es (Vector MessageWithContext)
readMessageWithContext provider readQuery = do
messages <- readMessage readQuery
Effectful.liftIO $
V.forM messages $ \msg -> do
parentCtx <- case msg.headers of
Just hdrs -> extractTraceContext provider (jsonToTraceHeaders hdrs) Ctxt.empty
Nothing -> pure Ctxt.empty
pure (msg, parentCtx)