otel-effectful-1.0.0: src/Effectful/OpenTelemetry.hs
{-# LANGUAGE Trustworthy #-}
-- |
-- Module : Effectful.OpenTelemetry
-- Copyright : (c) 2026 Institute for Digital Autonomy
-- License : EUPL-1.2
-- Maintainer : IDA
--
-- An implementation of <https://opentelemetry.io/ OpenTelemetry> for the <https://hackage.haskell.org/package/effectful Effectful> ecosystem.
--
-- This library provides 'Tracing', 'Logging', and 'Metrics' effects, which record the
-- OpenTelemetry signals: spans, log records, and instrument measurements.
--
-- Run them in any combination with 'runTracing', 'runLogging' and 'runMetrics', or all together
-- with 'runOpenTelemetry'.
-- These runners read environment variables to determine where telemetry should be sent.
--
-- Lower-level runners allow you to wire up the configuration in some other way.
--
-- The modules in this library are designed for qualified import. See the examples below.
--
-- = Instrumenting an application
--
-- > import Effectful
-- > import Effectful.OpenTelemetry
-- > import Effectful.OpenTelemetry.Logging qualified as Logging
-- > import Effectful.OpenTelemetry.Logging.Severity qualified as Severity
-- > import Effectful.OpenTelemetry.Metrics.Counter qualified as Counter
-- > import Effectful.OpenTelemetry.Protocol.Attributes qualified as Attributes
-- > import Effectful.OpenTelemetry.Protocol.Resource qualified as Resource
-- > import Effectful.OpenTelemetry.Protocol.Scope qualified as Scope
-- > import Effectful.OpenTelemetry.Tracing qualified as Tracing
-- > import Effectful.OpenTelemetry.Tracing.Span.Kind qualified as Span.Kind
-- >
-- > resource :: Resource
-- > resource = Resource{attributes = Attributes.fromList [("service.name", "my-service")]}
-- >
-- > scope :: Scope
-- > scope = Scope{name = "my-service", version = "1.0", attributes = mempty}
-- >
-- > app :: (Tracing :> es, Logging :> es, Metrics :> es) => Eff es ()
-- > app = do
-- > counter <- Counter.new "myCounter" mempty
-- > Tracing.inSpan "increment-counter" Span.Kind.Internal mempty do
-- > Counter.add counter 1
-- > Logging.log Severity.Info "incremented counter" mempty Nothing
-- >
-- > main :: IO ()
-- > main = runEff $ runOpenTelemetry resource scope app
module Effectful.OpenTelemetry
( -- * Effects
Logging
, Metrics
, Tracing
, runOpenTelemetry
, runOpenTelemetry'
, runInMemoryOpenTelemetry
, runOpenTelemetryWith
, runNoOpenTelemetry
, runTracing
, runLogging
, runMetrics
, runTracingWith
, runLoggingWith
, runMetricsWith
-- * Exporting signals
, Exporter
-- * Signals
, LogRecord
, Measurement
, Span
-- * Protocol
, Resource (Resource)
, Scope (Scope)
-- * Exceptions
, SomeOTLPException (..)
)
where
import Control.Monad ((>=>))
import Control.Monad.Extra (ifM)
import Effectful
import Effectful.Concurrent (Concurrent, runConcurrent)
import Effectful.Environment (Environment, runEnvironment)
import Effectful.OpenTelemetry.Exporter (Exporter)
import Effectful.OpenTelemetry.Exporter qualified as Exporter
import Effectful.OpenTelemetry.Logging
import Effectful.OpenTelemetry.Metrics
import Effectful.OpenTelemetry.Protocol
import Effectful.OpenTelemetry.Protocol.Environment qualified as Environment
import Effectful.OpenTelemetry.Protocol.Exception
import Effectful.OpenTelemetry.Tracing hiding (inject)
import Effectful.Retry (Retry, runRetry)
import Effectful.Timeout (Timeout, runTimeout)
import Numeric.Natural (Natural)
import Prelude
-- | Run all OpenTelmetry effects, sending telemetry to an exporter.
-- Reads the configuration from <https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration the standard environment variables>.
-- Delegates to 'runNoOpenTelemetry' if @OTEL_SDK_DISABLED = true@.
runOpenTelemetry
:: (IOE :> es)
=> Resource
-> Scope
-> Eff (Logging ': Metrics ': Tracing ': es) a
-> Eff es a
runOpenTelemetry resource scope =
runConcurrent
. runEnvironment
. runRetry
. runTimeout
. runOpenTelemetry' resource scope
. inject
runOpenTelemetry'
:: ( IOE :> es
, Concurrent :> es
, Environment :> es
, Retry :> es
, Timeout :> es
)
=> Resource
-> Scope
-> Eff (Logging ': Metrics ': Tracing ': es) a
-> Eff es a
runOpenTelemetry' resource scope eff =
ifM
Environment.isSdkDisabled
(runNoOpenTelemetry eff)
( runTracing resource scope
. runMetrics resource scope
. runLogging resource scope
$ eff
)
-- | Run all OpenTelmetry effects, collecting telemetry in-memory
-- rather than sending to a collector.
runInMemoryOpenTelemetry
:: (IOE :> es, Concurrent :> es)
=> Eff (Logging ': Metrics ': Tracing ': es) a
-> Eff es (a, [LogRecord], [Measurement], [Span])
runInMemoryOpenTelemetry =
runInMemoryTracing
. runInMemoryMetrics
. runInMemoryLogging
>=> \(((a, logs), metrics), spans) -> pure (a, logs, metrics, spans)
-- | Run all OpenTelemetry effects with the given 'Exproter's.
-- Passes records and spans to the exporters synchronously as they are produced,
-- without batching or retrying.
runOpenTelemetryWith
:: (IOE :> es)
=> Maybe Natural
-- ^ Background sampling interval in ms.
-- If set, a background thread periodically samples instruments.
-- Otherwise, they are only sampled once at the end.
-> Exporter es LogRecord
-> Exporter es Measurement
-> Exporter es Span
-> Eff (Logging ': Metrics ': Tracing ': es) a
-> Eff es a
runOpenTelemetryWith intervalMs logs metrics spans =
runTracingWith spans
. runMetricsWith intervalMs (Exporter.inject metrics)
. runLoggingWith (Exporter.inject logs)
-- | Run all OpenTelmetry effects as no-op actions.
runNoOpenTelemetry :: (IOE :> es) => Eff (Logging ': Metrics ': Tracing ': es) a -> Eff es a
runNoOpenTelemetry = runNoTracing . runNoMetrics . runNoLogging