effectful-tracing-0.1.0.0: src/Effectful/Tracing/Interpreter/InMemory.hs
{-# LANGUAGE DataKinds #-}
-- |
-- Module : Effectful.Tracing.Interpreter.InMemory
-- Description : An interpreter that captures completed spans in memory.
-- Copyright : (c) The effectful-tracing contributors
-- License : BSD-3-Clause
-- Stability : experimental
--
-- 'runTracerInMemory' captures every completed 't:Span' into a shared buffer so
-- tests can assert on what a traced computation produced. It is the workhorse
-- for testing user code that uses 'Tracer', and the reference against which the
-- later interpreters are tested.
--
-- == How to test traced code
--
-- > import Effectful (runEff)
-- > import Effectful.Tracing
-- > import Effectful.Tracing.Interpreter.InMemory
-- >
-- > test = do
-- > captured <- newCapturedSpans
-- > _ <- runEff . runTracerInMemory captured $
-- > withSpan "outer" (withSpan "inner" (pure ()))
-- > spans <- readCapturedSpans captured
-- > -- inner closes before outer, so it is captured first:
-- > let Just inner = findSpan "inner" spans
-- > Just outer = findSpan "outer" spans
-- > pure (childrenOf outer spans == [inner])
--
-- == Design
--
-- The span lifecycle (lexical active span, finalize-exactly-once under
-- 'Effectful.Exception.generalBracket') is shared with the other span-opening interpreters and
-- lives in "Effectful.Tracing.Internal.Live". This module supplies only the
-- sink: append each completed span to a shared, write-only capture buffer.
module Effectful.Tracing.Interpreter.InMemory
( -- * Capturing spans
CapturedSpans
, newCapturedSpans
, readCapturedSpans
, runTracerInMemory
, runTracerInMemoryWith
, runTracerInMemoryWithLimits
-- * Querying captured spans
, findSpan
, childrenOf
, rootSpans
) where
import Control.Concurrent.STM (TVar, atomically, modifyTVar', newTVarIO, readTVarIO)
import Control.Monad.IO.Class (liftIO)
import Data.Foldable (toList)
import Data.List (find)
import Data.Maybe (isNothing)
import Data.Sequence (Seq, (|>))
import Data.Sequence qualified as Seq
import Data.Text (Text)
import Effectful (Eff, IOE, (:>))
import Effectful.Tracing.Effect (Tracer)
import Effectful.Tracing.Internal.Live (interpretTracer)
import Effectful.Tracing.Sampler (Sampler, alwaysOn)
import Effectful.Tracing.SpanLimits (SpanLimits, defaultSpanLimits)
import Effectful.Tracing.Internal.Types
( Span (spanContext, spanName, spanParentContext)
, SpanContext (spanContextSpanId, spanContextTraceId)
)
-- | A buffer of completed spans, shared across threads. Created with
-- 'newCapturedSpans' and read with 'readCapturedSpans'.
newtype CapturedSpans = CapturedSpans (TVar (Seq Span))
-- | Allocate an empty capture buffer.
newCapturedSpans :: IOE :> es => Eff es CapturedSpans
newCapturedSpans = liftIO (CapturedSpans <$> newTVarIO Seq.empty)
-- | Read the spans captured so far, in completion order (a child span, which
-- closes before its parent, appears before the parent).
readCapturedSpans :: IOE :> es => CapturedSpans -> Eff es [Span]
readCapturedSpans (CapturedSpans buffer) = liftIO (toList <$> readTVarIO buffer)
-- | Capture completed spans into the given buffer, sampling every span
-- ('alwaysOn'). Scoped actions run inside a fresh child span; emit operations
-- annotate the lexically-current span and are silent no-ops when there is none.
runTracerInMemory
:: IOE :> es
=> CapturedSpans
-> Eff (Tracer : es) a
-> Eff es a
runTracerInMemory = runTracerInMemoryWith alwaysOn
-- | Like 'runTracerInMemory', but with a caller-chosen 't:Sampler'. There is no
-- export step here, so 'Effectful.Tracing.Sampler.RecordOnly' and 'Effectful.Tracing.Sampler.RecordAndSample' both capture the
-- span; only 'Effectful.Tracing.Sampler.Drop' omits it. Uses 'defaultSpanLimits';
-- 'runTracerInMemoryWithLimits' overrides them.
runTracerInMemoryWith
:: IOE :> es
=> Sampler
-> CapturedSpans
-> Eff (Tracer : es) a
-> Eff es a
runTracerInMemoryWith = runTracerInMemoryWithLimits defaultSpanLimits
-- | Like 'runTracerInMemoryWith', but with caller-chosen 'SpanLimits' as well as
-- a sampler. Pass 'Effectful.Tracing.SpanLimits.unlimitedSpanLimits' to capture
-- everything a computation emitted, or a tightened 'SpanLimits' to exercise the
-- caps in a test.
runTracerInMemoryWithLimits
:: IOE :> es
=> SpanLimits
-> Sampler
-> CapturedSpans
-> Eff (Tracer : es) a
-> Eff es a
runTracerInMemoryWithLimits limits sampler (CapturedSpans buffer) =
interpretTracer limits sampler (\completed -> atomically (modifyTVar' buffer (|> completed)))
-- | Find the first captured span with the given name.
findSpan :: Text -> [Span] -> Maybe Span
findSpan name = find ((== name) . spanName)
-- | The captured spans whose parent is the given span.
childrenOf :: Span -> [Span] -> [Span]
childrenOf parent = filter isChild
where
parentContext = spanContext parent
isChild s = case spanParentContext s of
Just pc ->
spanContextSpanId pc == spanContextSpanId parentContext
&& spanContextTraceId pc == spanContextTraceId parentContext
Nothing -> False
-- | The captured spans that have no parent.
rootSpans :: [Span] -> [Span]
rootSpans = filter (isNothing . spanParentContext)