packages feed

otel-effectful-1.0.0: src/Effectful/OpenTelemetry/Tracing/Trace/Flags.hs

module Effectful.OpenTelemetry.Tracing.Trace.Flags
    ( Flags (..)
    , Remote (..)
    , toHex
    , toWord8
    , toWord32
    , fromWord32
    )
where

import Control.Monad (mzero)
import Data.Aeson.Types (FromJSON (..), ToJSON (..), Value (..))
import Data.Bits (Bits (..))
import Data.Bool (bool)
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Word (Word32, Word8)
import GHC.Generics (Generic)
import Numeric (readHex, showHex)
import Proto3.Wire.Encode.Class qualified as Proto
import Prelude

-- | Describes whether a span's parent is remote.
data Remote
    = Unknown -- 0x
    | IsNotRemote -- 10
    | IsRemote -- 11
    deriving stock (Show, Eq, Bounded, Enum)

-- | Per-trace flags propagated alongside the 'Trace.ID' and 'Span.ID'.
data Flags = Flags
    { sampled :: Bool
    -- ^ The caller may have recorded trace data. When unset, the
    -- caller did not record trace data out-of-band.
    , random :: Bool
    -- ^ At least the right-most 7 bytes of the 'Trace.ID' have been
    -- selected randomly (or pseudo-randomly) with uniform distribution.
    , remote :: Remote
    }
    deriving stock (Generic, Show, Eq)

sampledBit, randomBit, hasIsRemoteBit, isRemoteBit :: Int
sampledBit = 0
randomBit = 1
hasIsRemoteBit = 8
isRemoteBit = 9

toWord32 :: Flags -> Word32
toWord32 Flags{..} =
    foldr
        (.|.)
        zeroBits
        [ setBit' sampledBit sampled
        , setBit' randomBit random
        , setBit' hasIsRemoteBit $ remote /= Unknown
        , setBit' isRemoteBit $ remote == IsRemote
        ]
  where
    setBit' :: Int -> Bool -> Word32
    setBit' = bool zeroBits . bit

fromWord32 :: Word32 -> Flags
fromWord32 bits =
    Flags
        { sampled = testBit bits sampledBit
        , random = testBit bits randomBit
        , remote = case (testBit bits hasIsRemoteBit, testBit bits isRemoteBit) of
            (False, _) -> Unknown
            (_, False) -> IsNotRemote
            (_, True) -> IsRemote
        }

instance Monoid Flags where
    mempty = fromWord32 zeroBits

instance Semigroup Flags where
    a <> b = fromWord32 $ toWord32 a .|. toWord32 b

toWord8 :: Flags -> Word8
toWord8 Flags{..} = setBit' sampledBit sampled .|. setBit' randomBit random
  where
    setBit' :: Int -> Bool -> Word8
    setBit' = bool zeroBits . bit

-- | Encode the @trace-flags@ field as the two hex characters it is specified as.
toHex :: Flags -> Text
toHex flags = Text.justifyRight 2 '0' . Text.pack $ showHex (toWord8 flags) ""

instance FromJSON Flags where
    parseJSON = \case
        Number n -> pure . fromWord32 . round $ n
        String s -> case readHex (Text.unpack s) of
            [(w, "")] -> pure $ fromWord32 w
            _ -> mzero
        _ -> mzero

instance ToJSON Flags where
    toJSON = Number . fromIntegral . toWord32

instance {-# OVERLAPPING #-} Proto.EncodeField Flags where
    encodeField n = Proto.encodeField n . toWord32