pgmq-effectful-0.1.0.0: src/Pgmq/Effectful/Interpreter/Traced.hs
-- | OpenTelemetry-instrumented interpreter for the Pgmq effect.
--
-- This module provides a traced version of the Pgmq interpreter that
-- creates OpenTelemetry spans for all PGMQ operations.
--
-- == Span Kinds
--
-- * Producer spans: Queue creation, message send operations
-- * Consumer spans: Message read operations
-- * Internal spans: Message lifecycle (delete, archive, visibility timeout)
--
-- == Usage
--
-- @
-- import OpenTelemetry.Trace qualified as OTel
-- import Pgmq.Effectful.Interpreter.Traced
--
-- main :: IO ()
-- main = do
-- tracerProvider <- OTel.getGlobalTracerProvider
-- let tracer = OTel.makeTracer tracerProvider "my-app" OTel.tracerOptions
-- pool <- ...
--
-- runEff
-- . runError @PgmqError
-- . runPgmqTraced pool tracer
-- $ do
-- createQueue "my-queue"
-- sendMessage $ SendMessage "my-queue" (MessageBody "hello") Nothing
-- @
module Pgmq.Effectful.Interpreter.Traced
( -- * Interpreters
runPgmqTraced,
runPgmqTracedWith,
-- * Configuration
TracingConfig (..),
defaultTracingConfig,
)
where
import Control.Monad (when)
import Data.Text (Text)
import Data.Text qualified as T
import Effectful (Eff, IOE, (:>))
import Effectful qualified
import Effectful.Dispatch.Dynamic (interpret)
import Hasql.Pool (Pool, UsageError)
import Hasql.Pool qualified as Pool
import Hasql.Session qualified
import OpenTelemetry.Attributes (Attribute (..), PrimitiveAttribute (..))
import OpenTelemetry.Attributes.Map qualified as AttrMap
import OpenTelemetry.Trace.Core qualified as OTel
import Pgmq.Effectful.Effect (Pgmq (..))
import Pgmq.Effectful.Telemetry
import Pgmq.Hasql.Sessions qualified as Sessions
import Pgmq.Hasql.Statements.Types qualified as Types
import Pgmq.Types (MessageId (..), QueueName, queueNameToText)
-- | Configuration for OpenTelemetry tracing.
data TracingConfig = TracingConfig
{ -- | The OpenTelemetry tracer to use
tracer :: !OTel.Tracer,
-- | Whether to record exceptions on spans
recordExceptions :: !Bool,
-- | Whether to include message bodies in spans (may contain PII)
includeMessageBodies :: !Bool
}
-- | Create a default tracing configuration.
defaultTracingConfig :: OTel.Tracer -> TracingConfig
defaultTracingConfig t =
TracingConfig
{ tracer = t,
recordExceptions = True,
includeMessageBodies = False
}
-- | Run the Pgmq effect with OpenTelemetry instrumentation.
runPgmqTraced ::
(IOE :> es) =>
Pool ->
OTel.Tracer ->
Eff (Pgmq : es) a ->
Eff es a
runPgmqTraced pool t = runPgmqTracedWith pool (defaultTracingConfig t)
-- | Run the Pgmq effect with custom tracing configuration.
runPgmqTracedWith ::
(IOE :> es) =>
Pool ->
TracingConfig ->
Eff (Pgmq : es) a ->
Eff es a
runPgmqTracedWith pool config = interpret $ \_ -> \case
-- Queue Management (Producer spans)
CreateQueue q ->
withTracedSession config "pgmq create_queue" OTel.Producer q pool $
Sessions.createQueue q
DropQueue q ->
withTracedSession config "pgmq drop_queue" OTel.Producer q pool $
Sessions.dropQueue q
CreatePartitionedQueue pq@(Types.CreatePartitionedQueue qn _ _) ->
withTracedSession config "pgmq create_partitioned_queue" OTel.Producer qn pool $
Sessions.createPartitionedQueue pq
CreateUnloggedQueue q ->
withTracedSession config "pgmq create_unlogged_queue" OTel.Producer q pool $
Sessions.createUnloggedQueue q
DetachArchive _q ->
pure ()
EnableNotifyInsert cfg@(Types.EnableNotifyInsert qn _) ->
withTracedSession config "pgmq enable_notify_insert" OTel.Internal qn pool $
Sessions.enableNotifyInsert cfg
DisableNotifyInsert q ->
withTracedSession config "pgmq disable_notify_insert" OTel.Internal q pool $
Sessions.disableNotifyInsert q
CreateFifoIndex q ->
withTracedSession config "pgmq create_fifo_index" OTel.Internal q pool $
Sessions.createFifoIndex q
CreateFifoIndexesAll ->
withTracedSessionNoQueue config "pgmq create_fifo_indexes_all" OTel.Internal pool $
Sessions.createFifoIndexesAll
-- Message Operations - Send (Producer spans)
SendMessage msg@(Types.SendMessage qn _ _) ->
withTracedSession config "pgmq send" OTel.Producer qn pool $
Sessions.sendMessage msg
SendMessageForLater msg@(Types.SendMessageForLater qn _ _) ->
withTracedSession config "pgmq send" OTel.Producer qn pool $
Sessions.sendMessageForLater msg
BatchSendMessage msg@(Types.BatchSendMessage qn bodies _) ->
withTracedSessionWithCount config "pgmq send_batch" OTel.Producer qn (length bodies) pool $
Sessions.batchSendMessage msg
BatchSendMessageForLater msg@(Types.BatchSendMessageForLater qn bodies _) ->
withTracedSessionWithCount config "pgmq send_batch" OTel.Producer qn (length bodies) pool $
Sessions.batchSendMessageForLater msg
SendMessageWithHeaders msg@(Types.SendMessageWithHeaders qn _ _ _) ->
withTracedSession config "pgmq send" OTel.Producer qn pool $
Sessions.sendMessageWithHeaders msg
SendMessageWithHeadersForLater msg@(Types.SendMessageWithHeadersForLater qn _ _ _) ->
withTracedSession config "pgmq send" OTel.Producer qn pool $
Sessions.sendMessageWithHeadersForLater msg
BatchSendMessageWithHeaders msg@(Types.BatchSendMessageWithHeaders qn bodies _ _) ->
withTracedSessionWithCount config "pgmq send_batch" OTel.Producer qn (length bodies) pool $
Sessions.batchSendMessageWithHeaders msg
BatchSendMessageWithHeadersForLater msg@(Types.BatchSendMessageWithHeadersForLater qn bodies _ _) ->
withTracedSessionWithCount config "pgmq send_batch" OTel.Producer qn (length bodies) pool $
Sessions.batchSendMessageWithHeadersForLater msg
-- Message Operations - Read (Consumer spans)
ReadMessage query@(Types.ReadMessage qn _ _ _) ->
withTracedSession config "pgmq read" OTel.Consumer qn pool $
Sessions.readMessage query
ReadWithPoll query@(Types.ReadWithPollMessage qn _ _ _ _ _) ->
withTracedSession config "pgmq read_with_poll" OTel.Consumer qn pool $
Sessions.readWithPoll query
Pop query@(Types.PopMessage qn _) ->
withTracedSession config "pgmq pop" OTel.Consumer qn pool $
Sessions.pop query
-- FIFO Read (Consumer spans)
ReadGrouped query@(Types.ReadGrouped qn _ _) ->
withTracedSession config "pgmq read_grouped" OTel.Consumer qn pool $
Sessions.readGrouped query
ReadGroupedWithPoll query@(Types.ReadGroupedWithPoll qn _ _ _ _) ->
withTracedSession config "pgmq read_grouped_with_poll" OTel.Consumer qn pool $
Sessions.readGroupedWithPoll query
-- Round-robin FIFO Read (Consumer spans)
ReadGroupedRoundRobin query@(Types.ReadGrouped qn _ _) ->
withTracedSession config "pgmq read_grouped_rr" OTel.Consumer qn pool $
Sessions.readGroupedRoundRobin query
ReadGroupedRoundRobinWithPoll query@(Types.ReadGroupedWithPoll qn _ _ _ _) ->
withTracedSession config "pgmq read_grouped_rr_with_poll" OTel.Consumer qn pool $
Sessions.readGroupedRoundRobinWithPoll query
-- Message Lifecycle (Internal spans)
DeleteMessage query@(Types.MessageQuery qn msgId) ->
withTracedSessionWithMsgId config "pgmq delete" OTel.Internal qn msgId pool $
Sessions.deleteMessage query
BatchDeleteMessages query@(Types.BatchMessageQuery qn msgIds) ->
withTracedSessionWithCount config "pgmq delete_batch" OTel.Internal qn (length msgIds) pool $
Sessions.batchDeleteMessages query
ArchiveMessage query@(Types.MessageQuery qn msgId) ->
withTracedSessionWithMsgId config "pgmq archive" OTel.Internal qn msgId pool $
Sessions.archiveMessage query
BatchArchiveMessages query@(Types.BatchMessageQuery qn msgIds) ->
withTracedSessionWithCount config "pgmq archive_batch" OTel.Internal qn (length msgIds) pool $
Sessions.batchArchiveMessages query
DeleteAllMessagesFromQueue q ->
withTracedSession config "pgmq purge_queue" OTel.Internal q pool $
Sessions.deleteAllMessagesFromQueue q
ChangeVisibilityTimeout query@(Types.VisibilityTimeoutQuery qn msgId _) ->
withTracedSessionWithMsgId config "pgmq set_vt" OTel.Internal qn msgId pool $
Sessions.changeVisibilityTimeout query
BatchChangeVisibilityTimeout query@(Types.BatchVisibilityTimeoutQuery qn msgIds _) ->
withTracedSessionWithCount config "pgmq set_vt_batch" OTel.Internal qn (length msgIds) pool $
Sessions.batchChangeVisibilityTimeout query
-- Timestamp-based VT (pgmq 1.10.0+)
SetVisibilityTimeoutAt query@(Types.VisibilityTimeoutAtQuery qn msgId _) ->
withTracedSessionWithMsgId config "pgmq set_vt_at" OTel.Internal qn msgId pool $
Sessions.setVisibilityTimeoutAt query
BatchSetVisibilityTimeoutAt query@(Types.BatchVisibilityTimeoutAtQuery qn msgIds _) ->
withTracedSessionWithCount config "pgmq set_vt_at_batch" OTel.Internal qn (length msgIds) pool $
Sessions.batchSetVisibilityTimeoutAt query
-- Queue Observability (Internal spans)
ListQueues ->
withTracedSessionNoQueue config "pgmq list_queues" OTel.Internal pool $
Sessions.listQueues
QueueMetrics q ->
withTracedSession config "pgmq metrics" OTel.Internal q pool $
Sessions.queueMetrics q
AllQueueMetrics ->
withTracedSessionNoQueue config "pgmq metrics_all" OTel.Internal pool $
Sessions.allQueueMetrics
-- Internal helpers
withTracedSession ::
(IOE :> es) =>
TracingConfig ->
Text ->
OTel.SpanKind ->
QueueName ->
Pool ->
Hasql.Session.Session a ->
Eff es a
withTracedSession config spanName kind queueName pool session = do
let args = OTel.defaultSpanArguments {OTel.kind = kind}
Effectful.liftIO $ OTel.inSpan' config.tracer spanName args $ \s -> do
addQueueAttributes s queueName
runSessionIO config s pool session
withTracedSessionNoQueue ::
(IOE :> es) =>
TracingConfig ->
Text ->
OTel.SpanKind ->
Pool ->
Hasql.Session.Session a ->
Eff es a
withTracedSessionNoQueue config spanName kind pool session = do
let args = OTel.defaultSpanArguments {OTel.kind = kind}
Effectful.liftIO $ OTel.inSpan' config.tracer spanName args $ \s -> do
addBaseAttributes s
runSessionIO config s pool session
withTracedSessionWithMsgId ::
(IOE :> es) =>
TracingConfig ->
Text ->
OTel.SpanKind ->
QueueName ->
MessageId ->
Pool ->
Hasql.Session.Session a ->
Eff es a
withTracedSessionWithMsgId config spanName kind queueName msgId pool session = do
let args = OTel.defaultSpanArguments {OTel.kind = kind}
Effectful.liftIO $ OTel.inSpan' config.tracer spanName args $ \s -> do
addQueueAttributes s queueName
addMessageIdAttribute s msgId
runSessionIO config s pool session
withTracedSessionWithCount ::
(IOE :> es) =>
TracingConfig ->
Text ->
OTel.SpanKind ->
QueueName ->
Int ->
Pool ->
Hasql.Session.Session a ->
Eff es a
withTracedSessionWithCount config spanName kind queueName count pool session = do
let args = OTel.defaultSpanArguments {OTel.kind = kind}
Effectful.liftIO $ OTel.inSpan' config.tracer spanName args $ \s -> do
addQueueAttributes s queueName
addBatchCountAttribute s count
runSessionIO config s pool session
runSessionIO ::
TracingConfig ->
OTel.Span ->
Pool ->
Hasql.Session.Session a ->
IO a
runSessionIO config s pool session = do
result <- Pool.use pool session
case result of
Left err -> do
when config.recordExceptions $ recordUsageError s err
OTel.setStatus s (OTel.Error $ T.pack $ show err)
fail $ "PgmqPoolError: " <> show err
Right a -> do
OTel.setStatus s OTel.Ok
pure a
recordUsageError :: OTel.Span -> UsageError -> IO ()
recordUsageError s err = do
OTel.addEvent s $
OTel.NewEvent
{ OTel.newEventName = "exception",
OTel.newEventTimestamp = Nothing,
OTel.newEventAttributes =
AttrMap.fromList
[ ("exception.type", AttributeValue $ TextAttribute "UsageError"),
("exception.message", AttributeValue $ TextAttribute $ T.pack $ show err)
]
}
addQueueAttributes :: OTel.Span -> QueueName -> IO ()
addQueueAttributes s queueName = do
addBaseAttributes s
OTel.addAttribute s messagingDestinationName (queueNameToText queueName)
addBaseAttributes :: OTel.Span -> IO ()
addBaseAttributes s = do
OTel.addAttribute s messagingSystem ("pgmq" :: Text)
OTel.addAttribute s dbSystem ("postgresql" :: Text)
addMessageIdAttribute :: OTel.Span -> MessageId -> IO ()
addMessageIdAttribute s (MessageId msgId) =
OTel.addAttribute s messagingMessageId (T.pack $ show msgId)
addBatchCountAttribute :: OTel.Span -> Int -> IO ()
addBatchCountAttribute s count =
OTel.addAttribute s messagingBatchMessageCount count