{-# LANGUAGE OverloadedStrings #-}
{- |
Module : OpenTelemetry.Instrumentation.MonadLogger
Copyright : (c) Ian Duncan, 2021-2026
License : BSD-3
Description : Bridge monad-logger to OpenTelemetry Logs
Stability : experimental
Provides a logging callback compatible with @runLoggingT@ that forwards
@monad-logger@ messages to the OpenTelemetry Logs pipeline. Log records
are emitted via the OTel Logs Bridge API, which means:
* __Trace correlation is automatic__: if a log statement executes inside
an 'OpenTelemetry.Trace.Core.inSpan' block, the emitted log record
carries the active trace\/span IDs with no extra code.
* __Severity mapping__: 'LevelDebug' → 'Debug', 'LevelInfo' → 'Info',
'LevelWarn' → 'Warn', 'LevelError' → 'Error', 'LevelOther' → 'Info'
(with the original level name preserved in @severityText@).
* __Source location__: The 'Loc' from Template Haskell logging macros
(e.g. @$logInfo@) is mapped to @code.filepath@, @code.function.name@,
and @code.lineno@ attributes.
= Usage
@
import OpenTelemetry.Log.Core
import OpenTelemetry.Instrumentation.MonadLogger
main :: IO ()
main = do
lp <- getGlobalLoggerProvider
let logger = makeLogger lp (instrumentationLibrary \"my-app\" \"1.0.0\")
runLoggingT myApp (makeOTelLogCallback logger)
@
Or with 'LoggingT' and the SDK's auto-initialized provider:
@
import OpenTelemetry.Trace (withTracerProvider)
import OpenTelemetry.Log (getGlobalLoggerProvider)
import OpenTelemetry.Instrumentation.MonadLogger
main :: IO ()
main = withTracerProvider $ \\_ -> do
lp <- getGlobalLoggerProvider
let logger = makeLogger lp (instrumentationLibrary \"my-app\" \"1.0.0\")
runLoggingT myApp (makeOTelLogCallback logger)
@
@since 0.1.0.0
-}
module OpenTelemetry.Instrumentation.MonadLogger (
makeOTelLogCallback,
monadLoggerSeverity,
) where
import Control.Monad (void)
import Control.Monad.Logger (Loc (..), LogLevel (..), LogSource, LogStr)
import qualified Data.HashMap.Strict as H
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import OpenTelemetry.Attributes.Key (AttributeKey (..), unkey)
import OpenTelemetry.Internal.Common.Types (AnyValue (..), ToValue (..))
import OpenTelemetry.Internal.Log.Types (
LogRecordArguments (..),
SeverityNumber (..),
emptyLogRecordArguments,
)
import OpenTelemetry.Log.Core (Logger, emitLogRecord)
import qualified OpenTelemetry.SemanticConventions as SC
import OpenTelemetry.SemanticsConfig (StabilityOpt (..), codeOption, getSemanticsOptions)
import System.Log.FastLogger (fromLogStr)
{- | Create a logging callback for use with @runLoggingT@ that forwards
all log messages to the OTel Logs pipeline via the given 'Logger'.
The callback signature matches what 'Control.Monad.Logger.runLoggingT'
expects: @Loc -> LogSource -> LogLevel -> LogStr -> IO ()@.
@since 0.1.0.0
-}
makeOTelLogCallback :: Logger -> (Loc -> LogSource -> LogLevel -> LogStr -> IO ())
makeOTelLogCallback logger loc src level msg = do
semOpts <- getSemanticsOptions
let bodyText = TE.decodeUtf8 (fromLogStr msg)
(sevNum, sevText) = monadLoggerSeverity level
attrs = locAttributes (codeOption semOpts) loc <> sourceAttributes src
args =
emptyLogRecordArguments
{ severityText = Just sevText
, severityNumber = Just sevNum
, body = toValue bodyText
, attributes = attrs
}
void $ emitLogRecord logger args
{- | Map a monad-logger 'LogLevel' to an OTel 'SeverityNumber' and
short text name.
@since 0.1.0.0
-}
monadLoggerSeverity :: LogLevel -> (SeverityNumber, Text)
monadLoggerSeverity LevelDebug = (Debug, "DEBUG")
monadLoggerSeverity LevelInfo = (Info, "INFO")
monadLoggerSeverity LevelWarn = (Warn, "WARN")
monadLoggerSeverity LevelError = (Error, "ERROR")
monadLoggerSeverity (LevelOther t) = (Info, t)
-- | @log.source@ – monad-logger log source tag (custom attribute).
logSourceKey :: AttributeKey Text
logSourceKey = AttributeKey "log.source"
locAttributes :: StabilityOpt -> Loc -> H.HashMap Text AnyValue
locAttributes opt loc =
let qualifiedName = T.pack (loc_package loc) <> ":" <> T.pack (loc_module loc)
stableAttrs =
[ (unkey SC.code_function_name, toValue qualifiedName)
, (unkey SC.code_file_path, toValue (T.pack (loc_filename loc)))
, (unkey SC.code_line_number, IntValue (fromIntegral (fst (loc_start loc))))
]
oldAttrs =
[ (unkey SC.code_function, toValue (T.pack (loc_module loc)))
, (unkey SC.code_namespace, toValue qualifiedName)
, (unkey SC.code_filepath, toValue (T.pack (loc_filename loc)))
, (unkey SC.code_lineno, IntValue (fromIntegral (fst (loc_start loc))))
]
in H.fromList $ case opt of
Stable -> stableAttrs
Old -> oldAttrs
StableAndOld -> stableAttrs <> oldAttrs
sourceAttributes :: LogSource -> H.HashMap Text AnyValue
sourceAttributes src
| T.null src = H.empty
| otherwise = H.singleton (unkey logSourceKey) (toValue src)