{- |
Module      : OpenTelemetry.Environment
Copyright   : (c) Ian Duncan, 2024-2026
License     : BSD-3
Description : Read standard OTEL_* environment variables for SDK configuration.
Stability   : experimental

Helpers for reading OpenTelemetry environment variables that control exporter
selection, metric export intervals, exemplar filters, and log exporter choice.
Used by the SDK during provider initialization.
-}
module OpenTelemetry.Environment (
  lookupBooleanEnv,
  readEnv,
  readEnvDefault,
  readEnvDefaultWithAlias,
  MetricsExporterSelection (..),
  lookupMetricsExporterSelection,
  lookupMetricExportIntervalMillis,
  lookupMetricExportTimeoutMillis,
  MetricsExemplarFilter (..),
  lookupMetricsExemplarFilter,
  LogsExporterSelection (..),
  lookupLogsExporterSelection,
) where

import qualified Data.Char as C
import Data.List (dropWhileEnd)
import System.Environment (lookupEnv)
import Text.Read (readMaybe)


{- | Does the given value of an environment variable correspond to "true" according
to [the OpenTelemetry specification](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#boolean-value)?
-}
isTrue :: String -> Bool
isTrue :: [Char] -> Bool
isTrue = ([Char]
"true" [Char] -> [Char] -> Bool
forall a. Eq a => a -> a -> Bool
==) ([Char] -> Bool) -> ([Char] -> [Char]) -> [Char] -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> Char) -> [Char] -> [Char]
forall a b. (a -> b) -> [a] -> [b]
map Char -> Char
C.toLower


lookupBooleanEnv :: String -> IO Bool
lookupBooleanEnv :: [Char] -> IO Bool
lookupBooleanEnv = (Maybe [Char] -> Bool) -> IO (Maybe [Char]) -> IO Bool
forall a b. (a -> b) -> IO a -> IO b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Bool -> ([Char] -> Bool) -> Maybe [Char] -> Bool
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Bool
False [Char] -> Bool
isTrue) (IO (Maybe [Char]) -> IO Bool)
-> ([Char] -> IO (Maybe [Char])) -> [Char] -> IO Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Char] -> IO (Maybe [Char])
lookupEnv


-- | Read an environment variable and parse it with 'readMaybe'. Returns 'Nothing' if unset or unparseable.
readEnv :: (Read a) => String -> IO (Maybe a)
readEnv :: forall a. Read a => [Char] -> IO (Maybe a)
readEnv [Char]
k = (Maybe [Char] -> ([Char] -> Maybe a) -> Maybe a
forall a b. Maybe a -> (a -> Maybe b) -> Maybe b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= [Char] -> Maybe a
forall a. Read a => [Char] -> Maybe a
readMaybe) (Maybe [Char] -> Maybe a) -> IO (Maybe [Char]) -> IO (Maybe a)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Char] -> IO (Maybe [Char])
lookupEnv [Char]
k


-- | Like 'readEnv' but falls back to a default value if the variable is unset or unparseable.
readEnvDefault :: (Read a) => String -> a -> IO a
readEnvDefault :: forall a. Read a => [Char] -> a -> IO a
readEnvDefault [Char]
k a
def = a -> (a -> a) -> Maybe a -> a
forall b a. b -> (a -> b) -> Maybe a -> b
maybe a
def a -> a
forall a. a -> a
id (Maybe a -> a) -> IO (Maybe a) -> IO a
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Char] -> IO (Maybe a)
forall a. Read a => [Char] -> IO (Maybe a)
readEnv [Char]
k


{- | Like 'readEnvDefault' but tries a primary key first, then a legacy alias.
Useful when an env var was renamed to maintain backwards compatibility.
-}
readEnvDefaultWithAlias :: (Read a) => String -> String -> a -> IO a
readEnvDefaultWithAlias :: forall a. Read a => [Char] -> [Char] -> a -> IO a
readEnvDefaultWithAlias [Char]
primary [Char]
fallback a
def = do
  mv <- [Char] -> IO (Maybe a)
forall a. Read a => [Char] -> IO (Maybe a)
readEnv [Char]
primary
  case mv of
    Just a
v -> a -> IO a
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure a
v
    Maybe a
Nothing -> [Char] -> a -> IO a
forall a. Read a => [Char] -> a -> IO a
readEnvDefault [Char]
fallback a
def


{- | Parsed value of @OTEL_METRICS_EXPORTER@ (first entry if comma-separated).
See <https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#exporter-selection>.
-}
data MetricsExporterSelection
  = MetricsExporterNone
  | MetricsExporterOtlp
  | MetricsExporterPrometheus
  | MetricsExporterConsole
  | -- | A name not in the built-in set; looked up via the exporter registry.
    MetricsExporterCustom !String
  deriving (MetricsExporterSelection -> MetricsExporterSelection -> Bool
(MetricsExporterSelection -> MetricsExporterSelection -> Bool)
-> (MetricsExporterSelection -> MetricsExporterSelection -> Bool)
-> Eq MetricsExporterSelection
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: MetricsExporterSelection -> MetricsExporterSelection -> Bool
== :: MetricsExporterSelection -> MetricsExporterSelection -> Bool
$c/= :: MetricsExporterSelection -> MetricsExporterSelection -> Bool
/= :: MetricsExporterSelection -> MetricsExporterSelection -> Bool
Eq, Int -> MetricsExporterSelection -> [Char] -> [Char]
[MetricsExporterSelection] -> [Char] -> [Char]
MetricsExporterSelection -> [Char]
(Int -> MetricsExporterSelection -> [Char] -> [Char])
-> (MetricsExporterSelection -> [Char])
-> ([MetricsExporterSelection] -> [Char] -> [Char])
-> Show MetricsExporterSelection
forall a.
(Int -> a -> [Char] -> [Char])
-> (a -> [Char]) -> ([a] -> [Char] -> [Char]) -> Show a
$cshowsPrec :: Int -> MetricsExporterSelection -> [Char] -> [Char]
showsPrec :: Int -> MetricsExporterSelection -> [Char] -> [Char]
$cshow :: MetricsExporterSelection -> [Char]
show :: MetricsExporterSelection -> [Char]
$cshowList :: [MetricsExporterSelection] -> [Char] -> [Char]
showList :: [MetricsExporterSelection] -> [Char] -> [Char]
Show)


trimSpaces :: String -> String
trimSpaces :: [Char] -> [Char]
trimSpaces = (Char -> Bool) -> [Char] -> [Char]
forall a. (a -> Bool) -> [a] -> [a]
dropWhile Char -> Bool
C.isSpace ([Char] -> [Char]) -> ([Char] -> [Char]) -> [Char] -> [Char]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> Bool) -> [Char] -> [Char]
forall a. (a -> Bool) -> [a] -> [a]
dropWhileEnd Char -> Bool
C.isSpace


-- | Read @OTEL_METRICS_EXPORTER@. Unknown or empty values return 'Nothing' (caller may default to OTLP).
lookupMetricsExporterSelection :: IO (Maybe MetricsExporterSelection)
lookupMetricsExporterSelection :: IO (Maybe MetricsExporterSelection)
lookupMetricsExporterSelection = do
  me <- [Char] -> IO (Maybe [Char])
lookupEnv [Char]
"OTEL_METRICS_EXPORTER"
  case me of
    Maybe [Char]
Nothing -> Maybe MetricsExporterSelection
-> IO (Maybe MetricsExporterSelection)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Maybe MetricsExporterSelection
forall a. Maybe a
Nothing
    Just [Char]
raw ->
      let firstSeg :: [Char]
firstSeg = [Char] -> [Char]
trimSpaces ([Char] -> [Char]) -> [Char] -> [Char]
forall a b. (a -> b) -> a -> b
$ case (Char -> Bool) -> [Char] -> ([Char], [Char])
forall a. (a -> Bool) -> [a] -> ([a], [a])
break (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
',') [Char]
raw of
            ([Char]
a, [Char]
_) -> [Char]
a
          key :: [Char]
key = (Char -> Char) -> [Char] -> [Char]
forall a b. (a -> b) -> [a] -> [b]
map Char -> Char
C.toLower ([Char] -> [Char]) -> [Char] -> [Char]
forall a b. (a -> b) -> a -> b
$ [Char] -> [Char]
trimSpaces [Char]
firstSeg
      in Maybe MetricsExporterSelection
-> IO (Maybe MetricsExporterSelection)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Maybe MetricsExporterSelection
 -> IO (Maybe MetricsExporterSelection))
-> Maybe MetricsExporterSelection
-> IO (Maybe MetricsExporterSelection)
forall a b. (a -> b) -> a -> b
$ case [Char]
key of
           [Char]
"" -> Maybe MetricsExporterSelection
forall a. Maybe a
Nothing
           [Char]
"none" -> MetricsExporterSelection -> Maybe MetricsExporterSelection
forall a. a -> Maybe a
Just MetricsExporterSelection
MetricsExporterNone
           [Char]
"otlp" -> MetricsExporterSelection -> Maybe MetricsExporterSelection
forall a. a -> Maybe a
Just MetricsExporterSelection
MetricsExporterOtlp
           [Char]
"prometheus" -> MetricsExporterSelection -> Maybe MetricsExporterSelection
forall a. a -> Maybe a
Just MetricsExporterSelection
MetricsExporterPrometheus
           [Char]
"console" -> MetricsExporterSelection -> Maybe MetricsExporterSelection
forall a. a -> Maybe a
Just MetricsExporterSelection
MetricsExporterConsole
           [Char]
other -> MetricsExporterSelection -> Maybe MetricsExporterSelection
forall a. a -> Maybe a
Just ([Char] -> MetricsExporterSelection
MetricsExporterCustom [Char]
other)


{- | Read @OTEL_METRIC_EXPORT_INTERVAL@ (milliseconds between periodic export cycles).
See <https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/>.
-}
lookupMetricExportIntervalMillis :: IO (Maybe Int)
lookupMetricExportIntervalMillis :: IO (Maybe Int)
lookupMetricExportIntervalMillis = do
  me <- [Char] -> IO (Maybe [Char])
lookupEnv [Char]
"OTEL_METRIC_EXPORT_INTERVAL"
  pure $ case me >>= readMaybe . trimSpaces of
    Just Int
n | Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
0 -> Int -> Maybe Int
forall a. a -> Maybe a
Just Int
n
    Maybe Int
_ -> Maybe Int
forall a. Maybe a
Nothing


{- | Read @OTEL_METRIC_EXPORT_TIMEOUT@ (milliseconds allowed per export call).
See <https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/>.
-}
lookupMetricExportTimeoutMillis :: IO (Maybe Int)
lookupMetricExportTimeoutMillis :: IO (Maybe Int)
lookupMetricExportTimeoutMillis = do
  me <- [Char] -> IO (Maybe [Char])
lookupEnv [Char]
"OTEL_METRIC_EXPORT_TIMEOUT"
  pure $ case me >>= readMaybe . trimSpaces of
    Just Int
n | Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
0 -> Int -> Maybe Int
forall a. a -> Maybe a
Just Int
n
    Maybe Int
_ -> Maybe Int
forall a. Maybe a
Nothing


{- | Parsed value of @OTEL_LOGS_EXPORTER@ (first entry if comma-separated).
See <https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#exporter-selection>.

@since 0.4.0.0
-}
data LogsExporterSelection
  = LogsExporterNone
  | LogsExporterOtlp
  | LogsExporterConsole
  | -- | A name not in the built-in set; looked up via the exporter registry.
    LogsExporterCustom !String
  deriving (LogsExporterSelection -> LogsExporterSelection -> Bool
(LogsExporterSelection -> LogsExporterSelection -> Bool)
-> (LogsExporterSelection -> LogsExporterSelection -> Bool)
-> Eq LogsExporterSelection
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: LogsExporterSelection -> LogsExporterSelection -> Bool
== :: LogsExporterSelection -> LogsExporterSelection -> Bool
$c/= :: LogsExporterSelection -> LogsExporterSelection -> Bool
/= :: LogsExporterSelection -> LogsExporterSelection -> Bool
Eq, Int -> LogsExporterSelection -> [Char] -> [Char]
[LogsExporterSelection] -> [Char] -> [Char]
LogsExporterSelection -> [Char]
(Int -> LogsExporterSelection -> [Char] -> [Char])
-> (LogsExporterSelection -> [Char])
-> ([LogsExporterSelection] -> [Char] -> [Char])
-> Show LogsExporterSelection
forall a.
(Int -> a -> [Char] -> [Char])
-> (a -> [Char]) -> ([a] -> [Char] -> [Char]) -> Show a
$cshowsPrec :: Int -> LogsExporterSelection -> [Char] -> [Char]
showsPrec :: Int -> LogsExporterSelection -> [Char] -> [Char]
$cshow :: LogsExporterSelection -> [Char]
show :: LogsExporterSelection -> [Char]
$cshowList :: [LogsExporterSelection] -> [Char] -> [Char]
showList :: [LogsExporterSelection] -> [Char] -> [Char]
Show)


{- | Read @OTEL_LOGS_EXPORTER@. Empty values return 'Nothing' (caller defaults to OTLP).
Unknown names are returned as 'LogsExporterCustom' for registry lookup.
-}
lookupLogsExporterSelection :: IO (Maybe LogsExporterSelection)
lookupLogsExporterSelection :: IO (Maybe LogsExporterSelection)
lookupLogsExporterSelection = do
  me <- [Char] -> IO (Maybe [Char])
lookupEnv [Char]
"OTEL_LOGS_EXPORTER"
  case me of
    Maybe [Char]
Nothing -> Maybe LogsExporterSelection -> IO (Maybe LogsExporterSelection)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Maybe LogsExporterSelection
forall a. Maybe a
Nothing
    Just [Char]
raw ->
      let firstSeg :: [Char]
firstSeg = [Char] -> [Char]
trimSpaces ([Char] -> [Char]) -> [Char] -> [Char]
forall a b. (a -> b) -> a -> b
$ case (Char -> Bool) -> [Char] -> ([Char], [Char])
forall a. (a -> Bool) -> [a] -> ([a], [a])
break (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
',') [Char]
raw of
            ([Char]
a, [Char]
_) -> [Char]
a
          key :: [Char]
key = (Char -> Char) -> [Char] -> [Char]
forall a b. (a -> b) -> [a] -> [b]
map Char -> Char
C.toLower ([Char] -> [Char]) -> [Char] -> [Char]
forall a b. (a -> b) -> a -> b
$ [Char] -> [Char]
trimSpaces [Char]
firstSeg
      in Maybe LogsExporterSelection -> IO (Maybe LogsExporterSelection)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Maybe LogsExporterSelection -> IO (Maybe LogsExporterSelection))
-> Maybe LogsExporterSelection -> IO (Maybe LogsExporterSelection)
forall a b. (a -> b) -> a -> b
$ case [Char]
key of
           [Char]
"" -> Maybe LogsExporterSelection
forall a. Maybe a
Nothing
           [Char]
"none" -> LogsExporterSelection -> Maybe LogsExporterSelection
forall a. a -> Maybe a
Just LogsExporterSelection
LogsExporterNone
           [Char]
"otlp" -> LogsExporterSelection -> Maybe LogsExporterSelection
forall a. a -> Maybe a
Just LogsExporterSelection
LogsExporterOtlp
           [Char]
"console" -> LogsExporterSelection -> Maybe LogsExporterSelection
forall a. a -> Maybe a
Just LogsExporterSelection
LogsExporterConsole
           [Char]
other -> LogsExporterSelection -> Maybe LogsExporterSelection
forall a. a -> Maybe a
Just ([Char] -> LogsExporterSelection
LogsExporterCustom [Char]
other)


-- | Parsed @OTEL_METRICS_EXEMPLAR_FILTER@ (when present).
data MetricsExemplarFilter
  = MetricsExemplarFilterTraceBased
  | MetricsExemplarFilterAlwaysOn
  | MetricsExemplarFilterAlwaysOff
  deriving (MetricsExemplarFilter -> MetricsExemplarFilter -> Bool
(MetricsExemplarFilter -> MetricsExemplarFilter -> Bool)
-> (MetricsExemplarFilter -> MetricsExemplarFilter -> Bool)
-> Eq MetricsExemplarFilter
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: MetricsExemplarFilter -> MetricsExemplarFilter -> Bool
== :: MetricsExemplarFilter -> MetricsExemplarFilter -> Bool
$c/= :: MetricsExemplarFilter -> MetricsExemplarFilter -> Bool
/= :: MetricsExemplarFilter -> MetricsExemplarFilter -> Bool
Eq, Int -> MetricsExemplarFilter -> [Char] -> [Char]
[MetricsExemplarFilter] -> [Char] -> [Char]
MetricsExemplarFilter -> [Char]
(Int -> MetricsExemplarFilter -> [Char] -> [Char])
-> (MetricsExemplarFilter -> [Char])
-> ([MetricsExemplarFilter] -> [Char] -> [Char])
-> Show MetricsExemplarFilter
forall a.
(Int -> a -> [Char] -> [Char])
-> (a -> [Char]) -> ([a] -> [Char] -> [Char]) -> Show a
$cshowsPrec :: Int -> MetricsExemplarFilter -> [Char] -> [Char]
showsPrec :: Int -> MetricsExemplarFilter -> [Char] -> [Char]
$cshow :: MetricsExemplarFilter -> [Char]
show :: MetricsExemplarFilter -> [Char]
$cshowList :: [MetricsExemplarFilter] -> [Char] -> [Char]
showList :: [MetricsExemplarFilter] -> [Char] -> [Char]
Show)


-- | Read @OTEL_METRICS_EXEMPLAR_FILTER@ (first segment if comma-separated). Unknown values return 'Nothing'.
lookupMetricsExemplarFilter :: IO (Maybe MetricsExemplarFilter)
lookupMetricsExemplarFilter :: IO (Maybe MetricsExemplarFilter)
lookupMetricsExemplarFilter = do
  me <- [Char] -> IO (Maybe [Char])
lookupEnv [Char]
"OTEL_METRICS_EXEMPLAR_FILTER"
  case me of
    Maybe [Char]
Nothing -> Maybe MetricsExemplarFilter -> IO (Maybe MetricsExemplarFilter)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Maybe MetricsExemplarFilter
forall a. Maybe a
Nothing
    Just [Char]
raw ->
      let firstSeg :: [Char]
firstSeg = [Char] -> [Char]
trimSpaces ([Char] -> [Char]) -> [Char] -> [Char]
forall a b. (a -> b) -> a -> b
$ case (Char -> Bool) -> [Char] -> ([Char], [Char])
forall a. (a -> Bool) -> [a] -> ([a], [a])
break (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
',') [Char]
raw of
            ([Char]
a, [Char]
_) -> [Char]
a
          key :: [Char]
key = (Char -> Char) -> [Char] -> [Char]
forall a b. (a -> b) -> [a] -> [b]
map Char -> Char
C.toLower ([Char] -> [Char]) -> [Char] -> [Char]
forall a b. (a -> b) -> a -> b
$ [Char] -> [Char]
trimSpaces [Char]
firstSeg
      in Maybe MetricsExemplarFilter -> IO (Maybe MetricsExemplarFilter)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Maybe MetricsExemplarFilter -> IO (Maybe MetricsExemplarFilter))
-> Maybe MetricsExemplarFilter -> IO (Maybe MetricsExemplarFilter)
forall a b. (a -> b) -> a -> b
$ case [Char]
key of
           [Char]
"" -> Maybe MetricsExemplarFilter
forall a. Maybe a
Nothing
           [Char]
"trace_based" -> MetricsExemplarFilter -> Maybe MetricsExemplarFilter
forall a. a -> Maybe a
Just MetricsExemplarFilter
MetricsExemplarFilterTraceBased
           [Char]
"always_on" -> MetricsExemplarFilter -> Maybe MetricsExemplarFilter
forall a. a -> Maybe a
Just MetricsExemplarFilter
MetricsExemplarFilterAlwaysOn
           [Char]
"always_off" -> MetricsExemplarFilter -> Maybe MetricsExemplarFilter
forall a. a -> Maybe a
Just MetricsExemplarFilter
MetricsExemplarFilterAlwaysOff
           [Char]
_ -> Maybe MetricsExemplarFilter
forall a. Maybe a
Nothing