packages feed

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

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module OpenTelemetry.TraceSpec where

import Control.Exception (IOException)
import Control.Monad
import Data.Foldable (traverse_)
import qualified Data.HashMap.Strict as HM
import Data.IORef
import Data.Int
import Data.List (nub)
import Data.Maybe (isJust)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Vector as V
import GHC.Stack (withFrozenCallStack)
import OpenTelemetry.Attributes (AttributeLimits (..), Attributes, defaultAttributeLimits, getCount, lookupAttribute)
import qualified OpenTelemetry.Context as Context
import OpenTelemetry.Exporter.InMemory.Span (inMemoryListExporter)
import OpenTelemetry.Processor.Span (SpanProcessor (..))
import OpenTelemetry.Resource
import OpenTelemetry.Trace
import OpenTelemetry.Trace.Core
import OpenTelemetry.Trace.Id
import OpenTelemetry.Trace.Id.Generator.Default
import OpenTelemetry.Trace.Sampler
import qualified OpenTelemetry.Trace.TraceState as TraceState
import OpenTelemetry.Util (appendOnlyBoundedCollectionValues)
import Test.Hspec


asIO :: IO a -> IO a
asIO = id


immutableSpanStatus :: ImmutableSpan -> IO SpanStatus
immutableSpanStatus imm = hotStatus <$> readIORef (spanHot imm)


immutableSpanAttributes :: ImmutableSpan -> IO Attributes
immutableSpanAttributes imm = hotAttributes <$> readIORef (spanHot imm)


immutableSpanEvents :: ImmutableSpan -> IO (V.Vector Event)
immutableSpanEvents imm = do
  hot <- readIORef (spanHot imm)
  pure $ appendOnlyBoundedCollectionValues (hotEvents hot)


immutableSpanLinks :: ImmutableSpan -> IO (V.Vector Link)
immutableSpanLinks imm = do
  hot <- readIORef (spanHot imm)
  pure $ appendOnlyBoundedCollectionValues (hotLinks hot)


pattern HostName :: Text
pattern HostName = "host.name"


pattern TelemetrySdkLanguage :: Text
pattern TelemetrySdkLanguage = "telemetry.sdk.language"


pattern ExampleName :: Text
pattern ExampleName = "example.name"


pattern ExampleCount :: Text
pattern ExampleCount = "example.count"


spec :: Spec
spec = describe "Trace" $ do
  describe "TracerProvider" $ do
    specify "Create TracerProvider" $ do
      void (createTracerProvider [] (emptyTracerProviderOptions :: TracerProviderOptions) :: IO TracerProvider)

    specify "Shutdown" $ do
      (processor, ref) <- inMemoryListExporter
      tp <- createTracerProvider [processor] emptyTracerProviderOptions {tracerProviderOptionsIdGenerator = defaultIdGenerator}
      let tracer = makeTracer tp "test" tracerOptions
      s <- createSpan tracer Context.empty "shutdown_test" defaultSpanArguments
      endSpan s Nothing
      result <- shutdownTracerProvider tp Nothing
      result `shouldBe` ShutdownSuccess
      spans <- readIORef ref
      length spans `shouldBe` 1
      -- After shutdown, createSpan should return a Dropped span
      s2 <- createSpan tracer Context.empty "after_shutdown" defaultSpanArguments
      recording <- isRecording s2
      recording `shouldBe` False

    specify "ForceFlush" $ do
      (processor, ref) <- inMemoryListExporter
      tp <- createTracerProvider [processor] emptyTracerProviderOptions {tracerProviderOptionsIdGenerator = defaultIdGenerator}
      let tracer = makeTracer tp "test" tracerOptions
      s <- createSpan tracer Context.empty "flush_test" defaultSpanArguments
      endSpan s Nothing
      result <- forceFlushTracerProvider tp Nothing
      result `shouldBe` FlushSuccess
      spans <- readIORef ref
      length spans `shouldBe` 1

    specify "Resource initialization prioritizes user override, then OTEL_RESOURCE_ATTRIBUTES env var" $ do
      let getInitialResourceAttrs :: Resource -> IO Attributes
          getInitialResourceAttrs resource = do
            opts <- snd <$> getTracerProviderInitializationOptions' resource
            pure . getMaterializedResourcesAttributes $ tracerProviderOptionsResources opts
          shouldHaveAttrPair :: Attributes -> (Text, Attribute) -> Expectation
          shouldHaveAttrPair attrs (k, v) = lookupAttribute attrs k `shouldBe` Just v
      attrsFromEnv <- getInitialResourceAttrs mempty
      traverse_
        (attrsFromEnv `shouldHaveAttrPair`)
        [ (HostName, toAttribute @Text "env_host_name")
        , (TelemetrySdkLanguage, toAttribute @Text "haskell")
        , (ExampleName, toAttribute @Text "env_example_name")
        , -- OTEL_RESOURCE_ATTRIBUTES uses Baggage format, where attribute values are always text
          (ExampleCount, toAttribute @Text "42")
        ]
      attrsFromUser <-
        getInitialResourceAttrs $
          mkResource
            [ HostName .= toAttribute @Text "user_host_name"
            , TelemetrySdkLanguage .= toAttribute @Text "GHC2021"
            , ExampleCount .= toAttribute @Int 11
            ]
      traverse_
        (attrsFromUser `shouldHaveAttrPair`)
        [ (HostName, toAttribute @Text "user_host_name")
        , (TelemetrySdkLanguage, toAttribute @Text "GHC2021")
        , -- No user override for "example.name", so the value from OTEL_RESOURCES_ATTRIBUTES shines through
          (ExampleName, toAttribute @Text "env_example_name")
        , (ExampleCount, toAttribute @Int 11)
        ]

  describe "Trace / Context interaction" $ do
    specify "Set active span, Get active span" $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      spanContext1 <- spanContext <$> unsafeReadSpan s
      let ctxt = Context.insertSpan s mempty
      let Just s' = Context.lookupSpan ctxt
      spanContext2 <- spanContext <$> unsafeReadSpan s'
      spanContext1 `shouldBe` spanContext2

  describe "Tracer" $ do
    specify "Create a new span" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      void $ createSpan t Context.empty "create_root_span" defaultSpanArguments

  describe "SpanContext" $ do
    specify "IsValid" $ do
      t <- newTraceId defaultIdGenerator
      s <- newSpanId defaultIdGenerator
      let (Right goodSpan) = bytesToSpanId "\1\0\0\0\0\0\0\1"
          (Right goodTrace) = bytesToTraceId "\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1"
          (Right badSpan) = bytesToSpanId "\0\0\0\0\0\0\0\0"
          (Right badTrace) = bytesToTraceId "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
          validSpan =
            SpanContext
              { traceFlags = defaultTraceFlags
              , isRemote = False
              , traceState = TraceState.empty
              , spanId = goodSpan
              , traceId = goodTrace
              }
      validSpan `shouldSatisfy` isValid
      (validSpan {spanId = badSpan, traceId = badTrace}) `shouldSatisfy` (not . isValid)
      -- Spec: BOTH TraceId AND SpanId must be non-zero
      (validSpan {spanId = badSpan}) `shouldSatisfy` (not . isValid)
      (validSpan {traceId = badTrace}) `shouldSatisfy` (not . isValid)

    specify "IsRemote" $ do
      let (Right sid) = bytesToSpanId "\1\0\0\0\0\0\0\1"
          (Right tid) = bytesToTraceId "\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1"
          sc =
            SpanContext
              { traceFlags = defaultTraceFlags
              , isRemote = True
              , traceState = TraceState.empty
              , spanId = sid
              , traceId = tid
              }
      OpenTelemetry.Trace.Core.isRemote sc `shouldBe` True
      OpenTelemetry.Trace.Core.isRemote (sc {isRemote = False}) `shouldBe` False

  describe "Span" $ do
    specify "Create root span" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      void $ createSpan t Context.empty "create_root_span" defaultSpanArguments

    specify "Create with default parent (active span)" $ do
      (processor, _ref) <- inMemoryListExporter
      tp <- createTracerProvider [processor] emptyTracerProviderOptions {tracerProviderOptionsIdGenerator = defaultIdGenerator}
      let tracer = makeTracer tp "test" tracerOptions
      parentSpan <- createSpan tracer Context.empty "parent" defaultSpanArguments
      let ctxt = Context.insertSpan parentSpan Context.empty
      childSpan <- createSpan tracer ctxt "child" defaultSpanArguments
      parentImm <- unsafeReadSpan parentSpan
      childImm <- unsafeReadSpan childSpan
      -- Child's parent should be present and match the parent span's context
      case spanParent childImm of
        Nothing -> expectationFailure "Child span should have a parent"
        Just p -> do
          parentSc <- getSpanContext p
          parentSc `shouldBe` spanContext parentImm

    specify "Create with parent from Context" $ do
      (processor, _ref) <- inMemoryListExporter
      tp <- createTracerProvider [processor] emptyTracerProviderOptions {tracerProviderOptionsIdGenerator = defaultIdGenerator}
      let tracer = makeTracer tp "test" tracerOptions
      parentSpan <- createSpan tracer Context.empty "parent" defaultSpanArguments
      let ctxtWithParent = Context.insertSpan parentSpan Context.empty
      childSpan <- createSpan tracer ctxtWithParent "child" defaultSpanArguments
      parentImm <- unsafeReadSpan parentSpan
      childImm <- unsafeReadSpan childSpan
      -- The child should share the parent's trace ID
      traceId (spanContext childImm) `shouldBe` traceId (spanContext parentImm)
      -- The child's parent span ID should be the parent's span ID
      case spanParent childImm of
        Nothing -> expectationFailure "Child span should have a parent"
        Just p -> do
          pSc <- getSpanContext p
          spanId pSc `shouldBe` spanId (spanContext parentImm)

    specify "Processor.OnStart receives parent Context" $ do
      ctxRef <- newIORef (Nothing :: Maybe Context.Context)
      let testProcessor =
            SpanProcessor
              { spanProcessorOnStart = \_imm ctx -> writeIORef ctxRef (Just ctx)
              , spanProcessorOnEnd = \_ -> pure ()
              , spanProcessorShutdown = pure ShutdownSuccess
              , spanProcessorForceFlush = pure FlushSuccess
              }
      tp <- createTracerProvider [testProcessor] emptyTracerProviderOptions {tracerProviderOptionsIdGenerator = defaultIdGenerator}
      let tracer = makeTracer tp "test" tracerOptions
      parentSpan <- createSpan tracer Context.empty "parent" defaultSpanArguments
      let ctxt = Context.insertSpan parentSpan Context.empty
      -- Reset the ref so we only see what the child's OnStart gets
      writeIORef ctxRef Nothing
      _childSpan <- createSpan tracer ctxt "child" defaultSpanArguments
      receivedCtx <- readIORef ctxRef
      case receivedCtx of
        Nothing -> expectationFailure "OnStart should receive context"
        Just ctx -> do
          let parentInCtx = Context.lookupSpan ctx
          case parentInCtx of
            Nothing -> expectationFailure "Context passed to OnStart should contain parent span"
            Just p -> do
              pSc <- getSpanContext p
              parentSc <- getSpanContext parentSpan
              pSc `shouldBe` parentSc

    specify "UpdateName" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      updateName s "renamed_span"

    specify "User-defined start timestamp" $ do
      (processor, _ref) <- inMemoryListExporter
      tp <- createTracerProvider [processor] emptyTracerProviderOptions {tracerProviderOptionsIdGenerator = defaultIdGenerator}
      let tracer = makeTracer tp "test" tracerOptions
      userTs <- getTimestamp
      s <- createSpan tracer Context.empty "ts_test" defaultSpanArguments {startTime = Just userTs}
      imm <- unsafeReadSpan s
      spanStart imm `shouldBe` userTs

    specify "End" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      endSpan s Nothing
    specify "End with timestamp" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      ts <- getTimestamp
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      endSpan s (Just ts)
    specify "IsRecording" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      recording <- isRecording s
      recording `shouldBe` True

    specify "IsRecording becomes false after End" $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      endSpan s Nothing
      recording <- isRecording s
      recording `shouldBe` False

    specify "Set status with StatusCode (Unset, Ok, Error)" $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments

      setStatus s $ Error "woo"
      do
        i <- unsafeReadSpan s
        immutableSpanStatus i `shouldReturn` Error "woo"
      setStatus s $ Ok
      do
        i <- unsafeReadSpan s
        immutableSpanStatus i `shouldReturn` Ok
      setStatus s $ Error "woo"
      do
        i <- unsafeReadSpan s
        immutableSpanStatus i `shouldReturn` Ok

    specify "events collection size limit" $ do
      (processor, _ref) <- inMemoryListExporter
      let limits = defaultSpanLimits {eventCountLimit = Just 3}
      tp <-
        createTracerProvider
          [processor]
          emptyTracerProviderOptions
            { tracerProviderOptionsIdGenerator = defaultIdGenerator
            , tracerProviderOptionsSpanLimits = limits
            }
      let tracer = makeTracer tp "test" tracerOptions
      s <- createSpan tracer Context.empty "limit_test" defaultSpanArguments
      -- Add 5 events but limit is 3
      forM_ [1 .. 5 :: Int] $ \i ->
        addEvent s $
          NewEvent
            { newEventName = Text.pack ("event_" <> show i)
            , newEventAttributes = HM.empty
            , newEventTimestamp = Nothing
            }
      imm <- unsafeReadSpan s
      evts <- immutableSpanEvents imm
      V.length evts `shouldBe` 3

    specify "attribute collection size limit" $ do
      (processor, _ref) <- inMemoryListExporter
      let limits = defaultSpanLimits {spanAttributeCountLimit = Just 5}
      tp <-
        createTracerProvider
          [processor]
          emptyTracerProviderOptions
            { tracerProviderOptionsIdGenerator = defaultIdGenerator
            , tracerProviderOptionsSpanLimits = limits
            , tracerProviderOptionsAttributeLimits = defaultAttributeLimits {attributeCountLimit = Just 5}
            }
      let tracer = makeTracer tp "test" tracerOptions
      s <- createSpan tracer Context.empty "limit_test" defaultSpanArguments
      -- Add many more attributes than the limit allows
      forM_ [1 .. 20 :: Int] $ \i ->
        addAttribute s (Text.pack ("attr_" <> show i)) (toAttribute i)
      imm <- unsafeReadSpan s
      attrs <- immutableSpanAttributes imm
      -- Total attribute count (including thread.id and any auto-added attrs)
      -- should not exceed the configured limit of 5
      getCount attrs `shouldSatisfy` (<= 5)

    specify "links collection size limit" $ do
      (processor, _ref) <- inMemoryListExporter
      let limits = defaultSpanLimits {linkCountLimit = Just 2}
      tp <-
        createTracerProvider
          [processor]
          emptyTracerProviderOptions
            { tracerProviderOptionsIdGenerator = defaultIdGenerator
            , tracerProviderOptionsSpanLimits = limits
            }
      let tracer = makeTracer tp "test" tracerOptions
      s <- createSpan tracer Context.empty "limit_test" defaultSpanArguments
      let (Right sid) = bytesToSpanId "\1\2\3\4\5\6\7\8"
          (Right tid) = bytesToTraceId "\1\2\3\4\5\6\7\8\9\10\11\12\13\14\15\16"
          linkSc =
            SpanContext
              { traceFlags = defaultTraceFlags
              , isRemote = False
              , traceState = TraceState.empty
              , spanId = sid
              , traceId = tid
              }
      -- Add 5 links but limit is 2
      forM_ [1 .. 5 :: Int] $ \_ ->
        addLink s $ NewLink {linkContext = linkSc, linkAttributes = HM.empty}
      imm <- unsafeReadSpan s
      lnks <- immutableSpanLinks imm
      V.length lnks `shouldBe` 2

  describe "Span attributes" $ do
    specify "SetAttribute" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      addAttribute s "attr" (1.0 :: Double)

    specify "String type" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      addAttribute s "string_type" ("" :: Text)

    specify "Boolean type" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      addAttribute s "bool_type" True

    specify "Double floating-point type" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      addAttribute s "attr" (1.0 :: Double)

    specify "Signed int64 type" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      addAttribute s "attr" (1 :: Int64)

    specify "Array of primitives (homegeneous)" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      addAttribute s "attr" [(1 :: Int64) .. 10]

    specify "Unicode support for keys and string values" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      addAttribute s "🚀" ("🚀" :: Text)
    -- TODO actually get attributes out

    specify "Source code attributes are added correctly" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      attrs <- immutableSpanAttributes =<< unsafeReadSpan =<< f t
      attrs `shouldSatisfy` \a ->
        (lookupAttribute a "code.function") == Just (toAttribute @Text "f")
          && (lookupAttribute a "code.namespace") == Just (toAttribute @Text "OpenTelemetry.TraceSpec")
          && isJust (lookupAttribute a "code.lineno")

    specify "Source code attributes are not added in the presence of frozen call stacks" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      attrs <- immutableSpanAttributes =<< unsafeReadSpan =<< g3 t
      attrs `shouldSatisfy` \a ->
        (lookupAttribute a "code.function") == Nothing
          && (lookupAttribute a "code.namespace") == Nothing
          && (lookupAttribute a "code.lineno") == Nothing

    specify "Source code attributes are not added if source attributes are already present" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      attrs <- immutableSpanAttributes =<< unsafeReadSpan =<< h t
      attrs `shouldSatisfy` \a ->
        (lookupAttribute a "code.function") == Just (toAttribute @Text "something")
          && (lookupAttribute a "code.namespace") == Nothing
          && (lookupAttribute a "code.lineno") == Nothing

    specify "Source code attributes can be added for span creation wrappers" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      attrs <- immutableSpanAttributes =<< unsafeReadSpan =<< useSpanHelper t
      attrs `shouldSatisfy` \a ->
        (lookupAttribute a "code.function") == Just (toAttribute @Text "useSpanHelper")

    specify "Attribute length limit is respected" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "test_span" defaultSpanArguments
      let
        longAttribute :: Text = "looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong"
        -- We set this limit in Spec.hs
        -- The long attribute should be truncated down to this length
        truncatedAttribute :: Text = Text.take 50 longAttribute
      addAttribute s "attr1" longAttribute
      addAttributes s (HM.singleton "attr2" (toAttribute longAttribute))
      s <- unsafeReadSpan s

      attrs <- immutableSpanAttributes s
      lookupAttribute attrs "attr1" `shouldBe` Just (toAttribute truncatedAttribute)
      lookupAttribute attrs "attr2" `shouldBe` Just (toAttribute truncatedAttribute)

  describe "Span events" $ do
    specify "AddEvent" $ asIO $ do
      p <- getGlobalTracerProvider
      let t = makeTracer p "woo" tracerOptions
      s <- createSpan t Context.empty "create_root_span" defaultSpanArguments
      addEvent s $
        NewEvent
          { newEventName = "EVENT"
          , newEventAttributes = mempty
          , newEventTimestamp = Nothing
          }

  describe "Span exceptions" $ do
    specify "RecordException" $ do
      (processor, _ref) <- inMemoryListExporter
      tp <- createTracerProvider [processor] emptyTracerProviderOptions {tracerProviderOptionsIdGenerator = defaultIdGenerator}
      let tracer = makeTracer tp "test" tracerOptions
      s <- createSpan tracer Context.empty "exception_test" defaultSpanArguments
      let ex = userError "test error" :: IOException
      recordException s HM.empty Nothing ex
      imm <- unsafeReadSpan s
      evts <- immutableSpanEvents imm
      V.length evts `shouldSatisfy` (>= 1)
      let evt = V.head evts
      eventName evt `shouldBe` "exception"
      let evtAttrs = eventAttributes evt
      lookupAttribute evtAttrs "exception.message" `shouldSatisfy` isJust
      lookupAttribute evtAttrs "exception.type" `shouldSatisfy` isJust

  describe "Sampling" $ do
    specify "Allow samplers to modify tracestate" $ do
      (processor, _ref) <- inMemoryListExporter
      let customTs = TraceState.insert (TraceState.Key "custom_key") (TraceState.Value "custom_value") TraceState.empty
          sampler =
            CustomSampler "tracestate-modifier" $ \_ctx _tid _name _args _scope ->
              pure $ SamplingDecision RecordAndSample HM.empty customTs
      tp <-
        createTracerProvider
          [processor]
          emptyTracerProviderOptions
            { tracerProviderOptionsIdGenerator = defaultIdGenerator
            , tracerProviderOptionsSampler = sampler
            }
      let tracer = makeTracer tp "test" tracerOptions
      s <- createSpan tracer Context.empty "sampler_test" defaultSpanArguments
      sc <- getSpanContext s
      traceState sc `shouldBe` customTs

    specify "ShouldSample gets full parent Context" $ do
      receivedCtxRef <- newIORef (Nothing :: Maybe Context.Context)
      let sampler =
            CustomSampler "context-recorder" $ \ctx _tid _name _args _scope -> do
              writeIORef receivedCtxRef (Just ctx)
              pure $ SamplingDecision RecordAndSample HM.empty TraceState.empty
      (processor, _ref) <- inMemoryListExporter
      tp <-
        createTracerProvider
          [processor]
          emptyTracerProviderOptions
            { tracerProviderOptionsIdGenerator = defaultIdGenerator
            , tracerProviderOptionsSampler = sampler
            }
      let tracer = makeTracer tp "test" tracerOptions
      parentSpan <- createSpan tracer Context.empty "parent" defaultSpanArguments
      -- Reset the ref after the parent creation
      writeIORef receivedCtxRef Nothing
      let ctxt = Context.insertSpan parentSpan Context.empty
      _childSpan <- createSpan tracer ctxt "child" defaultSpanArguments
      receivedCtx <- readIORef receivedCtxRef
      case receivedCtx of
        Nothing -> expectationFailure "Sampler should have received a context"
        Just ctx -> do
          let spanInCtx = Context.lookupSpan ctx
          case spanInCtx of
            Nothing -> expectationFailure "Context passed to sampler should contain parent span"
            Just p -> do
              pSc <- getSpanContext p
              parentSc <- getSpanContext parentSpan
              pSc `shouldBe` parentSc

    specify "ShouldSample gets InstrumentationLibrary" $ do
      (processor, _ref) <- inMemoryListExporter
      receivedScopeRef <- newIORef (Nothing :: Maybe InstrumentationLibrary)
      let sampler =
            CustomSampler "scope-recorder" $ \_ctx _tid _name _args scope -> do
              writeIORef receivedScopeRef (Just scope)
              pure $ SamplingDecision RecordAndSample HM.empty TraceState.empty
      tp <-
        createTracerProvider
          [processor]
          emptyTracerProviderOptions
            { tracerProviderOptionsIdGenerator = defaultIdGenerator
            , tracerProviderOptionsSampler = sampler
            }
      let tracer = makeTracer tp "my-library" tracerOptions
      _s <- createSpan tracer Context.empty "scope_test" defaultSpanArguments
      receivedScope <- readIORef receivedScopeRef
      case receivedScope of
        Nothing -> expectationFailure "Sampler should have received InstrumentationLibrary"
        Just scope -> libraryName scope `shouldBe` "my-library"

  specify "New Span ID created also for non-recording spans" $ do
    (processor, _ref) <- inMemoryListExporter
    tp <-
      createTracerProvider
        [processor]
        emptyTracerProviderOptions
          { tracerProviderOptionsIdGenerator = defaultIdGenerator
          , tracerProviderOptionsSampler = alwaysOff
          }
    let tracer = makeTracer tp "test" tracerOptions
    spanIds <- forM [1 .. 5 :: Int] $ \_ -> do
      s <- createSpan tracer Context.empty "dropped" defaultSpanArguments
      sc <- getSpanContext s
      pure (spanId sc)
    -- All span IDs should be distinct even for non-recording (dropped) spans
    nub spanIds `shouldBe` spanIds


f :: (HasCallStack) => Tracer -> IO Span
f tracer =
  createSpan tracer Context.empty "name" defaultSpanArguments


g :: (HasCallStack) => Tracer -> IO Span
-- block createSpan and callerAttributes from appearing in the call stack
g tracer = withFrozenCallStack $ createSpan tracer Context.empty "name" defaultSpanArguments


g2 :: (HasCallStack) => Tracer -> IO Span
g2 tracer = g tracer


-- Make a 3-deep call stack
g3 :: (HasCallStack) => Tracer -> IO Span
g3 tracer = g2 tracer


h :: (HasCallStack) => Tracer -> IO Span
h tracer =
  createSpan tracer Context.empty "name" (addAttributesToSpanArguments (HM.singleton "code.function" "something") defaultSpanArguments)


myInSpan :: (HasCallStack) => Tracer -> Text -> IO a -> IO (a, Span)
myInSpan tracer name act = inSpan' tracer name (addAttributesToSpanArguments callerAttributes defaultSpanArguments) $ \traceSpan -> do
  res <- act
  pure (res, traceSpan)


useSpanHelper :: (HasCallStack) => Tracer -> IO Span
useSpanHelper tracer = snd <$> myInSpan tracer "useSpanHelper" (pure ())