{-# LANGUAGE DeriveGeneric #-}
{-|
Module : Instana.SDK.Internal.SpanStack
Description : Keeps the current spans of a thread.
-}
module Instana.SDK.Internal.SpanStack
( SpanStack
, empty
, entry
, isEmpty
, isSuppressed
, mapEntry
, mapTop
, peek
, pop
, popWhenMatches
, push
, pushSuppress
, readTraceId
, readW3cTraceContext
, suppress
) where
import GHC.Generics
import Instana.SDK.Internal.Id (Id)
import Instana.SDK.Internal.Util ((|>))
import Instana.SDK.Internal.W3CTraceContext (W3CTraceContext)
import Instana.SDK.Span.EntrySpan (EntrySpan)
import qualified Instana.SDK.Span.EntrySpan as EntrySpan
import Instana.SDK.Span.ExitSpan (ExitSpan)
import qualified Instana.SDK.Span.ExitSpan as ExitSpan
import Instana.SDK.Span.Span (Span (..), SpanKind (..))
-- Implementation Note
-- ===================
--
-- This implementation currently heavily relies on the assumption that the
-- monitored application does not employ context switches in a thread (like
-- doing actual async IO, for example). Since Haskell's standard vehicle for
-- concurrency (Control.Concurrent#forkIO and friends) uses green threads (and
-- not OS level threads) doing multiple things in one thread at the same time is
-- not very common, in fact, I haven't seen it in the wild yet.
--
-- Under this assumptions there can be at most two current spans per thread, an
-- entry and an exit. A new exit can only be started once the IO action related
-- to the last exit has completed. The same holds for any entry span. Thus, the
-- spans in one thread do not form a tree (as async contexts and their spans do
-- in Node.js for example) but only a stack with maximal depth 2, like this:
-- * no current span, currently not tracing
-- * an active entry span but not exit
-- * a non-active entry and an active exit
{-|The stack of currently open spans in one thread.
-}
data SpanStack =
-- |Indicates that we are currently not processing any request.
None
-- |Indicates that we are currently processing a request that had
-- X-INSTANA-L=0 set and that should not record any spans.
| Suppressed W3CTraceContext
-- |Indicates that we are currently processing an entry.
| EntryOnly EntrySpan
-- |Indicates that currently an exit is in progress.
| EntryAndExit EntrySpan ExitSpan
deriving (Eq, Generic, Show)
{-|Creates an empty span stack.
-}
empty :: SpanStack
empty =
None
{-|Initializes a span stack with one entry span.
-}
entry :: EntrySpan -> SpanStack
entry entrySpan =
empty
|> push (Entry entrySpan)
{-|Creates a span stack with a suppressed marker.
-}
suppress :: W3CTraceContext -> SpanStack
suppress =
Suppressed
{-|Checks if the span stack is empty.
-}
isEmpty :: SpanStack -> Bool
isEmpty t =
t == None
{-|Checks if tracing is currently suppressed.
-}
isSuppressed :: SpanStack -> Bool
isSuppressed stack =
case stack of
Suppressed _ -> True
_ -> False
{-|Pushes a span onto the stack. Invalid calls are ignored (like pushing an
exit onto an empty stack or an entry span onto an already existing entry span.
-}
push :: Span -> SpanStack -> SpanStack
push (Entry entrySpan) None =
EntryOnly entrySpan
-- a new incoming entry can lift the suppression, an exit can't
push (Entry entrySpan) (Suppressed _) =
EntryOnly entrySpan
-- pushing an exit child onto an entry parent is valid
push (Exit exitSpan) (EntryOnly entrySpan) =
EntryAndExit entrySpan exitSpan
-- ignore invalid calls/invalid state
push _ current =
current
{-|Pushes a suppressed marker onto the stack. This is only valid if the span
stack is currently empty, otherwise the span stack is returned unmodified.
When pushing the suppressed marker, the w3c trace context for the request in
progress must still be provided.
-}
pushSuppress :: W3CTraceContext -> SpanStack -> SpanStack
pushSuppress w3cTraceContext None =
Suppressed w3cTraceContext
pushSuppress w3cTraceContext (Suppressed _) =
-- this effectively overwrites/discards the previous W3C trace context
Suppressed w3cTraceContext
-- ignore invalid calls/invalid state
pushSuppress _ current =
current
{-|Pops the top element, returns a tuple of the top element and the remaining
stack after poppint the top element.
-}
pop :: SpanStack -> (SpanStack, Maybe Span)
pop None =
(None, Nothing)
pop (Suppressed _) =
(None, Nothing)
pop (EntryOnly entrySpan) =
(None, Just $ Entry entrySpan)
pop (EntryAndExit entrySpan exitSpan) =
(EntryOnly entrySpan, Just $ Exit exitSpan)
{-|Pops the top element, but only if the top element is of the expected kind.
If so, a tuple of the top element and the remaining stack after popping the top
element is returned. If not, Nothing and an unmodified stack is returned. The
last part of the 3-tuple is an error message that is only provided if there is
a mismatch between the expected span kind and the actual span kind on the top of
the stack.
-}
popWhenMatches :: SpanKind -> SpanStack -> (SpanStack, Maybe Span, Maybe String)
popWhenMatches _ None =
(None, Nothing, Nothing)
popWhenMatches EntryKind (Suppressed _) =
-- This effectively unsuppresses - we started an entry that was suppressed and
-- now we are asked to complete this very entry, so the suppression is lifted
-- and we are back to a pristine state, ready to start the next entry when the
-- next request comes in.
(None, Nothing, Nothing)
popWhenMatches _ (Suppressed w3cTraceContext) =
(Suppressed w3cTraceContext, Nothing, Nothing)
popWhenMatches expectedKind stack =
case (expectedKind, peek stack) of
(EntryKind, Just (Entry _)) ->
(st, sp, Nothing)
where
(st, sp) = pop stack
(ExitKind, Just (Exit _)) ->
(st, sp, Nothing)
where
(st, sp) = pop stack
(_, actualTopElement) ->
( stack
, Nothing
, Just $ "Cannot pop \"" ++ (show expectedKind) ++
" from span stack. Current top element: " ++ show actualTopElement
)
{-|Returns the top element without modifying the stack.
-}
peek :: SpanStack -> Maybe Span
peek None =
Nothing
peek (Suppressed _) =
Nothing
peek (EntryOnly entrySpan) =
Just $ Entry entrySpan
peek (EntryAndExit _ exitSpan) =
Just $ Exit exitSpan
{-|Reads the trace ID from the entry span of the stack, if any.
-}
readTraceId :: SpanStack -> Maybe Id
readTraceId None =
Nothing
readTraceId (Suppressed _) =
Nothing
readTraceId (EntryOnly entrySpan) =
Just $ EntrySpan.traceId entrySpan
readTraceId (EntryAndExit entrySpan _) =
Just $ EntrySpan.traceId entrySpan
{-|Reads the W3C trace context from the current span or suppression marker,
if any.
-}
readW3cTraceContext :: SpanStack -> Maybe W3CTraceContext
readW3cTraceContext None =
Nothing
readW3cTraceContext (Suppressed w3cTraceContext) =
Just w3cTraceContext
readW3cTraceContext (EntryOnly entrySpan) =
EntrySpan.w3cTraceContext entrySpan
readW3cTraceContext (EntryAndExit _ exitSpan) =
Just $ ExitSpan.w3cTraceContext exitSpan
{-|Modifies the top element in place by applying the given function to it. This
is a no op if the span stack is empty.
-}
mapTop :: (Span -> Span) -> SpanStack -> SpanStack
mapTop _ None =
None
mapTop _ (Suppressed w3cTraceContext) =
Suppressed w3cTraceContext
mapTop fn stack =
let
(remainder, Just oldTop) = pop stack
newTop = fn oldTop
in
push newTop remainder
{-|Modifies the entry span in place by applying the given function to it. This
is a no op if the span stack is empty. This function will never modify the exit
span.
-}
mapEntry :: (Span -> Span) -> SpanStack -> SpanStack
mapEntry _ None =
None
mapEntry _ (Suppressed w3cTraceContext) =
Suppressed w3cTraceContext
mapEntry fn (EntryOnly entrySpan) =
mapTop fn (EntryOnly entrySpan)
mapEntry fn (EntryAndExit oldEntrySpan oldExitSpan) =
let
(Entry newEntrySpan) = fn (Entry oldEntrySpan)
in
EntryAndExit newEntrySpan oldExitSpan