packages feed

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

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

-- |
-- Module      : Effectful.Tracing.Propagation.JaegerSpec
-- Description : Tests for Jaeger (uber-trace-id) propagation.
--
-- 'extractContextJaeger' is exercised directly with @uber-trace-id@ test vectors
-- (the parse is the whole contract for inbound requests), the injectors are
-- exercised through the in-memory interpreter and the baggage interpreter where
-- the ambient context exists, and round-trip tests confirm inject-then-extract
-- preserves the trace and span ids, the sampled flag, and the baggage entries.
module Effectful.Tracing.Propagation.JaegerSpec
  ( tests
  ) where

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

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

import Effectful.Tracing (Tracer, withSpan)
import Effectful.Tracing.Baggage
  ( lookupBaggageValue
  , nullBaggage
  , runBaggage
  , withBaggageEntry
  )
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.Jaeger
  ( extractBaggageJaeger
  , extractContextJaeger
  , injectBaggageJaeger
  , injectContextJaeger
  , uberTraceIdHeader
  )

tests :: TestTree
tests =
  testGroup
    "Propagation.Jaeger"
    [ testGroup "extractContextJaeger" extractVectors
    , testGroup "inject" injectTests
    , testGroup "extractBaggageJaeger" baggageVectors
    , testGroup "round-trip" roundTripTests
    ]

traceId128 :: ByteString
traceId128 = "4bf92f3577b34da6a3ce929d0e0e4736"

spanId :: ByteString
spanId = "00f067aa0ba902b7"

extractVectors :: [TestTree]
extractVectors =
  [ testCase "parses a 128-bit uber-trace-id with sampling" $ do
      let context = extractContextJaeger [(uberTraceIdHeader, traceId128 <> ":" <> spanId <> ":0: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 leading-zero-stripped ids back to full width" $ do
      let context = extractContextJaeger [(uberTraceIdHeader, "a3ce929d0e0e4736:f067aa0ba902b7:0:1")]
      fmap (traceIdToHex . spanContextTraceId) context
        @?= Just "0000000000000000a3ce929d0e0e4736"
      fmap (spanIdToHex . spanContextSpanId) context @?= Just "00f067aa0ba902b7"
  , testCase "flags low bit clear parses as not sampled" $ do
      let context = extractContextJaeger [(uberTraceIdHeader, traceId128 <> ":" <> spanId <> ":0:0")]
      fmap (isSampled . spanContextTraceFlags) context @?= Just False
  , testCase "debug flag (3) still reads the sampled low bit" $ do
      let context = extractContextJaeger [(uberTraceIdHeader, traceId128 <> ":" <> spanId <> ":0:3")]
      fmap (isSampled . spanContextTraceFlags) context @?= Just True
  , testCase "fewer than four fields yields Nothing" $
      extractContextJaeger [(uberTraceIdHeader, traceId128 <> ":" <> spanId <> ":1")] @?= Nothing
  , testCase "an all-zero trace id is rejected" $
      extractContextJaeger [(uberTraceIdHeader, "0:" <> spanId <> ":0:1")] @?= Nothing
  , testCase "a non-hex flags field is rejected" $
      extractContextJaeger [(uberTraceIdHeader, traceId128 <> ":" <> spanId <> ":0:z")] @?= Nothing
  , testCase "absent header yields Nothing" $
      extractContextJaeger [] @?= Nothing
  , testCase "the header lookup is case-insensitive" $ do
      let context = extractContextJaeger [("Uber-Trace-Id", traceId128 <> ":" <> spanId <> ":0:1")]
      fmap (spanIdToHex . spanContextSpanId) context @?= Just "00f067aa0ba902b7"
  ]

injectTests :: [TestTree]
injectTests =
  [ testCase "emits an uber-trace-id header for the active span" $ do
      headers <- run (withSpan "outbound" injectContextJaeger)
      assertBool "an uber-trace-id header is present" (uberTraceIdHeader `elem` map fst headers)
  , testCase "emits no header when there is no active span" $ do
      headers <- run injectContextJaeger
      headers @?= []
  , testCase "emits uberctx- headers for the ambient baggage" $ do
      let headers = runPureEff (runBaggage (withBaggageEntry "tenant" "acme" injectBaggageJaeger))
      headers @?= [("uberctx-tenant", "acme")]
  , testCase "emits no baggage headers when the baggage is empty" $ do
      let headers = runPureEff (runBaggage injectBaggageJaeger)
      headers @?= []
  ]

baggageVectors :: [TestTree]
baggageVectors =
  [ testCase "reads a uberctx- header" $
      lookupBaggageValue "tenant" (extractBaggageJaeger [("uberctx-tenant", "acme")]) @?= Just "acme"
  , testCase "the prefix match is case-insensitive" $
      lookupBaggageValue "tenant" (extractBaggageJaeger [("Uberctx-tenant", "acme")]) @?= Just "acme"
  , testCase "trims surrounding whitespace" $
      lookupBaggageValue "tenant" (extractBaggageJaeger [("uberctx-tenant", " acme ")]) @?= Just "acme"
  , testCase "ignores non-uberctx headers" $
      nullBaggage (extractBaggageJaeger [("x-other", "v")]) @?= True
  , testCase "skips an empty key" $
      nullBaggage (extractBaggageJaeger [("uberctx-", "v")]) @?= True
  ]

roundTripTests :: [TestTree]
roundTripTests =
  [ testCase "context inject then extract preserves ids and sampled flag" $ do
      headers <- run (withSpan "outbound" injectContextJaeger)
      let extracted = extractContextJaeger headers
      assertBool "round-trips to a context" (isJust extracted)
      fmap (isSampled . spanContextTraceFlags) extracted @?= Just True
      fmap spanContextIsRemote extracted @?= Just True
  , testCase "baggage inject then extract preserves entries" $ do
      let headers = runPureEff (runBaggage (withBaggageEntry "tenant" "acme" injectBaggageJaeger))
      lookupBaggageValue "tenant" (extractBaggageJaeger headers) @?= Just "acme"
  ]

-- | 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