packages feed

tracing-0.0.7.0: src/Control/Monad/Trace.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances #-} -- For the MonadReader instance.

-- | This module is useful mostly for tracing backend implementors. If you are only interested in
-- adding tracing to an application, start at "Monitor.Tracing".
module Control.Monad.Trace (
  -- * Tracers
  Tracer, newTracer,
  runTraceT, runTraceT', TraceT,

  -- * Collected data
  -- | Tracers currently expose two pieces of data: completed spans and pending span count. Note
  -- that only sampled spans are eligible: spans which are 'Control.Monad.Trace.Class.neverSampled'
  -- appear in neither.

  -- ** Completed spans
  spanSamples, Sample(..), Tags, Logs,

  -- ** Pending spans
  pendingSpanCount,
) where

import Prelude hiding (span)

import Control.Monad.Trace.Class
import Control.Monad.Trace.Internal

import Control.Applicative ((<|>))
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Reader (ReaderT(ReaderT), ask, asks, local, runReaderT)
import Control.Monad.Reader.Class (MonadReader)
import Control.Monad.Trans.Class (MonadTrans, lift)
import qualified Data.Aeson as JSON
import Data.Foldable (for_)
import Data.List (sortOn)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Time.Clock (NominalDiffTime)
import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime)
import UnliftIO (MonadUnliftIO, withRunInIO)
import UnliftIO.Exception (finally)
import UnliftIO.STM (TChan, TVar, atomically, modifyTVar', newTChanIO, newTVarIO, readTVar, writeTChan, writeTVar)

-- | A collection of span tags.
type Tags = Map Key JSON.Value

-- | A collection of span logs.
type Logs = [(POSIXTime, Key, JSON.Value)]

-- | A sampled span and its associated metadata.
data Sample = Sample
  { sampleSpan :: !Span
  -- ^ The sampled span.
  , sampleTags :: !Tags
  -- ^ Tags collected during this span.
  , sampleLogs :: !Logs
  -- ^ Logs collected during this span, sorted in chronological order.
  , sampleStart :: !POSIXTime
  -- ^ The time the span started at.
  , sampleDuration :: !NominalDiffTime
  -- ^ The span's duration.
  }

-- | A tracer is a producer of spans.
--
-- More specifically, a tracer:
--
-- * runs 'MonadTrace' actions via 'runTraceT',
-- * transparently collects their generated spans,
-- * and outputs them to a channel (available via 'spanSamples').
--
-- These samples can then be consumed independently, decoupling downstream span processing from
-- their production.
data Tracer = Tracer
  { tracerChannel :: TChan Sample
  , tracerPendingCount :: TVar Int
  }

-- | Creates a new 'Tracer'.
newTracer :: MonadIO m => m Tracer
newTracer = liftIO $ Tracer <$> newTChanIO <*> newTVarIO 0

-- | Returns the number of spans currently in flight (started but not yet completed).
pendingSpanCount :: Tracer -> TVar Int
pendingSpanCount = tracerPendingCount

-- | Returns all newly completed spans' samples. The samples become available in the same order they
-- are completed.
spanSamples :: Tracer -> TChan Sample
spanSamples = tracerChannel

data Scope = Scope
  { scopeTracer :: !Tracer
  , scopeSpan :: !(Maybe Span)
  , scopeTags :: !(Maybe (TVar Tags))
  , scopeLogs :: !(Maybe (TVar Logs))
  }

-- | A span generation monad.
newtype TraceT m a = TraceT { traceTReader :: ReaderT (Maybe Scope) m a }
  deriving (Functor, Applicative, Monad, MonadIO, MonadTrans)

instance MonadReader r m => MonadReader r (TraceT m) where
  ask = lift ask
  local f (TraceT (ReaderT g)) = TraceT $ ReaderT $ \r -> local f $ g r

instance MonadUnliftIO m => MonadTrace (TraceT m) where
  trace bldr (TraceT reader) = TraceT $ ask >>= \case
    Nothing -> reader
    Just parentScope -> do
      let
        mbParentSpn = scopeSpan parentScope
        mbParentCtx = spanContext <$> mbParentSpn
        mbTraceID = contextTraceID <$> mbParentCtx
      spanID <- maybe (liftIO randomSpanID) pure $ builderSpanID bldr
      traceID <- maybe (liftIO randomTraceID) pure $ builderTraceID bldr <|> mbTraceID
      sampling <- case builderSamplingPolicy bldr of
        Just policy -> liftIO policy
        Nothing -> pure $ fromMaybe Never (spanSamplingDecision <$> mbParentSpn)
      let
        baggages = fromMaybe Map.empty $ contextBaggages <$> mbParentCtx
        ctx = Context traceID spanID (builderBaggages bldr `Map.union` baggages)
        spn = Span (builderName bldr) ctx (builderReferences bldr) sampling
        tracer = scopeTracer parentScope
      if spanIsSampled spn
        then do
          tagsTV <- newTVarIO $ builderTags bldr
          logsTV <- newTVarIO []
          startTV <- newTVarIO Nothing -- To detect whether an exception happened during span setup.
          let
            scope = Scope tracer (Just spn) (Just tagsTV) (Just logsTV)
            run = do
              start <- liftIO $ getPOSIXTime
              atomically $ do
                writeTVar startTV (Just start)
                modifyTVar' (tracerPendingCount tracer) (+1)
              local (const $ Just scope) reader
            cleanup = do
              end <- liftIO $ getPOSIXTime
              atomically $ readTVar startTV >>= \case
                Nothing -> pure () -- The action was interrupted before the span was pending.
                Just start -> do
                  modifyTVar' (tracerPendingCount tracer) (\n -> n - 1)
                  tags <- readTVar tagsTV
                  logs <- sortOn (\(t, k, _) -> (t, k)) <$> readTVar logsTV
                  writeTChan (tracerChannel tracer) (Sample spn tags logs start (end - start))
          run `finally` cleanup
        else local (const $ Just $ Scope tracer (Just spn) Nothing Nothing) reader

  activeSpan = TraceT $ asks (>>= scopeSpan)

  addSpanEntry key (TagValue val) = TraceT $ do
    mbTV <- asks (>>= scopeTags)
    for_ mbTV $ \tv -> atomically $ modifyTVar' tv $ Map.insert key val
  addSpanEntry key (LogValue val mbTime)  = TraceT $ do
    mbTV <- asks (>>= scopeLogs)
    for_ mbTV $ \tv -> do
      time <- maybe (liftIO getPOSIXTime) pure mbTime
      atomically $ modifyTVar' tv ((time, key, val) :)

instance MonadUnliftIO m => MonadUnliftIO (TraceT m) where
  withRunInIO inner = TraceT $ withRunInIO $ \run -> inner (run . traceTReader)

-- | Trace an action, sampling its generated spans. This method is thread-safe and can be used to
-- trace multiple actions concurrently.
--
-- Unless you are implementing a custom span publication backend, you should not need to call this
-- method explicitly. Instead, prefer to use the backend's functionality directly (e.g.
-- 'Monitor.Tracing.Zipkin.run' for Zipkin). To ease debugging in certain cases,
-- 'Monitor.Tracing.Local.collectSpanSamples' is also available.
--
-- See 'runTraceT'' for a variant which allows discarding spans.
runTraceT :: TraceT m a -> Tracer -> m a
runTraceT actn tracer = runTraceT' actn (Just tracer)

-- | Maybe trace an action. If the tracer is 'Nothing', no spans will be published.
runTraceT' :: TraceT m a -> Maybe Tracer -> m a
runTraceT' (TraceT reader) mbTracer =
  let scope = fmap (\tracer -> Scope tracer Nothing Nothing Nothing) mbTracer
  in runReaderT reader scope