packages feed

hs-opentelemetry-api-1.0.0.0: test/OpenTelemetry/Trace/UtilsSpec.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module OpenTelemetry.Trace.UtilsSpec where

import qualified Data.HashMap.Strict as H
import Data.IORef
import Data.Maybe (isJust, isNothing)
import Data.Text (Text)
import qualified Data.Vector as V
import OpenTelemetry.Attributes (lookupAttribute)
import qualified OpenTelemetry.Attributes as A
import OpenTelemetry.Context (empty, insertSpan)
import OpenTelemetry.Context.ThreadLocal (attachContext)
import OpenTelemetry.Processor.Span (FlushResult (..), ShutdownResult (..), SpanProcessor (..))
import OpenTelemetry.Trace.Core
import OpenTelemetry.Util (appendOnlyBoundedCollectionValues)
import System.IO.Error (userError)
import Test.Hspec


dummyProcessor :: SpanProcessor
dummyProcessor =
  SpanProcessor
    { spanProcessorOnStart = \_ _ -> pure ()
    , spanProcessorOnEnd = \_ -> pure ()
    , spanProcessorShutdown = pure ShutdownSuccess
    , spanProcessorForceFlush = pure FlushSuccess
    }


withTracer :: (Tracer -> IO a) -> IO a
withTracer f = do
  tp <- createTracerProvider [dummyProcessor] emptyTracerProviderOptions
  let lib = InstrumentationLibrary "test" "1.0.0" "" A.emptyAttributes
      t = makeTracer tp lib tracerOptions
  f t


spec :: Spec
spec = describe "Trace utilities" $ do
  -- Trace API §Span Kind: SpanKind values
  -- https://opentelemetry.io/docs/specs/otel/trace/api/#spankind
  describe "SpanKind Eq" $ do
    it "compares equal SpanKinds" $ do
      Server `shouldBe` Server
      Client `shouldBe` Client
      Producer `shouldBe` Producer
      Consumer `shouldBe` Consumer
      Internal `shouldBe` Internal

    it "distinguishes different SpanKinds" $ do
      Client `shouldNotBe` Server
      Producer `shouldNotBe` Consumer
      Internal `shouldNotBe` Client

  -- Context API §Context interactions with OTel: active context (thread-local helper)
  -- https://opentelemetry.io/docs/specs/otel/context/
  describe "getActiveSpan" $ do
    it "returns Nothing when no span is active" $ do
      _ <- attachContext empty
      result <- getActiveSpan
      result `shouldSatisfy` isNothing

    it "returns the active span from thread-local context" $ withTracer $ \t -> do
      s <- createSpan t empty "test-span" defaultSpanArguments
      _ <- attachContext (insertSpan s empty)
      result <- getActiveSpan
      result `shouldSatisfy` isJust
      let Just activeSpan = result
      activeSc <- getSpanContext activeSpan
      expectedSc <- getSpanContext s
      spanId activeSc `shouldBe` spanId expectedSc

  describe "withActiveSpan" $ do
    it "does nothing when there is no active span" $ do
      _ <- attachContext empty
      ref <- newIORef False
      withActiveSpan $ \_ -> writeIORef ref True
      readIORef ref `shouldReturn` False

    it "runs the action with the active span" $ withTracer $ \t -> do
      s <- createSpan t empty "test-span" defaultSpanArguments
      _ <- attachContext (insertSpan s empty)
      ref <- newIORef False
      withActiveSpan $ \_ -> writeIORef ref True
      readIORef ref `shouldReturn` True

  describe "getActiveSpanContext" $ do
    it "returns Nothing when no span is active" $ do
      _ <- attachContext empty
      result <- getActiveSpanContext
      result `shouldSatisfy` isNothing

    it "returns the active SpanContext" $ withTracer $ \t -> do
      s <- createSpan t empty "test-span" defaultSpanArguments
      expectedSc <- getSpanContext s
      _ <- attachContext (insertSpan s empty)
      result <- getActiveSpanContext
      result `shouldSatisfy` isJust
      let Just sc = result
      traceId sc `shouldBe` traceId expectedSc
      spanId sc `shouldBe` spanId expectedSc

  -- Trace API §Add Events: event name and attributes
  -- https://opentelemetry.io/docs/specs/otel/trace/api/#add-events
  describe "newEvent" $ do
    it "creates an event with just a name" $ do
      let e = newEvent "my-event"
      newEventName e `shouldBe` "my-event"
      newEventAttributes e `shouldBe` H.empty
      newEventTimestamp e `shouldBe` Nothing

  describe "newEventWith" $ do
    it "creates an event with name and attributes" $ do
      let attrs = H.fromList [("key" :: Text, A.toAttribute ("val" :: Text))]
          e = newEventWith "my-event" attrs
      newEventName e `shouldBe` "my-event"
      newEventAttributes e `shouldBe` attrs

  -- Trace API §Record exception: exception event and span status
  -- https://opentelemetry.io/docs/specs/otel/trace/api/#record-exception
  describe "recordError" $ do
    it "sets span status to Error and records exception event" $ withTracer $ \t -> do
      s <- createSpan t empty "test-span" defaultSpanArguments
      let err = userError "something broke"
      recordError s err
      endSpan s Nothing
      is <- unsafeReadSpan s
      hot <- readIORef (spanHot is)
      hotStatus hot `shouldSatisfy` \case Error _ -> True; _ -> False
      let evts = appendOnlyBoundedCollectionValues (hotEvents hot)
      V.length evts `shouldBe` 1
      let evt = V.head evts
      eventName evt `shouldBe` "exception"
      lookupAttribute (eventAttributes evt) "exception.type" `shouldSatisfy` isJust
      lookupAttribute (eventAttributes evt) "exception.message" `shouldSatisfy` isJust