packages feed

effectful-tracing-0.1.0.0: test/Effectful/Tracing/PropagationSpec.hs

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

-- |
-- Module      : Effectful.Tracing.PropagationSpec
-- Description : Tests for W3C Trace Context propagation.
--
-- 'extractContext' is exercised directly with W3C @traceparent@ test vectors
-- (the parse is the whole contract for inbound requests), and 'injectContext'
-- is exercised through the in-memory interpreter, where an active span exists.
-- A round-trip test confirms inject-then-extract preserves the trace and span
-- ids and the sampled flag.
module Effectful.Tracing.PropagationSpec
  ( 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
  ( extractContext
  , injectContext
  , traceparentHeader
  , tracestateHeader
  )

tests :: TestTree
tests =
  testGroup
    "Propagation"
    [ testGroup "extractContext (W3C vectors)" extractVectors
    , testGroup "injectContext" injectTests
    , testGroup "round-trip" roundTripTests
    ]

-- A canonical valid traceparent from the W3C spec examples.
validTraceparent :: ByteString
validTraceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"

extractVectors :: [TestTree]
extractVectors =
  [ testCase "parses a valid traceparent" $ do
      let context = extractContext [(traceparentHeader, validTraceparent)]
      fmap (traceIdToHex . spanContextTraceId) context
        @?= Just "4bf92f3577b34da6a3ce929d0e0e4736"
      fmap (spanIdToHex . spanContextSpanId) context
        @?= Just "00f067aa0ba902b7"
      fmap (isSampled . spanContextTraceFlags) context @?= Just True
      fmap spanContextIsRemote context @?= Just True
  , testCase "unsampled flag (00) parses as not sampled" $ do
      let context =
            extractContext
              [(traceparentHeader, "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00")]
      fmap (isSampled . spanContextTraceFlags) context @?= Just False
  , testCase "absent traceparent yields Nothing" $
      extractContext [] @?= Nothing
  , testCase "all-zero trace id is rejected" $
      extractContext
        [(traceparentHeader, "00-00000000000000000000000000000000-00f067aa0ba902b7-01")]
        @?= Nothing
  , testCase "all-zero span id is rejected" $
      extractContext
        [(traceparentHeader, "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01")]
        @?= Nothing
  , testCase "reserved version ff is rejected" $
      extractContext
        [(traceparentHeader, "ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")]
        @?= Nothing
  , testCase "version 00 with a trailing field is rejected" $
      extractContext
        [(traceparentHeader, validTraceparent <> "-extra")]
        @?= Nothing
  , testCase "future version with a trailing field is accepted" $ do
      let context =
            extractContext
              [(traceparentHeader, "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-extra")]
      fmap (traceIdToHex . spanContextTraceId) context
        @?= Just "4bf92f3577b34da6a3ce929d0e0e4736"
  , testCase "too few fields is rejected" $
      extractContext [(traceparentHeader, "00-4bf92f3577b34da6a3ce929d0e0e4736")] @?= Nothing
  , testCase "non-hex flags is rejected" $
      extractContext
        [(traceparentHeader, "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-zz")]
        @?= Nothing
  , testCase "a malformed tracestate does not fail extraction" $ do
      let context =
            extractContext
              [ (traceparentHeader, validTraceparent)
              , (tracestateHeader, "this is = not valid = tracestate")
              ]
      assertBool "traceparent still parses" (isJust context)
  , testCase "header lookup is case-insensitive" $ do
      let context = extractContext [("TraceParent", validTraceparent)]
      fmap (spanIdToHex . spanContextSpanId) context @?= Just "00f067aa0ba902b7"
  ]

injectTests :: [TestTree]
injectTests =
  [ testCase "emits a traceparent for the active span" $ do
      headers <- run (withSpan "outbound" injectContext)
      assertBool "a traceparent header is present" (traceparentHeader `elem` map fst headers)
  , testCase "emits no headers when there is no active span" $ do
      headers <- run injectContext
      headers @?= []
  ]

roundTripTests :: [TestTree]
roundTripTests =
  [ testCase "inject then extract preserves ids and sampled flag" $ do
      headers <- run (withSpan "outbound" injectContext)
      let extracted = extractContext headers
      -- The injected span is the live (sampled) child; extraction must recover
      -- its trace id, span id, and sampled flag, now marked remote.
      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