effectful-tracing-0.1.0.0: src/Effectful/Tracing/Baggage.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeFamilies #-}
-- |
-- Module : Effectful.Tracing.Baggage
-- Description : Ambient key-value context (W3C Baggage) as a scoped effect.
-- Copyright : (c) The effectful-tracing contributors
-- License : BSD-3-Clause
-- Stability : experimental
--
-- <https://www.w3.org/TR/baggage/ W3C Baggage> is a set of key-value pairs that
-- travels alongside a trace across service boundaries, carrying application
-- context (a tenant id, a feature flag, a request priority) that any downstream
-- service can read. Unlike span attributes, baggage is not tied to a single
-- span: it is __ambient__, in scope for everything that runs within it.
--
-- This module models that ambient context the same way the 'Tracer' interpreters
-- model the active span: __lexically__, carried in a private @effectful@
-- @Reader@ and scoped with a @local@-style combinator
-- rather than thread-local mutable state. 'runBaggage' installs the context,
-- 'getBaggage' reads it, and 'localBaggage' \/ 'withBaggageEntry' set entries for
-- a nested scope only. Because the carrier is a handler-local value, baggage
-- propagates into forked work through @effectful@'s environment cloning, the same
-- as the active span (see "Effectful.Tracing.Concurrent").
--
-- The 't:Baggage' value, its pure operations, and this effect are all backend
-- independent. To carry baggage across a network hop, render and parse the
-- @baggage@ header with "Effectful.Tracing.Propagation.Baggage".
--
-- > import Effectful (runEff)
-- > import Effectful.Tracing.Baggage
-- >
-- > example = runEff . runBaggage $ do
-- > withBaggageEntry "tenant.id" "acme" $ do
-- > tenant <- lookupBaggageValue "tenant.id" <$> getBaggage
-- > ... -- tenant == Just "acme" in this scope
module Effectful.Tracing.Baggage
( -- * Baggage values
Baggage
, BaggageEntry (..)
, emptyBaggage
, insertBaggage
, insertBaggageEntry
, deleteBaggage
, lookupBaggage
, lookupBaggageValue
, baggageToList
, baggageFromList
, nullBaggage
, baggageSize
-- * The ambient effect
, BaggageContext
, runBaggage
, runBaggageWith
, getBaggage
, localBaggage
, withBaggageEntry
) where
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Effectful (Dispatch (Dynamic), DispatchOf, Eff, Effect, (:>))
import Effectful.Dispatch.Dynamic (localSeqUnlift, reinterpret, send)
import Effectful.Reader.Static (ask, local, runReader)
-- | One baggage entry: a value plus optional metadata. The metadata is the
-- verbatim @;@-separated property string the W3C format allows after a value
-- (for example @;ttl=30@); most entries have none. It is preserved on a
-- round-trip but otherwise opaque to this library.
data BaggageEntry = BaggageEntry
{ baggageValue :: !Text
-- ^ The entry's value (already percent-decoded when parsed from a header).
, baggageMetadata :: !(Maybe Text)
-- ^ The verbatim property string, if any.
}
deriving (Eq, Show)
-- | An immutable set of baggage entries keyed by name. Construct it with
-- 'emptyBaggage' and the @insert@ \/ @delete@ operations, or parse it from a
-- header with "Effectful.Tracing.Propagation.Baggage".
newtype Baggage = Baggage (Map Text BaggageEntry)
deriving (Eq, Show)
-- | The empty baggage set.
emptyBaggage :: Baggage
emptyBaggage = Baggage Map.empty
-- | Set a key to a bare value (no metadata), replacing any existing entry.
insertBaggage :: Text -> Text -> Baggage -> Baggage
insertBaggage key value = insertBaggageEntry key (BaggageEntry value Nothing)
-- | Set a key to a full 't:BaggageEntry' (value plus metadata), replacing any
-- existing entry.
insertBaggageEntry :: Text -> BaggageEntry -> Baggage -> Baggage
insertBaggageEntry key entry (Baggage m) = Baggage (Map.insert key entry m)
-- | Remove a key. A no-op if it is absent.
deleteBaggage :: Text -> Baggage -> Baggage
deleteBaggage key (Baggage m) = Baggage (Map.delete key m)
-- | Look up an entry (value and metadata) by key.
lookupBaggage :: Text -> Baggage -> Maybe BaggageEntry
lookupBaggage key (Baggage m) = Map.lookup key m
-- | Look up just the value for a key, ignoring any metadata.
lookupBaggageValue :: Text -> Baggage -> Maybe Text
lookupBaggageValue key = fmap baggageValue . lookupBaggage key
-- | All entries, ordered by key.
baggageToList :: Baggage -> [(Text, BaggageEntry)]
baggageToList (Baggage m) = Map.toList m
-- | Build baggage from a list of entries. On a duplicate key the last entry
-- wins.
baggageFromList :: [(Text, BaggageEntry)] -> Baggage
baggageFromList = Baggage . Map.fromList
-- | Whether the baggage set is empty.
nullBaggage :: Baggage -> Bool
nullBaggage (Baggage m) = Map.null m
-- | The number of entries.
baggageSize :: Baggage -> Int
baggageSize (Baggage m) = Map.size m
-- | The ambient-baggage capability. @GetBaggage@ reads the in-scope baggage;
-- @LocalBaggage@ runs a sub-action with the baggage transformed for that scope
-- only. The constructors are private: program against 'getBaggage',
-- 'localBaggage', and 'withBaggageEntry', and discharge the effect with
-- 'runBaggage' \/ 'runBaggageWith'.
data BaggageContext :: Effect where
GetBaggage :: BaggageContext m Baggage
LocalBaggage :: (Baggage -> Baggage) -> m a -> BaggageContext m a
type instance DispatchOf BaggageContext = Dynamic
-- | Discharge the effect starting from 'emptyBaggage'. Use this when a process
-- originates baggage itself.
runBaggage :: Eff (BaggageContext : es) a -> Eff es a
runBaggage = runBaggageWith emptyBaggage
-- | Discharge the effect starting from the given baggage. Use this to seed the
-- context from an inbound request, pairing it with
-- 'Effectful.Tracing.Propagation.Baggage.extractBaggage'.
--
-- The baggage is carried in a private @Reader@ and scoped with @local@, so it is
-- lexical: 'localBaggage' and 'withBaggageEntry' affect only their nested action.
runBaggageWith :: Baggage -> Eff (BaggageContext : es) a -> Eff es a
runBaggageWith initial =
reinterpret (runReader initial) $ \env -> \case
GetBaggage -> ask
LocalBaggage f action ->
localSeqUnlift env $ \unlift -> local f (unlift action)
-- | The baggage currently in scope.
getBaggage :: BaggageContext :> es => Eff es Baggage
getBaggage = send GetBaggage
-- | Run a sub-action with the baggage transformed by the given function. The
-- change is visible only inside the action (and anything it forks); the
-- surrounding scope is unchanged.
--
-- > localBaggage (insertBaggage "request.priority" "high") $ ...
localBaggage :: BaggageContext :> es => (Baggage -> Baggage) -> Eff es a -> Eff es a
localBaggage f action = send (LocalBaggage f action)
-- | Run a sub-action with one extra entry (a bare value) in scope. A
-- convenience for the common @'localBaggage' . 'insertBaggage'@.
--
-- > withBaggageEntry "tenant.id" "acme" $ ...
withBaggageEntry :: BaggageContext :> es => Text -> Text -> Eff es a -> Eff es a
withBaggageEntry key value = localBaggage (insertBaggage key value)