packages feed

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

-- |
-- Module      : Effectful.Tracing.Gen
-- Description : Hedgehog generators for the core data model.
--
-- Generators for every public type in the core data model, used by the
-- property tests.
module Effectful.Tracing.Gen
  ( genTraceId
  , genSpanId
  , genAttributeKey
  , genAttributeValue
  , genAttribute
  , genTraceFlags
  , genTraceState
  , genTimestamp
  , genSpanContext
  , genSpanKind
  , genSpanStatus
  , genEvent
  , genLink
  , genSpan
  ) where

import Data.Int (Int64)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Vector qualified as V
import Data.Time.Clock (addUTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)

import Hedgehog (Gen)
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range

import Effectful.Tracing.Attribute (Attribute (..), AttributeValue (..))
import Effectful.Tracing.Internal.Clock (Timestamp (..))
import Effectful.Tracing.Internal.Ids (SpanId (..), TraceId (..))
import Effectful.Tracing.Internal.Types
  ( Event (..)
  , Link (..)
  , Span (..)
  , SpanContext (..)
  , SpanKind
  , SpanStatus (..)
  , TraceFlags (..)
  , TraceState
  , emptyTraceState
  , insertTraceState
  )

-- | A 16-byte trace identifier (may be all-zero, which is fine for the codec
-- properties).
genTraceId :: Gen TraceId
genTraceId = TraceId <$> Gen.bytes (Range.singleton 16)

-- | An 8-byte span identifier.
genSpanId :: Gen SpanId
genSpanId = SpanId <$> Gen.bytes (Range.singleton 8)

genText :: Gen Text
genText = Gen.text (Range.linear 0 16) Gen.alphaNum

-- | A non-empty attribute key.
genAttributeKey :: Gen Text
genAttributeKey = Gen.text (Range.linear 1 24) Gen.alphaNum

genInt64 :: Gen Int64
genInt64 = Gen.integral (Range.linearFrom 0 minBound maxBound)

genDouble :: Gen Double
genDouble = Gen.double (Range.linearFracFrom 0 (-1.0e9) 1.0e9)

-- | Any attribute value, scalar or homogeneous array.
genAttributeValue :: Gen AttributeValue
genAttributeValue =
  Gen.choice
    [ AttrText <$> genText
    , AttrBool <$> Gen.bool
    , AttrInt <$> genInt64
    , AttrDouble <$> genDouble
    , AttrTextArray . V.fromList <$> Gen.list (Range.linear 0 4) genText
    , AttrBoolArray . V.fromList <$> Gen.list (Range.linear 0 4) Gen.bool
    , AttrIntArray . V.fromList <$> Gen.list (Range.linear 0 4) genInt64
    , AttrDoubleArray . V.fromList <$> Gen.list (Range.linear 0 4) genDouble
    ]

genAttribute :: Gen Attribute
genAttribute = Attribute <$> genAttributeKey <*> genAttributeValue

genTraceFlags :: Gen TraceFlags
genTraceFlags = TraceFlags <$> Gen.word8 Range.constantBounded

-- | A valid trace state, built through 'insertTraceState' so all entries
-- satisfy the W3C key/value constraints and the entry cap.
genTraceState :: Gen TraceState
genTraceState = do
  pairs <- Gen.list (Range.linear 0 10) ((,) <$> genStateKey <*> genStateValue)
  pure (foldr addEntry emptyTraceState pairs)
  where
    addEntry (key, value) st = fromMaybe st (insertTraceState key value st)

genStateKey :: Gen Text
genStateKey = do
  start <- Gen.element keyStartChars
  rest <- Gen.list (Range.linear 0 8) (Gen.element keyChars)
  pure (mkText (start : rest))
  where
    keyStartChars = ['a' .. 'z'] <> ['0' .. '9']
    keyChars = keyStartChars <> "_-*/@"

genStateValue :: Gen Text
genStateValue = mkText <$> Gen.list (Range.linear 1 12) (Gen.element valueChars)
  where
    valueChars = [c | c <- [' ' .. '~'], c /= ' ', c /= ',', c /= '=']

mkText :: String -> Text
mkText = T.pack

genTimestamp :: Gen Timestamp
genTimestamp = do
  secs <- Gen.integral (Range.linear 0 2_000_000_000)
  pure (Timestamp (posixSecondsToUTCTime (fromInteger (secs :: Integer))))

genSpanContext :: Gen SpanContext
genSpanContext =
  SpanContext
    <$> genTraceId
    <*> genSpanId
    <*> genTraceFlags
    <*> genTraceState
    <*> Gen.bool

genSpanKind :: Gen SpanKind
genSpanKind = Gen.enumBounded

genSpanStatus :: Gen SpanStatus
genSpanStatus =
  Gen.choice
    [ pure Unset
    , pure Ok
    , Error <$> genText
    ]

genEvent :: Gen Event
genEvent =
  Event
    <$> genText
    <*> genTimestamp
    <*> Gen.list (Range.linear 0 4) genAttribute

genLink :: Gen Link
genLink = Link <$> genSpanContext <*> Gen.list (Range.linear 0 4) genAttribute

-- | A completed span with @start <= end@ by construction.
genSpan :: Gen Span
genSpan = do
  context <- genSpanContext
  parent <- Gen.maybe genSpanContext
  name <- genText
  kind <- genSpanKind
  Timestamp start <- genTimestamp
  durationSecs <- Gen.integral (Range.linear 0 100_000)
  let end = addUTCTime (fromInteger (durationSecs :: Integer)) start
  attributes <- Gen.list (Range.linear 0 6) genAttribute
  events <- Gen.list (Range.linear 0 4) genEvent
  links <- Gen.list (Range.linear 0 4) genLink
  status <- genSpanStatus
  pure
    Span
      { spanContext = context
      , spanParentContext = parent
      , spanName = name
      , spanKind = kind
      , spanStartTime = Timestamp start
      , spanEndTime = Timestamp end
      , spanAttributes = attributes
      , spanEvents = events
      , spanLinks = links
      , spanStatus = status
      }