packages feed

hs-opentelemetry-sdk-1.0.0.0: src/OpenTelemetry/Metric.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

{- |
Module      :  OpenTelemetry.Metric
Copyright   :  (c) Ian Duncan, 2026
License     :  BSD-3
Description :  OpenTelemetry Metrics SDK — batteries-included setup
Stability   :  experimental

= Overview

This is the main entry point for adding metrics to your Haskell application.
It re-exports the Metrics API types from "OpenTelemetry.Metric.Core" and
adds SDK-level initialization, periodic export, and views.

= Quick example

@
{\-# LANGUAGE OverloadedStrings #-\}
module Main where

import OpenTelemetry.Metric

main :: IO ()
main = withMeterProvider $ \\mp -> do
  meter <- getMeter mp "my-service"
  counter <- meterCreateCounterInt64 meter "http.requests" "" Nothing defaultAdvisoryParameters
  counterAdd counter 1 emptyAttributes
@

'withMeterProvider' reads configuration from @OTEL_*@ environment variables,
starts a periodic export thread, and ensures shutdown on exit. If you also
use 'OpenTelemetry.Trace.withTracerProvider', it already sets up a
'MeterProvider' for you via YAML config — use this module when you need
metrics without traces, or want explicit control.

= Configuration

| Variable | Default | Description |
|---|---|---|
| @OTEL_SDK_DISABLED@ | @false@ | Disable all telemetry |
| @OTEL_METRICS_EXPORTER@ | @otlp@ | @otlp@, @console@, @prometheus@, @none@ |
| @OTEL_METRIC_EXPORT_INTERVAL@ | @60000@ | Export interval in ms |
| @OTEL_METRIC_EXPORT_TIMEOUT@ | @30000@ | Export timeout in ms |
| @OTEL_METRICS_EXEMPLAR_FILTER@ | @trace_based@ | Exemplar filter: @always_on@, @always_off@, @trace_based@ |

= Spec reference

<https://opentelemetry.io/docs/specs/otel/metrics/sdk/>
-}
module OpenTelemetry.Metric (
  -- * Provider lifecycle
  withMeterProvider,
  initializeGlobalMeterProvider,

  -- * Metric API types (re-exported from "OpenTelemetry.Metric.Core")

  -- ** Provider
  MeterProvider (..),
  getMeter,
  noopMeterProvider,
  noopMeter,
  shutdownMeterProvider,
  forceFlushMeterProvider,
  getGlobalMeterProvider,
  setGlobalMeterProvider,

  -- ** Meter
  Meter (..),

  -- ** Instruments
  InstrumentKind (..),
  HistogramAggregation (..),
  AdvisoryParameters (..),
  defaultAdvisoryParameters,
  Counter (..),
  UpDownCounter (..),
  Histogram (..),
  Gauge (..),
  ObservableResult (..),
  ObservableCallbackHandle (..),
  ObservableCounter (..),
  ObservableUpDownCounter (..),
  ObservableGauge (..),

  -- * SDK configuration

  -- ** Provider options
  SdkMeterProviderOptions (..),
  defaultSdkMeterProviderOptions,
  SdkMeterExemplarOptions (..),
  defaultSdkMeterExemplarOptions,
  createMeterProvider,
  SdkMeterEnv (..),

  -- ** Metric readers
  MetricReader (..),
  cumulativeTemporality,
  deltaTemporality,
  PeriodicMetricReaderOptions (..),
  defaultPeriodicMetricReaderOptions,
  periodicMetricReaderOptionsFromEnv,
  forkPeriodicMetricReader,
  exportMetricsOnce,
  PeriodicMetricReaderHandle (..),

  -- ** Views
  View (..),
  ViewSelector (..),
  ViewAggregation (..),
  MetricsExemplarFilter (..),

  -- ** Exporter selection
  resolveMetricExporter,

  -- * Common types
  InstrumentationLibrary (..),
  ShutdownResult (..),
  FlushResult (..),
  Attributes,
  emptyAttributes,
  ToAttribute (..),
  Attribute (..),
) where

import Control.Exception (bracket)
import Control.Monad (void)
import Data.IORef (newIORef)
import qualified Data.Sequence as Seq
import OpenTelemetry.Attributes (Attributes, emptyAttributes)
import OpenTelemetry.Attributes.Attribute (Attribute (..), ToAttribute (..))
import OpenTelemetry.Environment (MetricsExemplarFilter (..), lookupBooleanEnv)
import OpenTelemetry.Exporter.Metric (AggregationTemporality (..))
import OpenTelemetry.Internal.Common.Types (FlushResult (..), InstrumentationLibrary (..), ShutdownResult (..))
import OpenTelemetry.Internal.Logging (otelLogDebug)
import OpenTelemetry.MeterProvider (
  MetricReader (..),
  SdkMeterEnv (..),
  SdkMeterExemplarOptions (..),
  SdkMeterProviderOptions (..),
  createMeterProvider,
  cumulativeTemporality,
  defaultSdkMeterExemplarOptions,
  defaultSdkMeterProviderOptions,
  deltaTemporality,
  emptyStorageState,
 )
import OpenTelemetry.Metric.Core
import OpenTelemetry.Metric.ExporterSelection (resolveMetricExporter)
import OpenTelemetry.Metric.View (
  View (..),
  ViewAggregation (..),
  ViewSelector (..),
 )
import OpenTelemetry.MetricReader (
  PeriodicMetricReaderHandle (..),
  PeriodicMetricReaderOptions (..),
  defaultPeriodicMetricReaderOptions,
  exportMetricsOnce,
  forkPeriodicMetricReader,
  periodicMetricReaderOptionsFromEnv,
 )
import OpenTelemetry.Resource (emptyMaterializedResources, materializeResources, mergeResources, mkResource)
import OpenTelemetry.Resource.Detect (detectBuiltInResources, detectResourceAttributes)


{- | Build an inert 'SdkMeterEnv' for the disabled-SDK case. All mutable
fields are properly allocated (not bottom) so shutdown and flush never crash.
-}
noopSdkMeterEnv :: IO SdkMeterEnv
noopSdkMeterEnv = do
  storageRef <- newIORef emptyStorageState
  cbRef <- newIORef Seq.empty
  sdRef <- newIORef True
  pure
    SdkMeterEnv
      { sdkMeterStorage = storageRef
      , sdkMeterCollectCallbacks = cbRef
      , sdkMeterResource = emptyMaterializedResources
      , sdkMeterShutdown = sdRef
      , sdkMeterCardinalityLimit = 0
      , sdkMeterAggregationTemporality = AggregationCumulative
      , sdkMeterViews = []
      , sdkMeterExemplarOptions = defaultSdkMeterExemplarOptions
      , sdkMeterStartTimeNanos = 0
      }


{- | Initialize a 'MeterProvider' from environment variables, set it as the
global meter provider, and run an action. The provider is shut down on exit
(including exceptions).

@
main :: IO ()
main = withMeterProvider $ \\mp -> do
  meter <- getMeter mp "my-service"
  counter <- meterCreateCounterInt64 meter "requests" "" Nothing defaultAdvisoryParameters
  counterAdd counter 1 emptyAttributes
  -- ... application code ...
@

@since 0.0.1.0
-}
withMeterProvider :: (MeterProvider -> IO a) -> IO a
withMeterProvider body =
  bracket
    initializeGlobalMeterProvider
    (\mp -> void $ shutdownMeterProvider mp Nothing)
    body


{- | Create a 'MeterProvider' from @OTEL_*@ environment variables and install
it as the global provider via 'setGlobalMeterProvider'.

The returned 'MeterProvider' has the periodic reader lifecycle baked into
its shutdown — call 'shutdownMeterProvider' when the application exits.

Reads:

* @OTEL_SDK_DISABLED@ — if @true@, returns the no-op provider
* @OTEL_METRICS_EXPORTER@ — selects the exporter (default: @otlp@)
* @OTEL_METRIC_EXPORT_INTERVAL@, @OTEL_METRIC_EXPORT_TIMEOUT@ — periodic reader timing
* @OTEL_METRICS_EXEMPLAR_FILTER@ — exemplar filter strategy (default: @trace_based@)

@since 0.0.1.0
-}
initializeGlobalMeterProvider :: IO MeterProvider
initializeGlobalMeterProvider = do
  disabled <- lookupBooleanEnv "OTEL_SDK_DISABLED"
  if disabled
    then do
      otelLogDebug "OTEL_SDK_DISABLED=true, using no-op MeterProvider"
      setGlobalMeterProvider noopMeterProvider
      pure noopMeterProvider
    else do
      exporter <- resolveMetricExporter
      readerOpts <- periodicMetricReaderOptionsFromEnv
      builtInRs <- detectBuiltInResources
      envVarRs <- mkResource . map Just <$> detectResourceAttributes
      let rs = materializeResources (mergeResources envVarRs builtInRs)
      (provider, env) <- createMeterProvider rs defaultSdkMeterProviderOptions
      readerHandle <- forkPeriodicMetricReader env exporter readerOpts
      let provider' =
            provider
              { meterProviderShutdown = \timeout -> do
                  stopPeriodicMetricReader readerHandle
                  meterProviderShutdown provider timeout
              }
      setGlobalMeterProvider provider'
      otelLogDebug "MeterProvider initialized from environment"
      pure provider'