packages feed

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

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

-- |
-- Module      : Effectful.Tracing.LifecycleSpec
-- Description : Tests for the shared span lifecycle in "Internal.Live".
--
-- These exercise the parts of the shared lifecycle that the per-interpreter
-- specs do not: continuing a remote trace under 'withRemoteParent', starting a
-- detached root in-thread under 'withLinkedRoot', honoring an explicit start
-- time, the 'setStatus' transition rules end to end, and the fact that
-- 'recordException' records an event without changing the status. They run
-- through the in-memory interpreter, which shares this lifecycle with the
-- pretty-print and OpenTelemetry interpreters.
module Effectful.Tracing.LifecycleSpec
  ( tests
  ) where

import Control.Exception (ErrorCall (ErrorCall), toException)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)

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

import Effectful.Tracing
  ( Event (eventName)
  , Link (Link, linkContext)
  , Span
  , SpanArguments (startTime)
  , SpanContext (..)
  , SpanStatus (Error, Ok, Unset)
  , Timestamp (Timestamp)
  , Tracer
  , defaultSpanArguments
  , defaultTraceFlags
  , emptyTraceState
  , getActiveSpan
  , recordException
  , setSampled
  , setStatus
  , spanContext
  , spanEvents
  , spanLinks
  , spanParentContext
  , spanStartTime
  , spanStatus
  , spanIdFromHex
  , traceIdFromHex
  , withLinkedRoot
  , withRemoteParent
  , withSpan
  , withSpan'
  )
import Effectful.Tracing.Interpreter.InMemory
  ( findSpan
  , newCapturedSpans
  , readCapturedSpans
  , runTracerInMemory
  )

tests :: TestTree
tests =
  testGroup
    "Span lifecycle"
    [ testCase "withRemoteParent continues the remote trace and parents to it" $ do
        spans <- captureSpans (withRemoteParent remoteContext (withSpan "child" (pure ())))
        child <- expectSpan "child" spans
        spanContextTraceId (spanContext child) @?= spanContextTraceId remoteContext
        fmap spanContextSpanId (spanParentContext child)
          @?= Just (spanContextSpanId remoteContext)
        assertBool
          "the child's parent is marked remote"
          (fmap spanContextIsRemote (spanParentContext child) == Just True)
    , testCase "withLinkedRoot starts a new trace in-thread and links back" $ do
        spans <-
          captureSpans $
            withSpan "outer" $ do
              parent <- getActiveSpan
              case parent of
                Just ctx -> withLinkedRoot [Link ctx []] (withSpan "root" (pure ()))
                Nothing -> pure ()
        outer <- expectSpan "outer" spans
        root <- expectSpan "root" spans
        spanParentContext root @?= Nothing
        assertBool
          "the linked root is a new trace, not the outer one"
          (spanContextTraceId (spanContext root) /= spanContextTraceId (spanContext outer))
        assertBool
          "the linked root carries a link back to the outer span"
          (spanContext outer `elem` map linkContext (spanLinks root))
    , testCase "an explicit start time is used verbatim" $ do
        let fixedStart = posixSecondsToUTCTime 1000 :: UTCTime
        spans <-
          captureSpans
            (withSpan' "timed" defaultSpanArguments {startTime = Just fixedStart} (pure ()))
        s <- expectSpan "timed" spans
        spanStartTime s @?= Timestamp fixedStart
    , testCase "Ok is terminal: a later Error does not override it" $ do
        spans <- captureSpans (withSpan "s" (setStatus Ok >> setStatus (Error "late")))
        s <- expectSpan "s" spans
        spanStatus s @?= Ok
    , testCase "an Error is overridden by a later Ok" $ do
        spans <- captureSpans (withSpan "s" (setStatus (Error "early") >> setStatus Ok))
        s <- expectSpan "s" spans
        spanStatus s @?= Ok
    , testCase "recordException records an event but leaves the status Unset" $ do
        spans <-
          captureSpans
            (withSpan "s" (recordException (toException (ErrorCall "noted, not fatal"))))
        s <- expectSpan "s" spans
        spanStatus s @?= Unset
        assertBool
          "an exception event is recorded"
          (any ((== "exception") . eventName) (spanEvents s))
    ]

-- | A remote span context, as if extracted from an inbound request.
remoteContext :: SpanContext
remoteContext =
  SpanContext
    { spanContextTraceId = unsafeHex traceIdFromHex "4bf92f3577b34da6a3ce929d0e0e4736"
    , spanContextSpanId = unsafeHex spanIdFromHex "00f067aa0ba902b7"
    , spanContextTraceFlags = setSampled True defaultTraceFlags
    , spanContextTraceState = emptyTraceState
    , spanContextIsRemote = True
    }

unsafeHex :: (Text -> Maybe a) -> Text -> a
unsafeHex parse raw = fromMaybe (error "bad fixture id") (parse raw)

captureSpans :: Eff '[Tracer, IOE] a -> IO [Span]
captureSpans program = runEff $ do
  captured <- newCapturedSpans
  _ <- runTracerInMemory captured program
  readCapturedSpans captured

expectSpan :: Text -> [Span] -> IO Span
expectSpan name spans =
  maybe (assertFailure ("expected a span named " <> show name)) pure (findSpan name spans)