effectful-tracing-0.1.0.0: src/Effectful/Tracing/Internal/Types.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Effectful.Tracing.Internal.Types
-- Description : Core span data model shared by all interpreters.
-- Copyright : (c) The effectful-tracing contributors
-- License : BSD-3-Clause
-- Stability : internal
--
-- The effect-system-independent data types that every interpreter shares:
-- trace flags, trace state, span context, and the immutable record of a
-- completed 't:Span'. These deliberately do not depend on @effectful@ or
-- @hs-opentelemetry@; translation to OpenTelemetry happens in the bridge.
module Effectful.Tracing.Internal.Types
( -- * Trace flags
TraceFlags (..)
, defaultTraceFlags
, isSampled
, setSampled
-- * Trace state
, TraceState
, emptyTraceState
, insertTraceState
, lookupTraceState
, traceStateEntries
, traceStateToHeader
, traceStateFromHeader
, maxTraceStateEntries
-- * Span context
, SpanContext (..)
-- * Span metadata
, SpanKind (..)
, SpanStatus (..)
, Event (..)
, Link (..)
-- * Completed span
, Span (..)
) where
import Data.Bits (clearBit, setBit, testBit)
import Data.Char (isAsciiLower, isDigit, ord)
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Word (Word8)
import Effectful.Tracing.Attribute (Attribute)
import Effectful.Tracing.Internal.Clock (Timestamp)
import Effectful.Tracing.Internal.Ids (SpanId, TraceId)
-- | W3C trace flags. Only bit 0 (@sampled@) currently has a defined meaning;
-- the remaining bits are reserved and are preserved across round-trips.
newtype TraceFlags = TraceFlags Word8
deriving (Eq, Ord, Show)
-- | Trace flags with all bits clear (not sampled).
defaultTraceFlags :: TraceFlags
defaultTraceFlags = TraceFlags 0
-- | Whether the @sampled@ bit is set.
isSampled :: TraceFlags -> Bool
isSampled (TraceFlags w) = testBit w 0
-- | Set or clear the @sampled@ bit, leaving the reserved bits untouched.
setSampled :: Bool -> TraceFlags -> TraceFlags
setSampled True (TraceFlags w) = TraceFlags (setBit w 0)
setSampled False (TraceFlags w) = TraceFlags (clearBit w 0)
-- | W3C @tracestate@: an ordered list of key/value pairs, most-recently-mutated
-- first, with at most 'maxTraceStateEntries' entries. Construct it through
-- 'emptyTraceState' and 'insertTraceState' (which validate keys and values) or
-- parse it with 'traceStateFromHeader'.
newtype TraceState = TraceState [(Text, Text)]
deriving (Eq, Show)
-- | The maximum number of entries a 't:TraceState' may hold (32, per the W3C
-- spec).
maxTraceStateEntries :: Int
maxTraceStateEntries = 32
-- | An empty trace state.
emptyTraceState :: TraceState
emptyTraceState = TraceState []
-- | The entries of a trace state, in order (most-recently-mutated first).
traceStateEntries :: TraceState -> [(Text, Text)]
traceStateEntries (TraceState entries) = entries
-- | Insert or update a key, moving it to the head (most recent). Returns
-- 'Nothing' if the key or value is invalid, or if adding a new key would exceed
-- 'maxTraceStateEntries'.
insertTraceState :: Text -> Text -> TraceState -> Maybe TraceState
insertTraceState key value (TraceState entries)
| not (isValidKey key) = Nothing
| not (isValidValue value) = Nothing
| length without >= maxTraceStateEntries = Nothing
| otherwise = Just (TraceState ((key, value) : without))
where
without = filter ((/= key) . fst) entries
-- | Look up a key's value.
lookupTraceState :: Text -> TraceState -> Maybe Text
lookupTraceState key (TraceState entries) = lookup key entries
-- | Serialize to a @tracestate@ header value.
traceStateToHeader :: TraceState -> Text
traceStateToHeader (TraceState entries) =
T.intercalate "," [key <> "=" <> value | (key, value) <- entries]
-- | Parse a @tracestate@ header value. Total: malformed members are dropped
-- (per the W3C resilience guidance), duplicate keys keep their first
-- occurrence, and the result is capped at 'maxTraceStateEntries'.
traceStateFromHeader :: Text -> TraceState
traceStateFromHeader header =
TraceState (take maxTraceStateEntries (dedupe (mapMaybe parseMember members)))
where
members = T.splitOn "," header
parseMember member =
let trimmed = T.strip member
(key, rest) = T.breakOn "=" trimmed
in case T.stripPrefix "=" rest of
Just value | isValidKey key && isValidValue value -> Just (key, value)
_ -> Nothing
dedupe = go []
where
go _ [] = []
go seen (kv@(key, _) : rest)
| key `elem` seen = go seen rest
| otherwise = kv : go (key : seen) rest
-- | Whether a string is a valid @tracestate@ key. This validates a practical
-- subset of the W3C grammar: a non-empty, at-most-256-character string of
-- @[a-z0-9_\-*\/\@]@ beginning with a lowercase letter or digit.
isValidKey :: Text -> Bool
isValidKey key =
not (T.null key)
&& T.length key <= 256
&& isKeyStart (T.head key)
&& T.all isKeyChar key
where
isKeyStart c = isAsciiLower c || isDigit c
isKeyChar c = isAsciiLower c || isDigit c || c `elem` ['_', '-', '*', '/', '@']
-- | Whether a string is a valid @tracestate@ value: non-empty, at most 256
-- printable ASCII characters excluding comma and equals, with no trailing
-- space.
isValidValue :: Text -> Bool
isValidValue value =
not (T.null value)
&& T.length value <= 256
&& T.all isValueChar value
&& T.last value /= ' '
where
isValueChar c =
let o = ord c
in o >= 0x20 && o <= 0x7E && c /= ',' && c /= '='
-- | The identity of a span and the trace it belongs to, as propagated in-band
-- and across process boundaries.
data SpanContext = SpanContext
{ spanContextTraceId :: !TraceId
, spanContextSpanId :: !SpanId
, spanContextTraceFlags :: !TraceFlags
, spanContextTraceState :: !TraceState
, spanContextIsRemote :: !Bool
}
deriving (Eq, Show)
-- | The role a span plays in a trace, per OpenTelemetry.
data SpanKind
= Internal
| Server
| Client
| Producer
| Consumer
deriving (Eq, Show, Enum, Bounded)
-- | A span's status. 'Unset' is the default; 'Error' carries a description.
data SpanStatus
= Unset
| Ok
| Error !Text
deriving (Eq, Show)
-- | A timestamped, named occurrence within a span.
data Event = Event
{ eventName :: !Text
, eventTime :: !Timestamp
, eventAttributes :: ![Attribute]
}
deriving (Eq, Show)
-- | A reference from this span to another span (possibly in another trace).
data Link = Link
{ linkContext :: !SpanContext
, linkAttributes :: ![Attribute]
}
deriving (Eq, Show)
-- | The immutable record of a completed span. This is the value that crosses
-- the boundary into an interpreter for capture or export. The
-- mutable-during-construction representation lives in the interpreter layer.
data Span = Span
{ spanContext :: !SpanContext
, spanParentContext :: !(Maybe SpanContext)
, spanName :: !Text
, spanKind :: !SpanKind
, spanStartTime :: !Timestamp
, spanEndTime :: !Timestamp
, spanAttributes :: ![Attribute]
, spanEvents :: ![Event]
, spanLinks :: ![Link]
, spanStatus :: !SpanStatus
}
deriving (Eq, Show)