instana-haskell-trace-sdk-0.10.0.0: src/Instana/SDK/Internal/W3CTraceContext.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-|
Module : Instana.SDK.Span.W3CTraceContext
Description : Utility module for decoding and encoding the W3C Trace Context
headers.
-}
module Instana.SDK.Internal.W3CTraceContext
( Flags(..)
, InstanaKeyValuePair(..)
, TraceParent(..)
, TraceState(..)
, W3CTraceContext(..)
, createExitContextForSuppressed
, decode
, emptyTraceState
, exitSpanContextFromIds
, inheritFrom
, inheritFromForSuppressed
, initBogusContextForSuppressedRequest
, toHeaders
) where
import qualified Data.Bits as Bits
import qualified Data.ByteString.Char8 as BSC8
import qualified Data.List as List
import qualified Data.Maybe as Maybe
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics
import qualified Network.HTTP.Types as HTTPTypes
import Numeric (readHex)
import Instana.SDK.Internal.Id (Id)
import qualified Instana.SDK.Internal.Id as Id
import Instana.SDK.Internal.Util (leftPad, leftPadAndLimit)
import qualified Instana.SDK.TracingHeaders as TracingHeaders
-- |A representation of the W3C trace context headers traceparent and
-- tracestate.
data W3CTraceContext = W3CTraceContext
{ traceParent :: TraceParent
, traceState :: TraceState
} deriving (Eq, Generic, Show)
-- |A representation of the W3C trace context header traceparent.
data TraceParent = TraceParent
{ version :: Int
, traceId :: Id
, parentId :: Id
, flags :: Flags
} deriving (Eq, Generic, Show)
-- |A representation of the flags part of the W3C trace context header
-- traceparent.
data Flags = Flags
{ sampled :: Bool
, randomTraceId :: Bool
} deriving (Eq, Generic, Show)
-- |A representation of the W3C trace context header tracestate.
data TraceState = TraceState
{ traceStateHead :: Maybe Text
, instanaKeyValuePair :: Maybe InstanaKeyValuePair
, traceStateTail :: Maybe Text
} deriving (Eq, Generic, Show)
-- |A representation of the Instana key-value pair W3C contained in the
-- tracestate header.
data InstanaKeyValuePair = InstanaKeyValuePair
{ instanaTraceId :: Id
, instanaParentId :: Id
} deriving (Eq, Generic, Show)
maxKeyValuePairsTraceState :: Int
maxKeyValuePairsTraceState = 32
-- |Decodes the raw values of traceparent and tracestate to the parsed
-- representation of the W3C trace context. If the traceparent value is invalid,
-- Nothing will be returned.
decode :: String -> Maybe String -> Maybe W3CTraceContext
decode traceparentHeader tracestateHeader =
let
maybeTraceParent = decodeTraceParent traceparentHeader
in
case maybeTraceParent of
Just tp ->
Just $
W3CTraceContext
{ traceParent = tp
, traceState = decodeTraceState tracestateHeader
}
Nothing ->
Nothing
-- |Decodes the raw traceparent value. If the traceparent value is invalid,
-- Nothing will be returned.
decodeTraceParent :: String -> Maybe TraceParent
decodeTraceParent traceParentString =
let
traceParentText = T.pack traceParentString
components = T.splitOn "-" traceParentText
in
if length components < 4
then
Nothing
else
decodeTraceParentComponents components
-- |Decodes the individual traceparent fields. If any of them is invalid,
-- Nothing will be returned.
decodeTraceParentComponents :: [Text] -> Maybe TraceParent
decodeTraceParentComponents components =
let
rawVersion = components !! 0
rawTraceId = components !! 1
rawParentId = components !! 2
rawFlags = components !! 3
in
case ( validVersion rawVersion
, validTraceId rawTraceId
, validParentId rawParentId
, validFlags rawFlags) of
(True, True, True, True) ->
let
tId = Id.fromText $ rawTraceId
pId = Id.fromText $ rawParentId
flagsReadResult = readHex $ T.unpack $ rawFlags
flgs :: Maybe Integer
flgs = Maybe.listToMaybe . map fst $ flagsReadResult
(smpld, rndmTrcId) = case flgs of
Just fl ->
(Bits.testBit fl 0, Bits.testBit fl 1)
Nothing ->
(False, False)
in
Just $ TraceParent
{ version = 0
, traceId = tId
, parentId = pId
, flags = Flags
{ sampled = smpld
, randomTraceId = rndmTrcId
}
}
_ ->
Nothing
-- |Checks if the version field of traceparent is valid.
validVersion :: Text -> Bool
validVersion rawVersion =
T.length rawVersion == 2 &&
onlyLowerCaseHex rawVersion &&
not (T.all (\c -> c == 'f') rawVersion)
-- |Checks if the trace ID field of traceparent is valid.
validTraceId :: Text -> Bool
validTraceId rawTraceId =
T.length rawTraceId == 32 &&
onlyLowerCaseHex rawTraceId &&
not (T.all (\c -> c == '0') rawTraceId)
-- |Checks if the parent ID field of traceparent is valid.
validParentId :: Text -> Bool
validParentId rawParentId =
T.length rawParentId == 16 &&
onlyLowerCaseHex rawParentId &&
not (T.all (\c -> c == '0') rawParentId)
-- |Checks if the flags field of traceparent is valid.
validFlags :: Text -> Bool
validFlags rawFlags =
T.length rawFlags == 2 &&
onlyLowerCaseHex rawFlags
-- |Checks if the given text contains only lower case hex strings (0-9, a-f.
onlyLowerCaseHex :: Text -> Bool
onlyLowerCaseHex t =
T.all (\c -> c `elem` (['0'..'9'] ++ ['a'..'f'])) t
-- |Decodes the raw tracestate value.
decodeTraceState :: Maybe String -> TraceState
decodeTraceState maybeTraceStateString =
case maybeTraceStateString of
Just traceStateString ->
decodeTraceState' traceStateString
Nothing ->
emptyTraceState
-- |Decodes the raw tracestate value.
decodeTraceState' :: String -> TraceState
decodeTraceState' traceStateString =
let
traceStateText = T.strip $ T.pack traceStateString
in
if T.length traceStateText == 0
then
emptyTraceState
else
decodeNonEmptyTraceState traceStateText
-- |Decodes the raw tracestate value.
decodeNonEmptyTraceState :: Text -> TraceState
decodeNonEmptyTraceState traceStateText =
let
keyValuePairs = map T.strip $ T.splitOn "," traceStateText
instanaKvPairIndex =
List.findIndex (\kvPairString ->
let
key = T.strip $ fst $ T.breakOn "=" kvPairString
in
key == "in"
) keyValuePairs
(tsHead, inKvPair, tsTail) =
case instanaKvPairIndex of
Just idx ->
let
-- Use at most 31 non-Instana key-value pairs plus the Instana
-- key-value pair, since 32 key-value pairs is the limit imposed by
-- the W3C trace context spec.
numKvPairsBeforeInstanaKvPair =
min (maxKeyValuePairsTraceState - 1) idx
maxKvPairsAfterInstanaKvPair =
maxKeyValuePairsTraceState - numKvPairsBeforeInstanaKvPair - 1
kvPairsBeforeInstanaKvPair =
take numKvPairsBeforeInstanaKvPair keyValuePairs
allKvPairsAfterInstanaKvPair = drop (idx + 1) keyValuePairs
limitedKvPairsAfterInstanaKvPair =
take maxKvPairsAfterInstanaKvPair allKvPairsAfterInstanaKvPair
tsHd =
if null kvPairsBeforeInstanaKvPair
then Nothing
else Just $ T.intercalate "," kvPairsBeforeInstanaKvPair
tsTl =
if null limitedKvPairsAfterInstanaKvPair
then Nothing
else Just $ T.intercalate "," limitedKvPairsAfterInstanaKvPair
in
( tsHd
, decodeInKeyValuePair $ keyValuePairs !! idx
, tsTl
)
Nothing ->
-- Limit the number of key-value pairs in tracestate to 32 as per
-- W3C trace context spec.
( Just $
T.intercalate "," (take maxKeyValuePairsTraceState keyValuePairs)
, Nothing
, Nothing
)
in
TraceState
{ traceStateHead = tsHead
, instanaKeyValuePair = inKvPair
, traceStateTail = tsTail
}
-- |Decodes the Instana key value pair from the raw tracestate value.
decodeInKeyValuePair :: Text -> Maybe InstanaKeyValuePair
decodeInKeyValuePair inKvPairText =
let
value = T.strip $ T.drop 1 $ snd $ T.breakOn "=" inKvPairText
(tIdRaw, pIdRaw) = T.breakOn ";" value
(tId, pId) =
( T.strip tIdRaw
, T.strip $ T.drop 1 $ pIdRaw
)
in
if (T.length tId >= 16 && T.length pId >= 16) then
Just InstanaKeyValuePair
{ instanaTraceId = Id.fromText $ tId
, instanaParentId = Id.fromText $ pId
}
else
Nothing
-- |Creates an empty trace state value.
emptyTraceState :: TraceState
emptyTraceState =
TraceState
{ traceStateHead = Nothing
, instanaKeyValuePair = Nothing
, traceStateTail = Nothing
}
-- |Tests whether the given trace state represents an empty trace state.
isEmpty :: TraceState -> Bool
isEmpty ts =
ts == TraceState
{ traceStateHead = Nothing
, instanaKeyValuePair = Nothing
, traceStateTail = Nothing
}
-- |Creates a new W3C trace context value for an outgoing HTTP request by
-- inheriting from the given parent context.
inheritFrom :: W3CTraceContext -> Id -> Id -> W3CTraceContext
inheritFrom parentW3cTraceContext exitSpanTraceId exitSpanSpanId =
let
parentTp = traceParent $ parentW3cTraceContext
parentTs :: TraceState
parentTs = traceState $ parentW3cTraceContext
(parentTsHead, parentTsTail) =
( traceStateHead parentTs
, traceStateTail parentTs
)
in
W3CTraceContext
{ traceParent = TraceParent
{ version = 0
, traceId = traceId $ parentTp
, parentId = exitSpanSpanId
, flags = Flags
{ sampled = True
, randomTraceId = randomTraceId $ flags parentTp
}
}
, traceState = TraceState
{ traceStateHead = Nothing
, instanaKeyValuePair = Just InstanaKeyValuePair
{ instanaTraceId = exitSpanTraceId
, instanaParentId = exitSpanSpanId
}
, traceStateTail =
case (parentTsHead, parentTsTail) of
(Nothing, Nothing) ->
Nothing
_ ->
Just $ T.intercalate "," $
Maybe.catMaybes
[ parentTsHead
, parentTsTail
]
}
}
-- |Creates a new W3C trace context value for an outgoing HTTP request when
-- tracing is suppressed.
inheritFromForSuppressed :: W3CTraceContext -> Id -> W3CTraceContext
inheritFromForSuppressed parentW3cTraceContext exitSpanSpanId =
let
parentTp = traceParent $ parentW3cTraceContext
parentTs :: TraceState
parentTs = traceState $ parentW3cTraceContext
parentTsHead = traceStateHead parentTs
parentTsTail = traceStateTail parentTs
in
W3CTraceContext
{ traceParent = TraceParent
{ version = 0
, traceId = traceId $ parentTp
, parentId = exitSpanSpanId
, flags = Flags
{ sampled = False
, randomTraceId = randomTraceId $ flags parentTp
}
}
, traceState = TraceState
{ traceStateHead = parentTsHead
, instanaKeyValuePair = Nothing
, traceStateTail = parentTsTail
}
}
-- |Creates a new W3C trace context value for an outgoing HTTP request from the
-- given trace ID and span ID, without inheriting from an existing context.
exitSpanContextFromIds :: Id -> Id -> W3CTraceContext
exitSpanContextFromIds exitSpanTraceId exitSpanSpanId =
W3CTraceContext
{ traceParent = TraceParent
{ version = 0
, traceId = exitSpanTraceId
, parentId = exitSpanSpanId
, flags = Flags
{ sampled = True
-- There was no incoming W3C trace context, so we can assume that we
-- either created the trace ID ourselves, or another upstream Instana
-- tracer did so. In both cases, the trace ID is random.
, randomTraceId = True
}
}
, traceState = TraceState
{ traceStateHead = Nothing
, instanaKeyValuePair = Just InstanaKeyValuePair
{ instanaTraceId = exitSpanTraceId
, instanaParentId = exitSpanSpanId
}
, traceStateTail = Nothing
}
}
initBogusContextForSuppressedRequest :: IO W3CTraceContext
initBogusContextForSuppressedRequest = do
bogusId <- Id.generate
return $ createExitContextForSuppressed bogusId bogusId
-- |When tracing is suppressed but no W3C trace context is incoming , we still
-- need to send down W3C trace context headers to signal sampled=false to
-- downstream services.
createExitContextForSuppressed :: Id -> Id -> W3CTraceContext
createExitContextForSuppressed bogusTraceId bogusParentId =
W3CTraceContext
{ traceParent = TraceParent
{ version = 0
, traceId = bogusTraceId
, parentId = bogusParentId
, flags = Flags
{ sampled = False
-- In this case, we have created the trace ID and we create them randomly.
, randomTraceId = True
}
}
, traceState = TraceState
{ traceStateHead = Nothing
, instanaKeyValuePair = Nothing
, traceStateTail = Nothing
}
}
-- |Serializes the given W3C trace context to a pair of HTTP headers.
toHeaders :: W3CTraceContext -> [HTTPTypes.Header]
toHeaders w3cTraceContext =
let
tp = traceParent w3cTraceContext
traceparentHeader =
Just
( TracingHeaders.traceparentHeaderName
, encodeTraceParent tp
)
ts = traceState w3cTraceContext
tracestateHeader =
if isEmpty ts then
Nothing
else
Just
( TracingHeaders.tracestateHeaderName
, encodeTraceState ts
)
in
Maybe.catMaybes [traceparentHeader, tracestateHeader]
-- |Encodes the traceparent header value.
encodeTraceParent :: TraceParent -> BSC8.ByteString
encodeTraceParent tp =
BSC8.concat
[ BSC8.pack $ leftPad 2 $ show $ version tp
, "-"
, BSC8.pack $ leftPadAndLimit 32 $ Id.toStringUnshortened $ traceId tp
, "-"
, BSC8.pack $ leftPadAndLimit 16 $ Id.toString $ parentId tp
, "-"
, encodeFlags $ flags tp
]
-- |Encodes the traceparent flag field.
encodeFlags :: Flags -> BSC8.ByteString
encodeFlags fl =
if sampled fl && randomTraceId fl
then "03"
else if sampled fl
then "01"
else if randomTraceId fl
then "02"
else
"00"
-- |Encodes the tracestate header value.
encodeTraceState :: TraceState -> BSC8.ByteString
encodeTraceState ts =
BSC8.intercalate "," $
Maybe.catMaybes $
-- We only encode for outgoing headers and for those we always move the
-- Instana key value pair to the front in compliance with the W3C
-- trace context specification
([ encodeInstanaKeyValuePair ts
, (BSC8.pack . T.unpack) <$> traceStateHead ts
, (BSC8.pack . T.unpack) <$> traceStateTail ts
])
-- |Encodes the Instana key-value pair for the tracestate header value.
encodeInstanaKeyValuePair :: TraceState -> Maybe BSC8.ByteString
encodeInstanaKeyValuePair ts =
let
inKvPair = instanaKeyValuePair ts
inTId = instanaTraceId <$> inKvPair
inPId = instanaParentId <$> inKvPair
in
case (inTId, inPId) of
(Just t, Just p) ->
Just $
BSC8.concat
[ "in="
, BSC8.pack $ leftPad 16 $ Id.toString t
, ";"
, BSC8.pack $ leftPad 16 $ Id.toString p
]
_ ->
Nothing