effectful-tracing-0.1.0.0: test/Effectful/Tracing/Instrumentation/MessagingSpec.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Effectful.Tracing.Instrumentation.MessagingSpec
-- Description : Tests for the framework-agnostic messaging span helpers.
--
-- These exercise the pure helpers ('operationTypeText', 'messagingSpanKind',
-- 'messagingSpanName', 'messagingAttributes') directly and run
-- 'withMessagingSpan' \/ 'withConsumerSpan' through the in-memory interpreter,
-- asserting the emitted span carries the producer \/ consumer kind, the
-- convention name, and exactly the @messaging.*@ attributes for the fields that
-- were set. The propagation round-trip ('injectMessageHeaders' \/
-- 'extractMessageHeaders') and the consumer continuing a remote parent are
-- covered too, since carrying the trace across the broker is the distinguishing
-- messaging behaviour. No live broker is involved.
module Effectful.Tracing.Instrumentation.MessagingSpec
( tests
) where
import Data.Maybe (isJust)
import Data.Text (Text)
import Effectful (Eff, IOE, runEff)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))
import Effectful.Tracing (Tracer, withSpan)
import Effectful.Tracing.Attribute
( Attribute (attributeKey, attributeValue)
, AttributeValue (AttrInt, AttrText)
)
import Effectful.Tracing.Instrumentation.Messaging
( MessagingOperation
( messagingBatchCount
, messagingBodySize
, messagingConversationId
, messagingDestination
, messagingDestinationTemplate
, messagingMessageId
, messagingOperationName
)
, MessagingOperationType (Create, Process, Receive, Send, Settle)
, extractMessageHeaders
, injectMessageHeaders
, messagingAttributes
, messagingOperation
, messagingSpanKind
, messagingSpanName
, operationTypeText
, withConsumerSpan
, withMessagingSpan
)
import Effectful.Tracing.Internal.Ids (traceIdToHex)
import Effectful.Tracing.Internal.Types
( Span
, SpanContext (spanContextTraceId)
, SpanKind (Client, Consumer, Producer)
, spanAttributes
, spanContext
, spanKind
, spanName
)
import Effectful.Tracing.Interpreter.InMemory
( newCapturedSpans
, readCapturedSpans
, runTracerInMemory
)
tests :: TestTree
tests =
testGroup
"Instrumentation.Messaging"
[ testGroup "operationTypeText" operationTypeCases
, testGroup "messagingSpanKind" spanKindCases
, testGroup "messagingSpanName" spanNameCases
, testGroup "messagingAttributes" attributeCases
, testGroup "withMessagingSpan" withMessagingSpanCases
, testGroup "propagation" propagationCases
]
operationTypeCases :: [TestTree]
operationTypeCases =
[ testCase "create" $ operationTypeText Create @?= "create"
, testCase "send" $ operationTypeText Send @?= "send"
, testCase "receive" $ operationTypeText Receive @?= "receive"
, testCase "process" $ operationTypeText Process @?= "process"
, testCase "settle" $ operationTypeText Settle @?= "settle"
]
spanKindCases :: [TestTree]
spanKindCases =
[ testCase "create is a producer" $ messagingSpanKind Create @?= Producer
, testCase "send is a producer" $ messagingSpanKind Send @?= Producer
, testCase "receive is a consumer" $ messagingSpanKind Receive @?= Consumer
, testCase "process is a consumer" $ messagingSpanKind Process @?= Consumer
, testCase "settle is a client" $ messagingSpanKind Settle @?= Client
]
spanNameCases :: [TestTree]
spanNameCases =
[ testCase "operation name and destination" $
messagingSpanName
(messagingOperation "kafka" Send) {messagingOperationName = Just "publish", messagingDestination = Just "orders"}
@?= "publish orders"
, testCase "falls back to the operation type for the leading word" $
messagingSpanName (messagingOperation "kafka" Send) {messagingDestination = Just "orders"}
@?= "send orders"
, testCase "prefers the destination template over the destination name" $
messagingSpanName
(messagingOperation "kafka" Send)
{ messagingDestination = Just "orders-42"
, messagingDestinationTemplate = Just "orders-{shard}"
}
@?= "send orders-{shard}"
, testCase "operation type only when no destination is known" $
messagingSpanName (messagingOperation "kafka" Receive) @?= "receive"
]
attributeCases :: [TestTree]
attributeCases =
[ testCase "system and operation type when nothing else is set" $
keys (messagingOperation "kafka" Send)
@?= ["messaging.system", "messaging.operation.type"]
, testCase "includes every set optional field in convention order" $
keys
(messagingOperation "kafka" Send)
{ messagingOperationName = Just "publish"
, messagingDestination = Just "orders"
, messagingDestinationTemplate = Just "orders-{shard}"
, messagingMessageId = Just "m-1"
, messagingConversationId = Just "c-1"
, messagingBodySize = Just 128
, messagingBatchCount = Just 4
}
@?= [ "messaging.system"
, "messaging.operation.type"
, "messaging.operation.name"
, "messaging.destination.name"
, "messaging.destination.template"
, "messaging.message.id"
, "messaging.message.conversation_id"
, "messaging.message.body.size"
, "messaging.batch.message_count"
]
, testCase "records the system name and operation type values" $ do
let ps = pairs (messagingOperation "rabbitmq" Process)
lookup "messaging.system" ps @?= Just (AttrText "rabbitmq")
lookup "messaging.operation.type" ps @?= Just (AttrText "process")
, testCase "records integer sizes as integer attributes" $ do
let ps = pairs (messagingOperation "kafka" Send) {messagingBatchCount = Just 4}
lookup "messaging.batch.message_count" ps @?= Just (AttrInt 4)
]
where
keys = map attributeKey . messagingAttributes
pairs = map (\a -> (attributeKey a, attributeValue a)) . messagingAttributes
withMessagingSpanCases :: [TestTree]
withMessagingSpanCases =
[ testCase "emits a producer-kind span named by the convention" $ do
spans <-
run $
withMessagingSpan
(messagingOperation "kafka" Send) {messagingDestination = Just "orders"}
(pure ())
case spans of
[s] -> do
spanName s @?= "send orders"
spanKind s @?= Producer
other -> fail ("expected exactly one captured span, got " <> show (length other))
, testCase "emits a consumer-kind span for a process operation" $ do
spans <- run (withMessagingSpan (messagingOperation "kafka" Process) (pure ()))
map spanKind spans @?= [Consumer]
, testCase "annotates the span with the messaging.* attributes" $ do
spans <-
run $
withMessagingSpan
(messagingOperation "kafka" Send) {messagingDestination = Just "orders"}
(pure ())
case spans of
[s] ->
map attributeKey (spanAttributes s)
@?= ["messaging.system", "messaging.operation.type", "messaging.destination.name"]
other -> fail ("expected exactly one captured span, got " <> show (length other))
, testCase "nests under an enclosing span" $ do
spans <- run (withSpan "outer" (withMessagingSpan (messagingOperation "kafka" Send) (pure ())))
map spanName spans @?= ["send", "outer"]
]
propagationCases :: [TestTree]
propagationCases =
[ testCase "inject emits a lowercase traceparent header for the active span" $ do
headers <- runResult (withSpan "outbound" injectMessageHeaders)
assertBool "a traceparent header is present" ("traceparent" `elem` map fst headers)
, testCase "inject emits no headers when there is no active span" $ do
headers <- runResult injectMessageHeaders
headers @?= []
, testCase "inject then extract round-trips to a remote context" $ do
headers <- runResult (withSpan "outbound" injectMessageHeaders)
assertBool "round-trips to a context" (isJust (extractMessageHeaders headers))
, testCase "consumer span continues the remote trace from the message headers" $ do
spans <-
run (withConsumerSpan remoteHeaders (messagingOperation "kafka" Process) (pure ()))
case spans of
[s] -> traceIdToHex (spanContextTraceId (spanContext s)) @?= remoteTraceIdHex
other -> fail ("expected exactly one captured span, got " <> show (length other))
, testCase "consumer span opens a fresh trace when headers carry no context" $ do
spans <-
run (withConsumerSpan [] (messagingOperation "kafka" Process) (pure ()))
case spans of
[s] ->
assertBool
"trace id is not the remote one"
(traceIdToHex (spanContextTraceId (spanContext s)) /= remoteTraceIdHex)
other -> fail ("expected exactly one captured span, got " <> show (length other))
]
-- | A canonical W3C @traceparent@ as a text message header, plus its trace id,
-- standing in for context a producer attached to a message.
remoteHeaders :: [(Text, Text)]
remoteHeaders =
[("traceparent", "00-" <> remoteTraceIdHex <> "-00f067aa0ba902b7-01")]
remoteTraceIdHex :: Text
remoteTraceIdHex = "4bf92f3577b34da6a3ce929d0e0e4736"
-- | Run a 'Tracer' computation through the in-memory interpreter, returning the
-- captured spans (innermost first, as the interpreter records them on close).
run :: Eff '[Tracer, IOE] a -> IO [Span]
run action = runEff $ do
captured <- newCapturedSpans
_ <- runTracerInMemory captured action
readCapturedSpans captured
-- | Run a 'Tracer' computation through the in-memory interpreter, discarding the
-- captured spans and returning the computation's result.
runResult :: Eff '[Tracer, IOE] a -> IO a
runResult action = runEff $ do
captured <- newCapturedSpans
runTracerInMemory captured action