packages feed

effectful-tracing-0.1.0.0: src/Effectful/Tracing/Testing.hs

-- |
-- Module      : Effectful.Tracing.Testing
-- Description : Helpers for testing code that uses the 'Effectful.Tracing.Tracer' effect.
-- Copyright   : (c) The effectful-tracing contributors
-- License     : BSD-3-Clause
-- Stability   : experimental
--
-- A one-stop module for testing your own instrumentation. It re-exports the
-- in-memory capture interpreter (run a 'Effectful.Tracing.Tracer' computation,
-- collect the completed spans) and adds pure matchers and finders over the
-- captured @['Span']@, so a test can assert on span shape, parent/child
-- structure, attributes, status, events, and kind.
--
-- The matchers are plain predicates ('Bool') and lookups ('Maybe'), with no
-- dependency on any test framework, so they compose with @tasty-hunit@,
-- @hspec@, @hedgehog@, or anything else: pair them with your framework's
-- assertion combinator.
--
-- > import Effectful (runEff)
-- > import Effectful.Tracing (SpanStatus (Ok), withSpan, addAttribute, setStatus)
-- > import Effectful.Tracing.Testing
-- > import Test.Tasty.HUnit (assertBool, (@?=))
-- >
-- > test = do
-- >   captured <- newCapturedSpans
-- >   _ <- runEff . runTracerInMemory captured $
-- >     withSpan "handler" $ do
-- >       addAttribute "http.response.status_code" (200 :: Int)
-- >       setStatus Ok
-- >       withSpan "db.query" (pure ())
-- >   spans <- readCapturedSpans captured
-- >   case (findSpan "handler" spans, findSpan "db.query" spans) of
-- >     (Just handler, Just db) -> do
-- >       assertBool "db.query is a child of handler" (db `isChildOf` handler)
-- >       assertBool "handler is a root" (isRoot handler)
-- >       hasStatus Ok handler @?= True
-- >       lookupAttribute "http.response.status_code" handler
-- >         @?= Just (AttrInt 200)
-- >     _ -> assertBool "both spans were captured" False
module Effectful.Tracing.Testing
  ( -- * Capturing spans
    -- | Re-exported from "Effectful.Tracing.Interpreter.InMemory".
    CapturedSpans
  , newCapturedSpans
  , readCapturedSpans
  , runTracerInMemory
  , runTracerInMemoryWith
  , runTracerInMemoryWithLimits

    -- * Finding spans
  , findSpan
  , findSpans
  , rootSpans
  , childrenOf
  , descendantsOf

    -- * Structure
  , isRoot
  , isChildOf

    -- * Attributes
  , lookupAttribute
  , hasAttribute
  , hasAttributeValue

    -- * Status, events, and kind
  , hasStatus
  , lookupEvent
  , hasEvent
  , hasKind
  ) where

import Data.List (find)
import Data.Maybe (isJust, isNothing)
import Data.Text (Text)

import Effectful.Tracing.Attribute
  ( Attribute (attributeKey, attributeValue)
  , AttributeValue
  )
import Effectful.Tracing.Interpreter.InMemory
  ( CapturedSpans
  , childrenOf
  , findSpan
  , newCapturedSpans
  , readCapturedSpans
  , rootSpans
  , runTracerInMemory
  , runTracerInMemoryWith
  , runTracerInMemoryWithLimits
  )
import Effectful.Tracing.Internal.Types
  ( Event (eventName)
  , Span
      ( spanAttributes
      , spanContext
      , spanEvents
      , spanKind
      , spanParentContext
      , spanStatus
      )
  , SpanContext (spanContextSpanId, spanContextTraceId)
  , SpanKind
  , SpanStatus
  , spanName
  )

-- | All captured spans with the given name, in capture (completion) order.
-- Use this rather than 'findSpan' when a name can repeat (a loop body, a
-- retried operation) and you want every occurrence.
findSpans :: Text -> [Span] -> [Span]
findSpans name = filter ((== name) . spanName)

-- | The spans transitively beneath the given span: its children, their
-- children, and so on. Captured spans form a tree (each has at most one
-- parent), so this terminates and visits each descendant once.
descendantsOf :: Span -> [Span] -> [Span]
descendantsOf parent spans = go (childrenOf parent spans)
  where
    go [] = []
    go (s : rest) = s : go (childrenOf s spans <> rest)

-- | Whether a span is a trace root (it has no parent context).
isRoot :: Span -> Bool
isRoot = isNothing . spanParentContext

-- | @child \`isChildOf\` parent@ holds when @child@'s recorded parent is
-- @parent@'s own context (same trace id and span id).
isChildOf :: Span -> Span -> Bool
isChildOf child parent =
  case spanParentContext child of
    Just pc ->
      spanContextSpanId pc == spanContextSpanId parentContext
        && spanContextTraceId pc == spanContextTraceId parentContext
    Nothing -> False
  where
    parentContext = spanContext parent

-- | Look up a span attribute by key, returning its typed value if present.
lookupAttribute :: Text -> Span -> Maybe AttributeValue
lookupAttribute key s =
  attributeValue <$> find ((== key) . attributeKey) (spanAttributes s)

-- | Whether the span carries an attribute with the given key (any value).
hasAttribute :: Text -> Span -> Bool
hasAttribute key = isJust . lookupAttribute key

-- | Whether the span carries an attribute with the given key /and/ exactly the
-- given value.
hasAttributeValue :: Text -> AttributeValue -> Span -> Bool
hasAttributeValue key value s = lookupAttribute key s == Just value

-- | Whether the span ended with the given status.
hasStatus :: SpanStatus -> Span -> Bool
hasStatus status s = spanStatus s == status

-- | Look up the first event on the span with the given name.
lookupEvent :: Text -> Span -> Maybe Event
lookupEvent name s = find ((== name) . eventName) (spanEvents s)

-- | Whether the span recorded an event with the given name.
hasEvent :: Text -> Span -> Bool
hasEvent name = isJust . lookupEvent name

-- | Whether the span has the given kind.
hasKind :: SpanKind -> Span -> Bool
hasKind kind s = spanKind s == kind