packages feed

hs-opentelemetry-sdk-1.0.0.0: test/OpenTelemetry/ContextSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module OpenTelemetry.ContextSpec where

import Control.Concurrent
import Control.Monad
import Data.Maybe
import qualified OpenTelemetry.Baggage as Baggage
import OpenTelemetry.Context (
  Key,
  empty,
  insert,
  insertBaggage,
  lookup,
  lookupBaggage,
  lookupSpan,
  newKey,
 )
import OpenTelemetry.Context.ThreadLocal
import OpenTelemetry.Propagator (
  Propagator (..),
  emptyTextMap,
  extract,
  getGlobalTextMapPropagator,
  inject,
  textMapFromList,
  textMapLookup,
 )
import OpenTelemetry.Propagator.B3 (b3TraceContextPropagator)
import OpenTelemetry.Propagator.W3CBaggage (w3cBaggagePropagator)
import OpenTelemetry.Propagator.W3CTraceContext (w3cTraceContextPropagator)
import Test.Hspec
import Prelude hiding (lookup)


spec :: Spec
spec = describe "Context" $ do
  -- Context API: immutable key/value context
  -- https://opentelemetry.io/docs/specs/otel/context/
  describe "Create Context Key" $ do
    -- Context API §Create a key: unique keys for Context values
    -- https://opentelemetry.io/docs/specs/otel/context/#create-a-key
    it "works" $ do
      void (newKey "k" :: IO (Key ()))
  describe "Set value for Context" $ do
    -- Context API §Get value / Set value
    -- https://opentelemetry.io/docs/specs/otel/context/#set-value
    it "works" $ do
      k <- newKey "k"
      let ctxt = insert k (12 :: Int) empty
      lookup k ctxt `shouldBe` Just 12
  describe "Get value from Context" $ do
    -- Context API §Get value
    -- https://opentelemetry.io/docs/specs/otel/context/#get-value
    it "works" $ do
      k1 <- newKey "k.1"
      k2 <- newKey "k.2"
      let ctxt = insert k2 (Just False) $ insert k1 (12 :: Int) empty
      lookup k1 ctxt `shouldBe` Just 12
      lookup k2 ctxt `shouldBe` (Just (Just False))
  describe "Attach Context" $ do
    -- Implementation-specific: thread-local attach maps to runtime propagation of "current" Context
    -- https://opentelemetry.io/docs/specs/otel/context/#optional-global-operations
    specify "ThreadLocal works" $ do
      k <- newKey "thingum"
      tok <- attachContext $ insert k True empty
      mctxt <- lookupContext
      (mctxt >>= lookup k) `shouldBe` Just True
      detachContext tok
  describe "Detach Context" $ do
    -- Implementation-specific: thread-local detach restores previous Context
    -- https://opentelemetry.io/docs/specs/otel/context/#optional-global-operations
    specify "ThreadLocal works" $ do
      k <- newKey "thingum"
      tok <- attachContext $ insert k True empty
      detachContext tok
      ctx <- getContext
      lookup k ctx `shouldBe` (Nothing :: Maybe Bool)

  -- Implementation-specific: nested attach/detach stack (thread-local)
  -- https://opentelemetry.io/docs/specs/otel/context/#optional-global-operations
  specify "Get current Context" $ do
    k1 <- newKey "k.1"
    k2 <- newKey "k.2"
    let ctxt1 = insert k1 (12 :: Int) empty
    tok1 <- attachContext ctxt1
    let ctxt2 = insert k2 (13 :: Int) empty
    tok2 <- attachContext ctxt2
    (Just ctxt) <- lookupContext
    lookup k1 ctxt `shouldBe` Nothing
    lookup k2 ctxt `shouldBe` Just 13
    detachContext tok2
    detachContext tok1

  describe "Thread-local advanced API" $ do
    -- Implementation-specific: Haskell thread-local Context helpers (not OTel API surface)
    specify "adjustContext modifies stored context" $ do
      k <- newKey "adj"
      tok <- attachContext empty
      adjustContext (insert k ("ok" :: String))
      mctxt <- lookupContext
      (mctxt >>= lookup k) `shouldBe` Just "ok"
      detachContext tok

    -- Implementation-specific: attach Context on another OS thread
    specify "attachContextOnThread and lookupContextOnThread roundtrip" $ do
      started <- newEmptyMVar
      mainDone <- newEmptyMVar
      _childTid <-
        forkIO $ do
          tid <- myThreadId
          putMVar started tid
          takeMVar mainDone
      k <- newKey "remote"
      childTid <- takeMVar started
      tok <- attachContextOnThread childTid (insert k True empty)
      mc <- lookupContextOnThread childTid
      (mc >>= lookup k) `shouldBe` Just True
      detachContextFromThread childTid tok
      putMVar mainDone ()

    -- Implementation-specific: detach restores prior Context on remote thread
    specify "detachContextFromThread restores previous context" $ do
      started <- newEmptyMVar
      mainDone <- newEmptyMVar
      _childTid <-
        forkIO $ do
          tid <- myThreadId
          putMVar started tid
          takeMVar mainDone
      k <- newKey "detach"
      childTid <- takeMVar started
      tok <- attachContextOnThread childTid (insert k True empty)
      detachContextFromThread childTid tok
      mc <- lookupContextOnThread childTid
      (mc >>= lookup k) `shouldBe` (Nothing :: Maybe Bool)
      putMVar mainDone ()

    -- Implementation-specific: mutate current Context on remote thread
    specify "adjustContextOnThread updates remote thread context" $ do
      started <- newEmptyMVar
      mainDone <- newEmptyMVar
      _childTid <-
        forkIO $ do
          tid <- myThreadId
          putMVar started tid
          takeMVar mainDone
      k <- newKey "adj-remote"
      childTid <- takeMVar started
      tok <- attachContextOnThread childTid empty
      adjustContextOnThread childTid (insert k (7 :: Int))
      mc <- lookupContextOnThread childTid
      (mc >>= lookup k) `shouldBe` Just 7
      detachContextFromThread childTid tok
      putMVar mainDone ()

  -- Propagators API §Composite Propagator
  -- https://opentelemetry.io/docs/specs/otel/context/api-propagators/#composite-propagator
  specify "Composite Propagator" $ do
    let composed = w3cTraceContextPropagator <> w3cBaggagePropagator
    propagatorFields composed `shouldBe` ["traceparent", "tracestate", "baggage"]
    let Propagator {extractor = ex, injector = inj} = composed
    c <- ex emptyTextMap empty
    isNothing (lookupSpan c) `shouldBe` True
    isNothing (lookupBaggage c) `shouldBe` True
    hs <- inj c emptyTextMap
    hs `shouldBe` emptyTextMap

  -- Propagators API: global TextMapPropagator (SDK/runtime wiring)
  -- https://opentelemetry.io/docs/specs/otel/context/api-propagators/#global-propagators
  specify "Global Propagator" $ do
    p <- getGlobalTextMapPropagator
    propagatorFields p `shouldNotBe` []
    propagatorFields p `shouldContain` ["traceparent"]

  -- W3C Trace Context propagation (fields: traceparent, tracestate)
  -- https://opentelemetry.io/docs/specs/otel/context/api-propagators/#trace-context-propagator
  specify "TraceContext Propagator" $
    propagatorFields w3cTraceContextPropagator `shouldBe` ["traceparent", "tracestate"]

  -- B3 propagator (de facto); OTel defines composite/multi-header behavior
  -- https://opentelemetry.io/docs/specs/otel/context/api-propagators/
  specify "B3 Propagator" $
    propagatorFields b3TraceContextPropagator
      `shouldBe` ["B3 Trace Context"]

  -- Implementation gap: Jaeger propagator not provided by this repo
  specify "Jaeger Propagator" $
    pendingWith "No Jaeger propagator implementation in this repository."

  describe "TextMap Propagator" $ do
    -- Baggage API §Propagating baggage: W3C Baggage header
    -- https://opentelemetry.io/docs/specs/otel/baggage/api/#propagating-baggage
    specify "Fields" $
      propagatorFields w3cBaggagePropagator `shouldBe` ["baggage"]

    -- Propagators API §Inject
    -- https://opentelemetry.io/docs/specs/otel/context/api-propagators/#inject
    specify "Setter argument" $ do
      let Just tok = Baggage.mkToken "uid"
          baggage = Baggage.insert tok (Baggage.element "x") Baggage.empty
          ctxt = insertBaggage baggage empty
      hs <- inject w3cBaggagePropagator ctxt emptyTextMap
      textMapLookup "baggage" hs `shouldNotBe` Nothing

    -- Propagators API §Extract
    -- https://opentelemetry.io/docs/specs/otel/context/api-propagators/#extract
    specify "Getter argument" $ do
      let hs = textMapFromList [("baggage", "uid=alpha")]
      ctxt' <- extract w3cBaggagePropagator hs empty
      lookupBaggage ctxt' `shouldNotBe` Nothing

    -- Baggage API: extract yields non-empty baggage in Context
    -- https://opentelemetry.io/docs/specs/otel/baggage/api/
    specify "Getter argument returning keys" $ do
      let hs = textMapFromList [("baggage", "a=1")]
      ctxt' <- extract w3cBaggagePropagator hs empty
      case lookupBaggage ctxt' of
        Nothing -> expectationFailure "expected baggage in context"
        Just b -> Baggage.values b `shouldNotBe` mempty