packages feed

effectful-tracing-0.1.0.0: test/Effectful/Tracing/Propagation/B3Spec.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : Effectful.Tracing.Propagation.B3Spec
-- Description : Tests for B3 (Zipkin) propagation.
--
-- 'extractContextB3' is exercised directly with single-header and multi-header
-- B3 test vectors (the parse is the whole contract for inbound requests), and
-- the injectors are exercised through the in-memory interpreter, where an
-- active span exists. Round-trip tests confirm inject-then-extract preserves the
-- trace and span ids and the sampled flag in both wire encodings.
module Effectful.Tracing.Propagation.B3Spec
  ( tests
  ) where

import Data.ByteString (ByteString)
import Data.Maybe (isJust)

import Effectful (Eff, IOE, runEff)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))

import Effectful.Tracing (Tracer, withSpan)
import Effectful.Tracing.Internal.Ids (spanIdToHex, traceIdToHex)
import Effectful.Tracing.Internal.Types (SpanContext (..), isSampled)
import Effectful.Tracing.Interpreter.InMemory
  ( newCapturedSpans
  , runTracerInMemory
  )
import Effectful.Tracing.Propagation.B3
  ( b3FlagsHeader
  , b3Header
  , b3SampledHeader
  , b3SpanIdHeader
  , b3TraceIdHeader
  , extractContextB3
  , injectContextB3
  , injectContextB3Multi
  )

tests :: TestTree
tests =
  testGroup
    "Propagation.B3"
    [ testGroup "extractContextB3 (single header)" singleVectors
    , testGroup "extractContextB3 (multi header)" multiVectors
    , testGroup "inject" injectTests
    , testGroup "round-trip" roundTripTests
    ]

traceId128 :: ByteString
traceId128 = "4bf92f3577b34da6a3ce929d0e0e4736"

spanId :: ByteString
spanId = "00f067aa0ba902b7"

singleVectors :: [TestTree]
singleVectors =
  [ testCase "parses a 128-bit single header with sampling" $ do
      let context = extractContextB3 [(b3Header, traceId128 <> "-" <> spanId <> "-1")]
      fmap (traceIdToHex . spanContextTraceId) context @?= Just "4bf92f3577b34da6a3ce929d0e0e4736"
      fmap (spanIdToHex . spanContextSpanId) context @?= Just "00f067aa0ba902b7"
      fmap (isSampled . spanContextTraceFlags) context @?= Just True
      fmap spanContextIsRemote context @?= Just True
  , testCase "left-pads a 64-bit trace id to 128 bits" $ do
      let context = extractContextB3 [(b3Header, "a3ce929d0e0e4736-" <> spanId <> "-1")]
      fmap (traceIdToHex . spanContextTraceId) context
        @?= Just "0000000000000000a3ce929d0e0e4736"
  , testCase "deny sampling (0) parses as not sampled" $ do
      let context = extractContextB3 [(b3Header, traceId128 <> "-" <> spanId <> "-0")]
      fmap (isSampled . spanContextTraceFlags) context @?= Just False
  , testCase "debug (d) parses as sampled" $ do
      let context = extractContextB3 [(b3Header, traceId128 <> "-" <> spanId <> "-d")]
      fmap (isSampled . spanContextTraceFlags) context @?= Just True
  , testCase "absent sampling field defers to unsampled" $ do
      let context = extractContextB3 [(b3Header, traceId128 <> "-" <> spanId)]
      fmap (isSampled . spanContextTraceFlags) context @?= Just False
  , testCase "a trailing parent span id is accepted and ignored" $ do
      let context =
            extractContextB3
              [(b3Header, traceId128 <> "-" <> spanId <> "-1-05e3ac9a4f6e3b90")]
      fmap (spanIdToHex . spanContextSpanId) context @?= Just "00f067aa0ba902b7"
  , testCase "the deny-only single value (0) yields Nothing" $
      extractContextB3 [(b3Header, "0")] @?= Nothing
  , testCase "an all-zero trace id is rejected" $
      extractContextB3 [(b3Header, "00000000000000000000000000000000-" <> spanId <> "-1")]
        @?= Nothing
  , testCase "a non-hex span id is rejected" $
      extractContextB3 [(b3Header, traceId128 <> "-zzzzzzzzzzzzzzzz-1")] @?= Nothing
  , testCase "absent headers yield Nothing" $
      extractContextB3 [] @?= Nothing
  , testCase "the b3 header lookup is case-insensitive" $ do
      let context = extractContextB3 [("B3", traceId128 <> "-" <> spanId <> "-1")]
      fmap (spanIdToHex . spanContextSpanId) context @?= Just "00f067aa0ba902b7"
  ]

multiVectors :: [TestTree]
multiVectors =
  [ testCase "parses the multi-header form with X-B3-Sampled" $ do
      let context =
            extractContextB3
              [ (b3TraceIdHeader, traceId128)
              , (b3SpanIdHeader, spanId)
              , (b3SampledHeader, "1")
              ]
      fmap (traceIdToHex . spanContextTraceId) context @?= Just "4bf92f3577b34da6a3ce929d0e0e4736"
      fmap (isSampled . spanContextTraceFlags) context @?= Just True
      fmap spanContextIsRemote context @?= Just True
  , testCase "X-B3-Flags: 1 (debug) implies sampled" $ do
      let context =
            extractContextB3
              [ (b3TraceIdHeader, traceId128)
              , (b3SpanIdHeader, spanId)
              , (b3FlagsHeader, "1")
              ]
      fmap (isSampled . spanContextTraceFlags) context @?= Just True
  , testCase "X-B3-Sampled: 0 parses as not sampled" $ do
      let context =
            extractContextB3
              [ (b3TraceIdHeader, traceId128)
              , (b3SpanIdHeader, spanId)
              , (b3SampledHeader, "0")
              ]
      fmap (isSampled . spanContextTraceFlags) context @?= Just False
  , testCase "missing X-B3-SpanId yields Nothing" $
      extractContextB3 [(b3TraceIdHeader, traceId128)] @?= Nothing
  , testCase "the single header is preferred when both forms are present" $ do
      -- The single header carries the real ids; the multi headers are decoys.
      let context =
            extractContextB3
              [ (b3Header, traceId128 <> "-" <> spanId <> "-1")
              , (b3TraceIdHeader, "00000000000000000000000000000000")
              , (b3SpanIdHeader, "0000000000000000")
              ]
      fmap (traceIdToHex . spanContextTraceId) context @?= Just "4bf92f3577b34da6a3ce929d0e0e4736"
  ]

injectTests :: [TestTree]
injectTests =
  [ testCase "emits a single b3 header for the active span" $ do
      headers <- run (withSpan "outbound" injectContextB3)
      assertBool "a b3 header is present" (b3Header `elem` map fst headers)
  , testCase "emits multi headers for the active span" $ do
      headers <- run (withSpan "outbound" injectContextB3Multi)
      let names = map fst headers
      assertBool "X-B3-TraceId present" (b3TraceIdHeader `elem` names)
      assertBool "X-B3-SpanId present" (b3SpanIdHeader `elem` names)
      assertBool "X-B3-Sampled present" (b3SampledHeader `elem` names)
  , testCase "emits no single header when there is no active span" $ do
      headers <- run injectContextB3
      headers @?= []
  , testCase "emits no multi headers when there is no active span" $ do
      headers <- run injectContextB3Multi
      headers @?= []
  ]

roundTripTests :: [TestTree]
roundTripTests =
  [ testCase "single-header inject then extract preserves ids and sampled flag" $ do
      headers <- run (withSpan "outbound" injectContextB3)
      let extracted = extractContextB3 headers
      assertBool "round-trips to a context" (isJust extracted)
      fmap (isSampled . spanContextTraceFlags) extracted @?= Just True
      fmap spanContextIsRemote extracted @?= Just True
  , testCase "multi-header inject then extract preserves ids and sampled flag" $ do
      headers <- run (withSpan "outbound" injectContextB3Multi)
      let extracted = extractContextB3 headers
      assertBool "round-trips to a context" (isJust extracted)
      fmap (isSampled . spanContextTraceFlags) extracted @?= Just True
      fmap spanContextIsRemote extracted @?= Just True
  ]

-- | Run a 'Tracer' computation through the in-memory interpreter, discarding the
-- captured spans and returning the computation's result.
run :: Eff '[Tracer, IOE] a -> IO a
run action = runEff $ do
  captured <- newCapturedSpans
  runTracerInMemory captured action