effectful-tracing-0.1.0.0: src/Effectful/Tracing/SpanLimits.hs
-- |
-- Module : Effectful.Tracing.SpanLimits
-- Description : Per-span caps on attributes, events, links, and value length.
-- Copyright : (c) The effectful-tracing contributors
-- License : BSD-3-Clause
-- Stability : experimental
--
-- A span with no upper bound on what it records is a memory hazard: an
-- accidental loop that calls @addEvent@ or @addAttribute@ grows the in-flight
-- span without limit, and a stray multi-megabyte string value rides all the way
-- to the exporter. OpenTelemetry's SDK guards against this with __span limits__,
-- and this module is the same idea: a small 'SpanLimits' record that the
-- span-opening interpreters honour.
--
-- Four caps are modelled, each a @'Maybe' 'Int'@ where 'Nothing' means
-- unlimited:
--
-- * 'attributeCountLimit': the most attributes a span keeps. Attributes past the
-- cap are dropped (the earliest are kept).
-- * 'attributeValueLengthLimit': the most characters a string attribute value
-- keeps; longer values (and the elements of string arrays) are truncated.
-- * 'eventCountLimit': the most events a span keeps (earliest kept).
-- * 'linkCountLimit': the most links a span keeps (earliest kept).
--
-- 'defaultSpanLimits' matches the OpenTelemetry defaults (128 attributes, 128
-- events, 128 links, and no value-length cap); 'unlimitedSpanLimits' disables
-- every cap. The interpreters apply the count caps as a span records, so an
-- in-flight span cannot grow past the limit, and 'applySpanLimits' is the pure
-- transform that produces the final, capped-and-truncated span. Keeping that
-- transform pure is what makes the whole policy testable without running an
-- interpreter.
module Effectful.Tracing.SpanLimits
( -- * Limits
SpanLimits (..)
, defaultSpanLimits
, unlimitedSpanLimits
-- * Applying limits (pure)
, applySpanLimits
) where
import Data.Text qualified as T
import Data.Vector qualified as V
import Effectful.Tracing.Attribute (Attribute (Attribute), AttributeValue (AttrText, AttrTextArray))
import Effectful.Tracing.Internal.Types
( Event (eventAttributes)
, Link (linkAttributes)
, Span (spanAttributes, spanEvents, spanLinks)
)
-- | The per-span caps an interpreter enforces. Each count is a @'Maybe' 'Int'@:
-- 'Nothing' disables that cap, @'Just' n@ caps at @n@ (a negative @n@ is treated
-- as @0@).
data SpanLimits = SpanLimits
{ attributeCountLimit :: !(Maybe Int)
-- ^ The most attributes a span keeps. Once reached, further attributes are
-- dropped; the earliest-recorded are the ones kept.
, attributeValueLengthLimit :: !(Maybe Int)
-- ^ The most characters a string attribute value keeps. Longer 'AttrText'
-- values, and the elements of 'AttrTextArray' values, are truncated to this
-- many characters. Non-string values are unaffected.
, eventCountLimit :: !(Maybe Int)
-- ^ The most events a span keeps (earliest kept).
, linkCountLimit :: !(Maybe Int)
-- ^ The most links a span keeps (earliest kept).
}
deriving (Eq, Show)
-- | The OpenTelemetry default limits: 128 attributes, 128 events, 128 links, and
-- no value-length cap.
defaultSpanLimits :: SpanLimits
defaultSpanLimits =
SpanLimits
{ attributeCountLimit = Just 128
, attributeValueLengthLimit = Nothing
, eventCountLimit = Just 128
, linkCountLimit = Just 128
}
-- | No caps at all: a span keeps every attribute, event, and link, and never
-- truncates a value. Useful in tests that assert on everything a computation
-- emitted.
unlimitedSpanLimits :: SpanLimits
unlimitedSpanLimits =
SpanLimits
{ attributeCountLimit = Nothing
, attributeValueLengthLimit = Nothing
, eventCountLimit = Nothing
, linkCountLimit = Nothing
}
-- | Apply the limits to a completed span: cap its attributes, events, and links
-- to their counts (keeping the earliest), and truncate every string attribute
-- value (on the span, its events, and its links) to the value-length cap. Pure,
-- so it is the unit under test for the limit policy and is what every
-- span-opening interpreter runs as it finalizes a span.
applySpanLimits :: SpanLimits -> Span -> Span
applySpanLimits limits s =
s
{ spanAttributes = map truncateAttr (capCount (attributeCountLimit limits) (spanAttributes s))
, spanEvents = map truncateEventAttrs (capCount (eventCountLimit limits) (spanEvents s))
, spanLinks = map truncateLinkAttrs (capCount (linkCountLimit limits) (spanLinks s))
}
where
truncateAttr = truncateAttribute (attributeValueLengthLimit limits)
truncateEventAttrs e = e {eventAttributes = map truncateAttr (eventAttributes e)}
truncateLinkAttrs l = l {linkAttributes = map truncateAttr (linkAttributes l)}
-- | Keep at most @n@ of a list (the first @n@) when a cap is set; a negative cap
-- keeps none. 'Nothing' keeps everything.
capCount :: Maybe Int -> [a] -> [a]
capCount Nothing = id
capCount (Just n) = take (max 0 n)
-- | Truncate a string attribute value to the character cap, if one is set.
-- 'AttrText' is truncated; 'AttrTextArray' has each element truncated; every
-- other value is returned unchanged.
truncateAttribute :: Maybe Int -> Attribute -> Attribute
truncateAttribute Nothing attr = attr
truncateAttribute (Just n) (Attribute key value) = Attribute key (truncateValue (max 0 n) value)
truncateValue :: Int -> AttributeValue -> AttributeValue
truncateValue n (AttrText t) = AttrText (T.take n t)
truncateValue n (AttrTextArray v) = AttrTextArray (V.map (T.take n) v)
truncateValue _ value = value