hs-opentelemetry-sdk-1.0.0.0: src/OpenTelemetry/Resource/Detect.hs
{- FOURMOLU_DISABLE -}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{- |
Module : OpenTelemetry.Resource.Detect
Copyright : (c) Ian Duncan, 2024-2026
License : BSD-3
Description : Shared resource detection orchestration for trace and metrics SDK initialization.
Stability : experimental
Shared resource detection orchestration used by both the trace and
metrics SDK initialization paths. Separated to avoid circular module
dependencies between @OpenTelemetry.Trace@ and @OpenTelemetry.Metric@.
-}
module OpenTelemetry.Resource.Detect (
detectBuiltInResources,
registerBuiltinResourceDetectors,
detectResourceAttributes,
) where
import Control.Exception (SomeException, catch)
import Control.Monad (foldM)
import qualified Data.ByteString.Char8 as B
import qualified Data.HashMap.Strict as H
{- FOURMOLU_DISABLE -}
#if !MIN_VERSION_base(4,20,0)
import Data.List (foldl')
#endif
{- FOURMOLU_ENABLE -}
import Data.Maybe (mapMaybe)
import qualified Data.Set as Set
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import OpenTelemetry.Attributes.Attribute (Attribute, ToAttribute (..))
import OpenTelemetry.Baggage (decodeBaggageHeader)
import qualified OpenTelemetry.Baggage as Baggage
import OpenTelemetry.Internal.Logging (otelLogError, otelLogWarning)
import qualified OpenTelemetry.Registry as Registry
import OpenTelemetry.Resource
import OpenTelemetry.Resource.Cloud ()
import OpenTelemetry.Resource.Cloud.Detector (detectCloud)
import OpenTelemetry.Resource.Container ()
import OpenTelemetry.Resource.Container.Detector (detectContainer)
import OpenTelemetry.Resource.Detector.AWS.EC2 (detectEC2Self)
import OpenTelemetry.Resource.Detector.AWS.ECS (detectECSSelf)
import OpenTelemetry.Resource.Detector.AWS.EKS (detectEKSSelf)
import OpenTelemetry.Resource.Detector.Azure (detectAzureVMSelf)
import OpenTelemetry.Resource.Detector.GCP (detectGCPComputeSelf)
import OpenTelemetry.Resource.Detector.Heroku (detectHeroku)
import OpenTelemetry.Resource.FaaS (FaaS)
import OpenTelemetry.Resource.FaaS.Detector (detectFaaS)
import OpenTelemetry.Resource.Host ()
import OpenTelemetry.Resource.Host.Detector (detectHost)
import OpenTelemetry.Resource.Kubernetes (Cluster, Namespace, Node, Pod)
import OpenTelemetry.Resource.Kubernetes.Detector (KubernetesResources (..), detectKubernetes)
import OpenTelemetry.Resource.OperatingSystem ()
import OpenTelemetry.Resource.OperatingSystem.Detector (detectOperatingSystem)
import OpenTelemetry.Resource.Process ()
import OpenTelemetry.Resource.Process.Detector (detectProcess, detectProcessRuntime)
import OpenTelemetry.Resource.Service ()
import OpenTelemetry.Resource.Service.Detector (detectService)
import OpenTelemetry.Resource.Telemetry ()
import OpenTelemetry.Resource.Telemetry.Detector (detectTelemetry)
import System.Environment (lookupEnv)
{- | Register all built-in resource detectors in the global registry.
Idempotent: uses 'Registry.registerResourceDetectorIfAbsent'.
@since 0.1.0.2
-}
registerBuiltinResourceDetectors :: IO ()
registerBuiltinResourceDetectors = do
_ <- Registry.registerResourceDetectorIfAbsent "service" (toResource <$> detectService)
_ <- Registry.registerResourceDetectorIfAbsent "telemetry" (pure $ toResource detectTelemetry)
_ <- Registry.registerResourceDetectorIfAbsent "process_runtime" (pure $ toResource detectProcessRuntime)
_ <- Registry.registerResourceDetectorIfAbsent "process" (toResource <$> detectProcess)
_ <- Registry.registerResourceDetectorIfAbsent "os" (toResource <$> detectOperatingSystem)
_ <- Registry.registerResourceDetectorIfAbsent "host" (toResource <$> detectHost)
_ <- Registry.registerResourceDetectorIfAbsent "container" (toResource <$> detectContainer)
_ <- Registry.registerResourceDetectorIfAbsent "cloud" (toResource <$> detectCloud)
_ <- Registry.registerResourceDetectorIfAbsent "faas" (mergeOptionalFaaS <$> detectFaaS)
_ <- Registry.registerResourceDetectorIfAbsent "kubernetes" (mergeOptionalK8s <$> detectKubernetes)
_ <- Registry.registerResourceDetectorIfAbsent "aws_ecs" detectECSSelf
_ <- Registry.registerResourceDetectorIfAbsent "aws_ec2" detectEC2Self
_ <- Registry.registerResourceDetectorIfAbsent "aws_eks" detectEKSSelf
_ <- Registry.registerResourceDetectorIfAbsent "gcp" detectGCPComputeSelf
_ <- Registry.registerResourceDetectorIfAbsent "azure_vm" detectAzureVMSelf
_ <- Registry.registerResourceDetectorIfAbsent "heroku" detectHeroku
pure ()
builtinDetectorOrder :: [T.Text]
builtinDetectorOrder =
[ "telemetry"
, "service"
, "process_runtime"
, "process"
, "os"
, "host"
, "container"
, "cloud"
, "faas"
, "kubernetes"
, "aws_ec2"
, "aws_ecs"
, "aws_eks"
, "gcp"
, "azure_vm"
, "heroku"
]
allDetectorsInDefaultOrder :: H.HashMap T.Text (IO Resource) -> [IO Resource]
allDetectorsInDefaultOrder allDetectors =
let orderedBuiltins = mapMaybe (`H.lookup` allDetectors) builtinDetectorOrder
builtinNames = Set.fromList builtinDetectorOrder
extraDetectors =
H.elems $
H.filterWithKey (\k _ -> not (k `Set.member` builtinNames)) allDetectors
in orderedBuiltins ++ extraDetectors
{- | Use all registered resource detectors to populate resource information.
Reads @OTEL_RESOURCE_DETECTORS@ (comma-separated) to control which detectors
run. The special value @all@ (the default) runs every registered detector.
@since 0.0.1.0
-}
detectBuiltInResources :: IO Resource
detectBuiltInResources = do
mFilter <- lookupEnv "OTEL_RESOURCE_DETECTORS"
registerBuiltinResourceDetectors
allDetectors <- Registry.registeredResourceDetectors
activeDetectors <- case mFilter of
Nothing -> pure $ allDetectorsInDefaultOrder allDetectors
Just filterStr ->
let names = fmap T.strip $ T.splitOn "," $ T.pack filterStr
in if names == ["all"]
then pure $ allDetectorsInDefaultOrder allDetectors
else
foldM
( \acc name -> case H.lookup name allDetectors of
Just d -> pure (acc <> [d])
Nothing -> do
otelLogWarning ("Unknown resource detector '" <> T.unpack name <> "' in OTEL_RESOURCE_DETECTORS, ignoring")
pure acc
)
[]
names
resources <- mapM runDetectorSafely activeDetectors
pure $ foldl' mergeResources (mkResource []) resources
where
runDetectorSafely :: IO Resource -> IO Resource
runDetectorSafely detector =
detector `catch` \(ex :: SomeException) -> do
otelLogWarning $ "Resource detector failed, skipping: " <> show ex
pure (mkResource [])
-- | Parse @OTEL_RESOURCE_ATTRIBUTES@ into key-value pairs.
detectResourceAttributes :: IO [(T.Text, Attribute)]
detectResourceAttributes = do
mEnv <- lookupEnv "OTEL_RESOURCE_ATTRIBUTES"
case mEnv of
Nothing -> pure []
Just envVar -> case decodeBaggageHeader $ B.pack envVar of
Left err -> do
otelLogError $ "Failed to parse OTEL_RESOURCE_ATTRIBUTES: " <> err
pure []
Right ok ->
pure $
map (\(k, v) -> (decodeUtf8 $ Baggage.tokenValue k, toAttribute $ Baggage.value v)) $
H.toList $
Baggage.values ok
mergeOptionalFaaS :: Maybe FaaS -> Resource
mergeOptionalFaaS Nothing = mkResource []
mergeOptionalFaaS (Just faas) = toResource faas
mergeOptionalK8s :: Maybe KubernetesResources -> Resource
mergeOptionalK8s Nothing = mkResource []
mergeOptionalK8s (Just KubernetesResources {..}) =
toResource (k8sCluster :: Cluster)
`mergeResources` toResource (k8sNamespace :: Namespace)
`mergeResources` toResource (k8sNode :: Node)
`mergeResources` toResource (k8sPod :: Pod)