otel-effectful-1.0.0: src/Effectful/OpenTelemetry/Tracing/Trace/State.hs
module Effectful.OpenTelemetry.Tracing.Trace.State
( Key
, key
, Value
, value
, State
, fromList
, fromText
, toText
, fromByteString
, toByteString
, toList
, lookup
, insert
, delete
)
where
import Data.Aeson (ToJSON (..))
import Data.ByteString (ByteString)
import Data.Char qualified as Char
import Data.Foldable qualified as Foldable
import Data.List qualified as List
import Data.List.Extra qualified as List
import Data.Maybe (fromJust, mapMaybe)
import Data.Sequence (Seq)
import Data.Sequence qualified as Seq
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import GHC.IsList (IsList)
import GHC.IsList qualified as GHC
import Proto3.Wire.Encode.Class qualified as Proto
import Prelude hiding (lookup)
-- | W3C TraceState Key
--
-- See <https://www.w3.org/TR/trace-context/#key the W3C specification>.
newtype Key = Key Text
deriving newtype (Show, Eq, Ord)
-- | W3C TraceState Value
--
-- See <https://www.w3.org/TR/trace-context/#value the W3C specification>.
newtype Value = Value Text
deriving newtype (Show, Eq)
-- | W3C TraceState
--
-- May hold at most 32 unique 'Key's.
-- The 'Semigroup' operation for 'State' prefers values from the left operand.
newtype State = State (Seq (Key, Value))
deriving newtype (Show, Eq, Monoid)
instance Semigroup State where
State left <> State right =
State . Seq.take 32 $ left <> Seq.filter (not . isInLeft . fst) right
where
isInLeft = (`elem` (fst <$> left))
instance IsList State where
type Item State = (Key, Value)
fromList = fromList
toList = toList
instance ToJSON State where
toJSON = toJSON . toText
instance {-# OVERLAPPING #-} Proto.EncodeField State where
encodeField n = Proto.encodeField n . toText
toList :: State -> [(Key, Value)]
toList (State s) = Foldable.toList s
fromList :: [(Key, Value)] -> State
fromList = State . Seq.fromList . List.take 32 . List.nubOrdOn fst
-- | Parse a 'State' from a comma-separated list of 'key'@=@'value' pairs.
-- Drops any key-value pairs that cannot be parsed.
-- Evaluates to an empty state if the 'Text' contains no parseable key-value pairs.
fromText :: Text -> State
fromText = fromList . mapMaybe (kv . Text.strip) . Text.splitOn ","
where
kv :: Text -> Maybe (Key, Value)
kv t = case Text.breakOn "=" t of
( key -> Just k
, Text.stripPrefix "=" -> fromJust -> value -> Just v
) -> Just (k, v)
_ -> Nothing
toText :: State -> Text
toText = Text.intercalate "," . fmap (\(Key k, Value v) -> k <> "=" <> v) . toList
toByteString :: State -> ByteString
toByteString = Text.encodeUtf8 . toText
-- | Parse a 'State' from a comma-separated list of 'key'@=@'value' pairs.
-- Drops any key-value pairs that cannot be parsed.
-- Evaluates to an empty state if the 'ByteString' contains no parseable key-value pairs.
fromByteString :: ByteString -> State
fromByteString = either (const mempty) fromText . Text.decodeUtf8'
-- | Construct a 'Key', or 'Nothing' on invalid input.
key :: Text -> Maybe Key
key (Text.strip -> t)
| isValidKey = Just (Key t)
| otherwise = Nothing
where
isValidKey
| Text.length t > 256 = False
| otherwise = case Text.splitOn "@" t of
[simpleKey] -> isValidSimpleKey simpleKey
[tenant, system] -> isValidTenantKey tenant system
_ -> False
isValidTenantKey tenant system =
not (Text.null tenant)
&& Text.length tenant <= 241
&& Text.length system <= 14
&& isValidSimpleKey tenant
&& isValidSimpleKey system
isValidSimpleKey k = case Text.uncons k of
Just (firstChar, rest) -> Char.isAsciiLower firstChar && Text.all isSimpleChar rest
Nothing -> False
isSimpleChar c = Char.isAsciiLower c || Char.isDigit c || c `elem` ['_', '-', '*', '/']
-- | Construct a 'Value', or 'Nothing' on invalid input.
value :: Text -> Maybe Value
value (Text.strip -> t)
| isValidValue = Just (Value t)
| otherwise = Nothing
where
isValidValue = Text.length t < 256 && Text.all isValidChar t
isValidChar c =
let code = fromEnum c
in code >= 0x20 && code <= 0x7E && c /= ',' && c /= '='
-- | Lookup a 'Value' by 'Key'.
lookup :: Key -> State -> Maybe Value
lookup k = List.lookup k . toList
-- | Insert or update a 'Key'-'Value' pair.
-- If the key already exists, it is moved to the left-most position in 'State'.
-- If the 'State' contains 32 keys before insertion, the right-most key will be removed.
insert :: Key -> Value -> State -> State
insert k v s = State (pure (k, v)) <> s
-- | Remove a 'Value' by 'Key' if it exists.
delete :: Key -> State -> State
delete k (State s) = State $ Seq.filter ((k /=) . fst) s