packages feed

effectful-tracing-0.1.0.0: src/Effectful/Tracing/Log.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : Effectful.Tracing.Log
-- Description : Correlate log lines with the active trace and span.
-- Copyright   : (c) The effectful-tracing contributors
-- License     : BSD-3-Clause
-- Stability   : experimental
--
-- The single most useful thing you can do with a trace and a log line is join
-- them: stamp every log record with the trace and span id that was active when
-- it was written, so a log search jumps straight to the trace and a trace view
-- links back to its logs. This module exposes the active span's identifiers for
-- exactly that, using the field names from the OpenTelemetry
-- <https://opentelemetry.io/docs/specs/otel/logs/ logs data model> (@trace_id@,
-- @span_id@, @trace_flags@).
--
-- It is deliberately framework-agnostic, in the same spirit as
-- "Effectful.Tracing.Testing": the accessors return plain 'Text' and
-- @[('Text', 'Text')]@ with no logging-library dependency, so they drop into
-- @co-log@, @katip@, @fast-logger@, @monad-logger@, or a bare @hPutStrLn@ the
-- same way.
--
-- > -- attach trace context to a structured log call
-- > logWithTrace :: (Tracer :> es, Logger :> es) => Text -> Eff es ()
-- > logWithTrace message = do
-- >   fields <- activeCorrelationFields
-- >   emit message fields
--
-- All accessors read the active span through the 'Tracer' effect, so they return
-- the empty / 'Nothing' case cleanly when no span is in scope (a startup log
-- line, say) rather than forcing the caller to special-case it.
module Effectful.Tracing.Log
  ( -- * Correlation context
    Correlation (..)
  , activeCorrelation

    -- * Log fields
  , correlationFields
  , activeCorrelationFields

    -- * Individual identifiers
  , activeTraceId
  , activeSpanId
  ) where

import Data.Text (Text)

import Effectful (Eff, (:>))

import Effectful.Tracing.Effect (Tracer, getActiveSpan)
import Effectful.Tracing.Internal.Ids (spanIdToHex, traceIdToHex)
import Effectful.Tracing.Internal.Types
  ( SpanContext (..)
  , isSampled
  )

-- | The identifiers needed to tie a log record to a trace: the active span's
-- trace id and span id (lower-case hex, the wire form), and whether the trace is
-- sampled. Built from the active 't:SpanContext'.
data Correlation = Correlation
  { correlationTraceId :: !Text
  -- ^ The 32-character hex trace id.
  , correlationSpanId :: !Text
  -- ^ The 16-character hex span id.
  , correlationSampled :: !Bool
  -- ^ Whether the trace is sampled (the @trace_flags@ low bit).
  }
  deriving (Eq, Show)

-- | The 'Correlation' for the active span, or 'Nothing' when no span is in
-- scope.
activeCorrelation :: Tracer :> es => Eff es (Maybe Correlation)
activeCorrelation = fmap toCorrelation <$> getActiveSpan
  where
    toCorrelation context =
      Correlation
        { correlationTraceId = traceIdToHex (spanContextTraceId context)
        , correlationSpanId = spanIdToHex (spanContextSpanId context)
        , correlationSampled = isSampled (spanContextTraceFlags context)
        }

-- | Render a 'Correlation' as the OpenTelemetry log-correlation fields:
-- @trace_id@, @span_id@, and @trace_flags@ (@"01"@ when sampled, @"00"@
-- otherwise). The pairs drop straight into any structured logger's key-value
-- context.
correlationFields :: Correlation -> [(Text, Text)]
correlationFields correlation =
  [ ("trace_id", correlationTraceId correlation)
  , ("span_id", correlationSpanId correlation)
  , ("trace_flags", if correlationSampled correlation then "01" else "00")
  ]

-- | The log-correlation fields for the active span, or @[]@ when no span is in
-- scope, so the result appends to a logger's context unconditionally.
activeCorrelationFields :: Tracer :> es => Eff es [(Text, Text)]
activeCorrelationFields = maybe [] correlationFields <$> activeCorrelation

-- | The active span's hex trace id, or 'Nothing' when no span is in scope.
activeTraceId :: Tracer :> es => Eff es (Maybe Text)
activeTraceId = fmap correlationTraceId <$> activeCorrelation

-- | The active span's hex span id, or 'Nothing' when no span is in scope.
activeSpanId :: Tracer :> es => Eff es (Maybe Text)
activeSpanId = fmap correlationSpanId <$> activeCorrelation