packages feed

hs-opentelemetry-sdk-1.0.0.0: src/OpenTelemetry/Configuration/Create.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

{- | Implements the "Create" operation from the OpenTelemetry Configuration SDK spec.
Interprets the in-memory configuration model and produces SDK components.
See: https://opentelemetry.io/docs/specs/otel/configuration/sdk/#create
-}
module OpenTelemetry.Configuration.Create (
  createFromConfig,
  OTelSignals (..),
  OTelComponents,
) where

import Control.Monad (when)
import qualified Data.CaseInsensitive as CI
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Network.HTTP.Types.Header (Header)
import OpenTelemetry.Attributes (AttributeLimits (..), defaultAttributeLimits)
import OpenTelemetry.Configuration.Types
import qualified OpenTelemetry.Exporter.Handle.LogRecord as HandleLog
import qualified OpenTelemetry.Exporter.Handle.Metric as HandleMetric
import qualified OpenTelemetry.Exporter.Handle.Span as HandleSpan
import OpenTelemetry.Exporter.LogRecord (LogRecordExporter, LogRecordExporterArguments (..), mkLogRecordExporter)
import OpenTelemetry.Exporter.Metric (MetricExporter)
import OpenTelemetry.Exporter.OTLP.LogRecord (otlpLogRecordExporter)
import OpenTelemetry.Exporter.OTLP.Metric (otlpMetricExporter)
import OpenTelemetry.Exporter.OTLP.Span (CompressionFormat (..), OTLPExporterConfig (..), otlpExporter)
import OpenTelemetry.Exporter.Span (SpanExporter (..))
import OpenTelemetry.Internal.Common.Types (ExportResult (..))
import OpenTelemetry.Internal.Logging (otelLogWarning)
import OpenTelemetry.Log.Core (LoggerProvider, LoggerProviderOptions (..), createLoggerProvider, emptyLoggerProviderOptions, shutdownLoggerProvider)
import OpenTelemetry.MeterProvider
import OpenTelemetry.Metric.Core (MeterProvider (..), shutdownMeterProvider)
import OpenTelemetry.MetricReader
import OpenTelemetry.Processor.Batch.LogRecord (batchLogRecordProcessor)
import qualified OpenTelemetry.Processor.Batch.LogRecord as BlogProc
import OpenTelemetry.Processor.Batch.Span (BatchTimeoutConfig (..), batchProcessor, batchTimeoutConfig)
import OpenTelemetry.Processor.LogRecord (LogRecordProcessor)
import qualified OpenTelemetry.Processor.Simple.LogRecord as SlogProc
import OpenTelemetry.Processor.Simple.Span (SimpleProcessorConfig (..), simpleProcessor)
import OpenTelemetry.Processor.Span (SpanProcessor)
import OpenTelemetry.Propagator (TextMapPropagator, setGlobalTextMapPropagator)
import OpenTelemetry.Propagator.B3 (b3MultiTraceContextPropagator, b3TraceContextPropagator)
import OpenTelemetry.Propagator.W3CBaggage
import OpenTelemetry.Propagator.W3CTraceContext
import OpenTelemetry.Resource
import OpenTelemetry.Trace.Core
import OpenTelemetry.Trace.Id.Generator.Default (defaultIdGenerator)
import OpenTelemetry.Trace.Sampler


-- | @since 1.0.0.0
data OTelSignals = OTelSignals
  { otelTracerProvider :: !TracerProvider
  , otelMeterProvider :: !MeterProvider
  , otelLoggerProvider :: !LoggerProvider
  , otelPropagators :: !TextMapPropagator
  , otelShutdown :: !(IO ())
  }


{-# DEPRECATED OTelComponents "Use OTelSignals instead" #-}


type OTelComponents = OTelSignals


{- | Create SDK components from a parsed configuration model.
Returns all three providers and the composite propagator.

@since 0.1.0.0
-}
createFromConfig :: OTelConfiguration -> IO OTelSignals
createFromConfig cfg = do
  let disabled = fromMaybe False (configDisabled cfg)
  if disabled
    then do
      tp <- createTracerProvider [] emptyTracerProviderOptions
      (mp, _env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions
      lp <- createLoggerProvider [] emptyLoggerProviderOptions
      pure
        OTelSignals
          { otelTracerProvider = tp
          , otelMeterProvider = mp
          , otelLoggerProvider = lp
          , otelPropagators = mempty
          , otelShutdown = pure ()
          }
    else do
      let globalAttrLimits = configAttrLimits cfg

      -- Resource
      let res = buildResource cfg

      -- Propagators
      props <- buildPropagators cfg
      setGlobalTextMapPropagator props

      -- TracerProvider
      (spanProcessors, _spanShutdowns) <- buildSpanProcessors cfg
      let tpOpts =
            emptyTracerProviderOptions
              { tracerProviderOptionsIdGenerator = defaultIdGenerator
              , tracerProviderOptionsSampler = buildSampler cfg
              , tracerProviderOptionsAttributeLimits = globalAttrLimits
              , tracerProviderOptionsSpanLimits = buildSpanLimits cfg
              , tracerProviderOptionsPropagators = props
              , tracerProviderOptionsResources = res
              }
      tp <- createTracerProvider spanProcessors tpOpts

      -- MeterProvider
      mp <- buildMeterProvider cfg res

      -- LoggerProvider
      (lp, _logShutdowns) <- buildLoggerProvider cfg globalAttrLimits res

      let shutdown = do
            _ <- shutdownTracerProvider tp Nothing
            _ <- shutdownMeterProvider mp Nothing
            _ <- shutdownLoggerProvider lp Nothing
            pure ()

      pure
        OTelSignals
          { otelTracerProvider = tp
          , otelMeterProvider = mp
          , otelLoggerProvider = lp
          , otelPropagators = props
          , otelShutdown = shutdown
          }


configAttrLimits :: OTelConfiguration -> AttributeLimits
configAttrLimits cfg = case configAttributeLimits cfg of
  Nothing -> defaultAttributeLimits
  Just al ->
    defaultAttributeLimits
      { attributeCountLimit = alAttributeCountLimit al
      , attributeLengthLimit = alAttributeValueLengthLimit al
      }


buildResource :: OTelConfiguration -> MaterializedResources
buildResource cfg = case configResource cfg of
  Nothing -> emptyMaterializedResources
  Just rc ->
    let attrs = maybe [] (map (\(k, v) -> (k, toAttribute v)) . Map.toList) (resourceAttributes rc)
        r = mkResource (map Just attrs)
        schema = fmap T.unpack (resourceSchemaUrl rc)
    in materializeResourcesWithSchema schema r


buildPropagators :: OTelConfiguration -> IO TextMapPropagator
buildPropagators cfg = case configPropagator cfg of
  Nothing -> pure $ w3cTraceContextPropagator <> w3cBaggagePropagator
  Just pc -> case propagatorComposite pc of
    Nothing -> pure $ w3cTraceContextPropagator <> w3cBaggagePropagator
    Just names -> mconcat <$> mapM resolvePropagator names
  where
    resolvePropagator name = case T.toLower name of
      "tracecontext" -> pure w3cTraceContextPropagator
      "baggage" -> pure w3cBaggagePropagator
      "b3" -> pure b3TraceContextPropagator
      "b3multi" -> pure b3MultiTraceContextPropagator
      "none" -> pure mempty
      unknown -> do
        otelLogWarning $ "Unknown propagator " <> show unknown <> ", skipping"
        pure mempty


buildSampler :: OTelConfiguration -> Sampler
buildSampler cfg = case configTracerProvider cfg of
  Nothing -> parentBased (parentBasedOptions alwaysOn)
  Just tp -> case tpSampler tp of
    Nothing -> parentBased (parentBasedOptions alwaysOn)
    Just sc -> resolveSampler sc
  where
    resolveSampler SamplerAlwaysOn = alwaysOn
    resolveSampler SamplerAlwaysOff = alwaysOff
    resolveSampler (SamplerTraceIdRatioBased r) = traceIdRatioBased (ratioValue r)
    resolveSampler (SamplerParentBased pb) =
      parentBased
        ParentBasedOptions
          { rootSampler = maybe alwaysOn resolveSampler (pbRoot pb)
          , remoteParentSampled = maybe alwaysOn resolveSampler (pbRemoteParentSampled pb)
          , remoteParentNotSampled = maybe alwaysOff resolveSampler (pbRemoteParentNotSampled pb)
          , localParentSampled = maybe alwaysOn resolveSampler (pbLocalParentSampled pb)
          , localParentNotSampled = maybe alwaysOff resolveSampler (pbLocalParentNotSampled pb)
          }


buildSpanLimits :: OTelConfiguration -> SpanLimits
buildSpanLimits cfg = case configTracerProvider cfg >>= tpLimits of
  Nothing -> defaultSpanLimits
  Just sl ->
    SpanLimits
      { spanAttributeValueLengthLimit = slAttributeValueLengthLimit sl
      , spanAttributeCountLimit = slAttributeCountLimit sl
      , eventCountLimit = slEventCountLimit sl
      , linkCountLimit = slLinkCountLimit sl
      , eventAttributeCountLimit = slEventAttributeCountLimit sl
      , linkAttributeCountLimit = slLinkAttributeCountLimit sl
      }


buildSpanProcessors :: OTelConfiguration -> IO ([SpanProcessor], [IO ()])
buildSpanProcessors cfg = case configTracerProvider cfg >>= tpProcessors of
  Nothing -> pure ([], [])
  Just procs -> do
    results <- mapM buildOneSpanProcessor procs
    let (sps, shutdowns) = unzip (concatMap (maybe [] pure) results)
    pure (sps, shutdowns)


buildOneSpanProcessor :: SpanProcessorConfig -> IO (Maybe (SpanProcessor, IO ()))
buildOneSpanProcessor (SpanProcessorBatch bsp) = do
  exporter <- buildSpanExporter (bspExporter bsp)
  let conf =
        batchTimeoutConfig
          { maxQueueSize = fromMaybe (maxQueueSize batchTimeoutConfig) (bspMaxQueueSize bsp)
          , scheduledDelayMillis = fromMaybe (scheduledDelayMillis batchTimeoutConfig) (bspScheduleDelay bsp)
          , exportTimeoutMillis = fromMaybe (exportTimeoutMillis batchTimeoutConfig) (bspExportTimeout bsp)
          , maxExportBatchSize = fromMaybe (maxExportBatchSize batchTimeoutConfig) (bspMaxExportBatchSize bsp)
          }
  processor <- batchProcessor conf exporter
  pure $ Just (processor, pure ())
buildOneSpanProcessor (SpanProcessorSimple ssp) = do
  exporter <- buildSpanExporter (sspExporter ssp)
  processor <- simpleProcessor SimpleProcessorConfig {spanExporter = exporter, simpleSpanExportTimeoutMicros = 30_000_000}
  pure $ Just (processor, pure ())
buildOneSpanProcessor SpanProcessorUnknown = do
  otelLogWarning "Unrecognized span processor type in configuration; this processor will be skipped"
  pure Nothing


buildSpanExporter :: SpanExporterConfig -> IO SpanExporter
buildSpanExporter (SpanExporterOtlpHttp otlpCfg) = do
  let envConfig = otlpHttpToExporterConfig otlpCfg
  otlpExporter envConfig
buildSpanExporter (SpanExporterConsole _) = pure $ HandleSpan.stdoutExporter' HandleSpan.defaultFormatter
buildSpanExporter SpanExporterNone =
  pure $
    SpanExporter
      { spanExporterExport = \_ -> pure Success
      , spanExporterShutdown = pure ShutdownSuccess
      , spanExporterForceFlush = pure FlushSuccess
      }


buildMeterProvider :: OTelConfiguration -> MaterializedResources -> IO MeterProvider
buildMeterProvider cfg res = case configMeterProvider cfg >>= mpReaders of
  Nothing -> do
    (mp, _env) <- createMeterProvider res defaultSdkMeterProviderOptions
    pure mp
  Just readers -> do
    case readers of
      [] -> do
        (mp, _env) <- createMeterProvider res defaultSdkMeterProviderOptions
        pure mp
      (MetricReaderPeriodic pmc : rest) -> do
        when (not (null rest)) $
          otelLogWarning "Multiple metric readers configured; only the first is currently supported — additional readers will be ignored"
        case pmrTimeout pmc of
          Just _ -> otelLogWarning "metric_reader.periodic.timeout is configured but not yet supported; the setting will be ignored"
          Nothing -> pure ()
        mExporter <- buildMetricExporter (pmrExporter pmc)
        case mExporter of
          Just ex -> do
            let opts = defaultSdkMeterProviderOptions {metricExporter = Just ex}
            (mp, env) <- createMeterProvider res opts
            let interval = fromMaybe 60000 (pmrInterval pmc)
                readerOpts = PeriodicMetricReaderOptions {periodicIntervalMicros = interval * 1000}
            handle <- forkPeriodicMetricReader env ex readerOpts
            pure
              mp
                { meterProviderShutdown = \mTimeout -> do
                    stopPeriodicMetricReader handle
                    meterProviderShutdown mp mTimeout
                }
          Nothing -> do
            (mp, _env) <- createMeterProvider res defaultSdkMeterProviderOptions
            pure mp


buildMetricExporter :: PushMetricExporterConfig -> IO (Maybe MetricExporter)
buildMetricExporter (PushMetricExporterOtlpHttp otlpCfg) = do
  let envConfig = otlpHttpToExporterConfig otlpCfg
  ex <- otlpMetricExporter envConfig
  pure (Just ex)
buildMetricExporter (PushMetricExporterConsole _) = do
  ex <- HandleMetric.stdoutMetricExporter
  pure (Just ex)
buildMetricExporter PushMetricExporterNone = pure Nothing


buildLoggerProvider :: OTelConfiguration -> AttributeLimits -> MaterializedResources -> IO (LoggerProvider, [IO ()])
buildLoggerProvider cfg attrLimits res = case configLoggerProvider cfg >>= lpProcessors of
  Nothing -> do
    lp <- createLoggerProvider [] (LoggerProviderOptions res attrLimits Nothing)
    pure (lp, [])
  Just procs -> do
    results <- mapM buildOneLogProcessor procs
    let (lps, shutdowns) = unzip (concatMap (maybe [] pure) results)
        lpOpts = LoggerProviderOptions res attrLimits Nothing
    lp <- createLoggerProvider lps lpOpts
    pure (lp, shutdowns)


buildOneLogProcessor :: LogRecordProcessorConfig -> IO (Maybe (LogRecordProcessor, IO ()))
buildOneLogProcessor (LogRecordProcessorBatch blp) = do
  exporter <- buildLogExporter (blpExporter blp)
  let conf =
        BlogProc.BatchLogRecordProcessorConfig
          { BlogProc.batchLogExporter = exporter
          , BlogProc.batchLogMaxQueueSize = fromMaybe 2048 (blpMaxQueueSize blp)
          , BlogProc.batchLogScheduledDelayMillis = fromMaybe 1000 (blpScheduleDelay blp)
          , BlogProc.batchLogExportTimeoutMillis = fromMaybe 30000 (blpExportTimeout blp)
          , BlogProc.batchLogMaxExportBatchSize = fromMaybe 512 (blpMaxExportBatchSize blp)
          }
  processor <- batchLogRecordProcessor conf
  pure $ Just (processor, pure ())
buildOneLogProcessor (LogRecordProcessorSimple slp) = do
  exporter <- buildLogExporter (slpExporter slp)
  let conf = SlogProc.SimpleLogRecordProcessorConfig {SlogProc.simpleLogRecordExporter = exporter, SlogProc.simpleLogRecordExportTimeoutMicros = 30_000_000}
  processor <- SlogProc.simpleLogRecordProcessor conf
  pure $ Just (processor, pure ())
buildOneLogProcessor LogRecordProcessorUnknown = do
  otelLogWarning "Unrecognized log record processor type in configuration; this processor will be skipped"
  pure Nothing


buildLogExporter :: LogRecordExporterConfig -> IO LogRecordExporter
buildLogExporter (LogRecordExporterOtlpHttp otlpCfg) = do
  let envConfig = otlpHttpToExporterConfig otlpCfg
  otlpLogRecordExporter envConfig
buildLogExporter (LogRecordExporterConsole _) = HandleLog.stdoutLogRecordExporter
buildLogExporter LogRecordExporterNone =
  mkLogRecordExporter
    LogRecordExporterArguments
      { logRecordExporterArgumentsExport = \_ -> pure Success
      , logRecordExporterArgumentsForceFlush = pure FlushSuccess
      , logRecordExporterArgumentsShutdown = pure ()
      }


otlpHttpToExporterConfig :: OtlpHttpExporterConfig -> OTLPExporterConfig
otlpHttpToExporterConfig cfg' =
  OTLPExporterConfig
    { otlpEndpoint = fmap T.unpack (otlpCfgEndpoint cfg')
    , otlpTracesEndpoint = fmap T.unpack (otlpSignalEndpoint cfg')
    , otlpMetricsEndpoint = Nothing
    , otlpInsecure = False
    , otlpSpanInsecure = False
    , otlpMetricInsecure = False
    , otlpCertificate = Nothing
    , otlpTracesCertificate = Nothing
    , otlpMetricCertificate = Nothing
    , otlpHeaders = fmap headersFromMap (otlpCfgHeaders cfg')
    , otlpTracesHeaders = Nothing
    , otlpMetricsHeaders = Nothing
    , otlpCompression = otlpCfgCompression cfg' >>= parseCompression
    , otlpTracesCompression = Nothing
    , otlpMetricsCompression = Nothing
    , otlpTimeout = otlpCfgTimeout cfg'
    , otlpTracesTimeout = Nothing
    , otlpMetricsTimeout = Nothing
    , otlpProtocol = Nothing
    , otlpTracesProtocol = Nothing
    , otlpMetricsProtocol = Nothing
    }


headersFromMap :: Map.Map Text Text -> [Header]
headersFromMap = map (\(k, v) -> (CI.mk (T.encodeUtf8 k), T.encodeUtf8 v)) . Map.toList


parseCompression :: Text -> Maybe CompressionFormat
parseCompression t = case T.toLower t of
  "gzip" -> Just GZip
  "none" -> Just None
  _ -> Nothing