kafka-effectful-0.2.0.0: src/Kafka/Effectful/OpenTelemetry/Producer/Interpreter.hs
-- The 'EffectHandler' type synonym in effectful-core expands to a
-- constraint that GHC's redundant-constraint check flags on the
-- handler's signature, even though the constraint is required for
-- 'interpret' to type-check. Suppress the warning at the file level.
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
{- | OpenTelemetry-traced interpreter for the 'KafkaProducer' effect.
Drop-in alternative to 'Kafka.Effectful.Producer.Interpreter.runKafkaProducer'
that opens a Producer-kind span around every record-sending operation
('produceMessage', 'produceMessage'', 'produceMessageSync',
'produceMessageBatch'), populates the span with the spec-aligned
@messaging.*@ attribute set, and injects the current OTel context as
W3C @traceparent@\/@tracestate@ headers on the record before handing
it off to the underlying @hw-kafka-client@ produce call.
Non-sending operations (flush, transactional begin\/commit\/abort, etc.)
are passed through unchanged — they do not represent message sends and
therefore do not get a span.
The design parallels the upstream
@hs-opentelemetry-instrumentation-hw-kafka-client@\'s
@OpenTelemetry.Instrumentation.Kafka.produceMessage@.
@since 0.2.0.0
-}
module Kafka.Effectful.OpenTelemetry.Producer.Interpreter (
-- * Interpreter
runKafkaProducerTraced,
)
where
import Control.Concurrent.MVar qualified as Concurrent
import Data.Foldable (for_)
import Effectful (Eff, IOE, (:>))
import Effectful qualified
import Effectful.Dispatch.Dynamic (EffectHandler, interpret)
import Effectful.Error.Static (Error, throwError)
import Effectful.Exception qualified as Exception
import Kafka.Effectful.OpenTelemetry.Propagation (
injectTraceContextIntoRecord,
)
import Kafka.Effectful.OpenTelemetry.Semantic (
producerRecordAttributes,
producerSpanName,
)
import Kafka.Effectful.Producer.Effect (KafkaProducer (..))
import Kafka.Producer (ProducerRecord (prTopic))
import Kafka.Producer qualified as K
import Kafka.Producer.ProducerProperties (ProducerProperties)
import Kafka.Transaction qualified as K
import Kafka.Types (KafkaError)
import OpenTelemetry.Context qualified as Context
import OpenTelemetry.Context.ThreadLocal (getContext)
import OpenTelemetry.Trace.Core (
SpanArguments (kind),
SpanKind (Producer),
Tracer,
addAttributesToSpanArguments,
defaultSpanArguments,
inSpan'',
)
{- | Run the 'KafkaProducer' effect with OpenTelemetry tracing.
Identical in shape to 'Kafka.Effectful.Producer.Interpreter.runKafkaProducer',
plus an additional 'Tracer' argument used to open a Producer-kind
span around every record-sending operation. The current
'OpenTelemetry.Context.Context' is injected as W3C trace-context
headers on the outgoing record before the underlying
@hw-kafka-client@ produce call runs, so that downstream consumers
can extract the context and continue the trace.
The producer handle is acquired and released via 'Exception.bracket',
exactly as 'runKafkaProducer' does. Errors are thrown via the
'Error' effect.
@since 0.2.0.0
-}
runKafkaProducerTraced ::
(IOE :> es, Error KafkaError :> es) =>
Tracer ->
ProducerProperties ->
Eff (KafkaProducer : es) a ->
Eff es a
runKafkaProducerTraced tracer props action =
Exception.bracket
acquire
(Effectful.liftIO . K.closeProducer)
(\producer -> interpret (handleTracedProducer tracer producer) action)
where
acquire = do
result <- Effectful.liftIO $ K.newProducer props
case result of
Left err -> throwError err
Right producer -> pure producer
handleTracedProducer ::
(IOE :> es, Error KafkaError :> es) =>
Tracer ->
K.KafkaProducer ->
EffectHandler KafkaProducer es
handleTracedProducer tracer producer _env = \case
ProduceMessage record ->
withProducerSpan tracer record $ \instrumentedRecord -> do
mbErr <- Effectful.liftIO $ K.produceMessage producer instrumentedRecord
for_ mbErr throwError
ProduceMessage' record cb ->
withProducerSpan tracer record $ \instrumentedRecord -> do
res <-
Effectful.liftIO $
K.produceMessage' producer instrumentedRecord cb
case res of
Left (K.ImmediateError err) -> throwError err
Right () -> pure ()
ProduceMessageSync record ->
withProducerSpan tracer record $ \instrumentedRecord -> do
var <- Effectful.liftIO Concurrent.newEmptyMVar
res <-
Effectful.liftIO $
K.produceMessage' producer instrumentedRecord (Concurrent.putMVar var)
case res of
Left (K.ImmediateError err) -> throwError err
Right () -> do
Effectful.liftIO $ K.flushProducer producer
report <- Effectful.liftIO $ Concurrent.takeMVar var
case report of
K.DeliverySuccess _ offset -> pure offset
K.DeliveryFailure _ err -> throwError err
K.NoMessageError err -> throwError err
ProduceMessageBatch records -> do
-- One span per record so the Producer-kind attributes
-- (partition, key) are per-record, matching the upstream
-- reference. Spans are opened sequentially as the list is
-- traversed.
results <-
traverse
( \r ->
withProducerSpan tracer r $ \instrumentedRecord -> do
mbErr <-
Effectful.liftIO $
K.produceMessage producer instrumentedRecord
pure (r, mbErr)
)
records
pure [(r, err) | (r, Just err) <- results]
FlushProducer ->
Effectful.liftIO $ K.flushProducer producer
InitTransactions timeout ->
throwOnJust $ K.initTransactions producer timeout
BeginTransaction ->
throwOnJust $ K.beginTransaction producer
CommitTransaction timeout ->
Effectful.liftIO $ K.commitTransaction producer timeout
AbortTransaction timeout ->
throwOnJust $ K.abortTransaction producer timeout
SendOffsetsToTransaction consumer record timeout ->
Effectful.liftIO $
K.commitOffsetMessageTransaction producer consumer record timeout
AskProducerHandle ->
pure producer
where
throwOnJust action' = do
mbErr <- Effectful.liftIO action'
for_ mbErr throwError
{- | Open a Producer-kind span around a record-sending action.
Builds the @messaging.*@ attribute set from the record, opens a
span named @\"send \<topic\>\"@, injects the current OTel context
as W3C trace-context headers onto a clone of the record, and runs
the supplied action with that instrumented record.
-}
withProducerSpan ::
(IOE :> es) =>
Tracer ->
ProducerRecord ->
(ProducerRecord -> Eff es a) ->
Eff es a
withProducerSpan tracer record action =
inSpan'' tracer (producerSpanName (prTopic record)) spanArgs $ \newSpan -> do
ctx <- getContext
instrumentedRecord <-
Effectful.liftIO $
injectTraceContextIntoRecord (Context.insertSpan newSpan ctx) record
action instrumentedRecord
where
spanArgs =
addAttributesToSpanArguments
(producerRecordAttributes record)
defaultSpanArguments{kind = Producer}