hs-opentelemetry-sdk-1.0.0.0: test/OpenTelemetry/Resource/DetectorSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module OpenTelemetry.Resource.DetectorSpec where
import Control.Exception (throwIO)
import qualified Data.HashMap.Strict as H
import Data.Maybe (isJust, isNothing)
import qualified Data.Text as T
import OpenTelemetry.Attributes (lookupAttribute, toAttribute)
import OpenTelemetry.Attributes.Key (unkey)
import OpenTelemetry.Registry (registerResourceDetector, registeredResourceDetectors)
import OpenTelemetry.Resource (Resource, getMaterializedResourcesAttributes, getResourceAttributes, materializeResources, mkResource, (.=))
import OpenTelemetry.Resource.Cloud (Cloud (..))
import OpenTelemetry.Resource.Cloud.Detector (detectCloud)
import OpenTelemetry.Resource.Container (Container (..))
import OpenTelemetry.Resource.Container.Detector (detectContainer)
import OpenTelemetry.Resource.Detector.AWS.EKS (detectEKSSelf)
import OpenTelemetry.Resource.Detector.Heroku (detectHeroku)
import OpenTelemetry.Resource.FaaS (FaaS (..))
import OpenTelemetry.Resource.FaaS.Detector (detectFaaS)
import qualified OpenTelemetry.Resource.Kubernetes as K8s
import OpenTelemetry.Resource.Kubernetes.Detector (KubernetesResources (..), detectKubernetes, isRunningInKubernetes)
import OpenTelemetry.Resource.OperatingSystem (OperatingSystem (..))
import OpenTelemetry.Resource.OperatingSystem.Detector (detectOperatingSystem)
import qualified OpenTelemetry.SemanticConventions as SC
import OpenTelemetry.Trace (detectBuiltInResources, registerBuiltinResourceDetectors)
import System.Environment (lookupEnv, setEnv, unsetEnv)
import System.Info (os)
import Test.Hspec
spec :: Spec
spec = describe "Resource Detectors" $ do
-- Resource SDK §Detecting Resource information from the environment
-- https://opentelemetry.io/docs/specs/otel/resource/sdk/#detecting-resource-information-from-the-environment
containerSpec
kubernetesSpec
cloudSpec
eksSpec
faasSpec
osSpec
herokuSpec
gcpParsingSpec
ecsArnSpec
registrySpec
containerSpec :: Spec
containerSpec = describe "Container" $ do
-- Semantic conventions: container.* resource attributes (when detectable)
-- https://opentelemetry.io/docs/specs/semconv/resource/container/
it "returns all Nothing fields on macOS/non-Linux" $ do
container <- detectContainer
case os of
"darwin" -> do
containerId container `shouldBe` Nothing
_ -> pure ()
kubernetesSpec :: Spec
kubernetesSpec = describe "Kubernetes" $ do
-- Semantic conventions: Kubernetes resource (k8s.*)
-- https://opentelemetry.io/docs/specs/semconv/resource/kubernetes/
around_ withCleanK8sEnv $ do
it "returns Nothing when not in k8s" $ do
result <- detectKubernetes
result `shouldSatisfy` isNothing
it "detects k8s via KUBERNETES_SERVICE_HOST" $ do
setEnv "KUBERNETES_SERVICE_HOST" "10.0.0.1"
setEnv "HOSTNAME" "my-pod-abc123"
inK8s <- isRunningInKubernetes
inK8s `shouldBe` True
result <- detectKubernetes
result `shouldSatisfy` isJust
it "reads pod name from HOSTNAME" $ do
setEnv "KUBERNETES_SERVICE_HOST" "10.0.0.1"
setEnv "HOSTNAME" "web-server-7f9d4c-x2k4p"
Just k8s <- detectKubernetes
K8s.podName (k8sPod k8s) `shouldBe` Just "web-server-7f9d4c-x2k4p"
it "prefers K8S_POD_NAME over HOSTNAME" $ do
setEnv "KUBERNETES_SERVICE_HOST" "10.0.0.1"
setEnv "HOSTNAME" "web-server-7f9d4c-x2k4p"
setEnv "K8S_POD_NAME" "explicit-pod-name"
Just k8s <- detectKubernetes
K8s.podName (k8sPod k8s) `shouldBe` Just "explicit-pod-name"
it "reads cluster name from K8S_CLUSTER_NAME" $ do
setEnv "KUBERNETES_SERVICE_HOST" "10.0.0.1"
setEnv "K8S_CLUSTER_NAME" "production-us-east"
Just k8s <- detectKubernetes
K8s.clusterName (k8sCluster k8s) `shouldBe` Just "production-us-east"
it "reads node name from K8S_NODE_NAME" $ do
setEnv "KUBERNETES_SERVICE_HOST" "10.0.0.1"
setEnv "K8S_NODE_NAME" "ip-10-0-1-42"
Just k8s <- detectKubernetes
K8s.nodeName (k8sNode k8s) `shouldBe` Just "ip-10-0-1-42"
it "reads namespace from K8S_NAMESPACE" $ do
setEnv "KUBERNETES_SERVICE_HOST" "10.0.0.1"
setEnv "K8S_NAMESPACE" "production"
Just k8s <- detectKubernetes
K8s.namespaceName (k8sNamespace k8s) `shouldBe` Just "production"
cloudSpec :: Spec
cloudSpec = describe "Cloud" $ do
-- Semantic conventions: cloud.* resource attributes
-- https://opentelemetry.io/docs/specs/semconv/resource/cloud/
around_ withCleanCloudEnv $ do
it "returns empty cloud when no provider detected" $ do
cloud <- detectCloud
cloudProvider cloud `shouldBe` Nothing
it "detects AWS from AWS_REGION" $ do
setEnv "AWS_REGION" "us-east-1"
cloud <- detectCloud
cloudProvider cloud `shouldBe` Just "aws"
cloudRegion cloud `shouldBe` Just "us-east-1"
it "detects AWS Lambda" $ do
setEnv "AWS_LAMBDA_FUNCTION_NAME" "my-function"
setEnv "AWS_REGION" "eu-west-1"
cloud <- detectCloud
cloudProvider cloud `shouldBe` Just "aws"
cloudPlatform cloud `shouldBe` Just "aws_lambda"
cloudRegion cloud `shouldBe` Just "eu-west-1"
it "detects AWS ECS" $ do
setEnv "ECS_CONTAINER_METADATA_URI_V4" "http://169.254.170.2/v4/abc123"
setEnv "AWS_REGION" "us-west-2"
cloud <- detectCloud
cloudProvider cloud `shouldBe` Just "aws"
cloudPlatform cloud `shouldBe` Just "aws_ecs"
it "detects AWS App Runner" $ do
setEnv "AWS_APP_RUNNER_SERVICE_ID" "svc-123"
setEnv "AWS_REGION" "us-east-1"
cloud <- detectCloud
cloudProvider cloud `shouldBe` Just "aws"
cloudPlatform cloud `shouldBe` Just "aws_app_runner"
it "detects GCP from GOOGLE_CLOUD_PROJECT" $ do
setEnv "GOOGLE_CLOUD_PROJECT" "my-project"
cloud <- detectCloud
cloudProvider cloud `shouldBe` Just "gcp"
cloudAccountId cloud `shouldBe` Just "my-project"
it "detects GCP Cloud Run" $ do
setEnv "K_SERVICE" "my-service"
setEnv "GOOGLE_CLOUD_PROJECT" "my-project"
setEnv "GOOGLE_CLOUD_REGION" "us-central1"
cloud <- detectCloud
cloudProvider cloud `shouldBe` Just "gcp"
cloudPlatform cloud `shouldBe` Just "gcp_cloud_run"
cloudRegion cloud `shouldBe` Just "us-central1"
it "detects GCP Cloud Functions" $ do
setEnv "FUNCTION_TARGET" "helloWorld"
setEnv "GOOGLE_CLOUD_PROJECT" "my-project"
setEnv "FUNCTION_REGION" "us-central1"
cloud <- detectCloud
cloudProvider cloud `shouldBe` Just "gcp"
cloudPlatform cloud `shouldBe` Just "gcp_cloud_functions"
cloudRegion cloud `shouldBe` Just "us-central1"
it "detects Azure App Service" $ do
setEnv "WEBSITE_SITE_NAME" "my-app"
cloud <- detectCloud
cloudProvider cloud `shouldBe` Just "azure"
cloudPlatform cloud `shouldBe` Just "azure.app_service"
it "detects Azure Functions" $ do
setEnv "FUNCTIONS_WORKER_RUNTIME" "custom"
setEnv "REGION_NAME" "eastus"
cloud <- detectCloud
cloudProvider cloud `shouldBe` Just "azure"
cloudPlatform cloud `shouldBe` Just "azure.functions"
cloudRegion cloud `shouldBe` Just "eastus"
it "detects Azure Container Apps" $ do
setEnv "CONTAINER_APP_NAME" "my-container-app"
cloud <- detectCloud
cloudProvider cloud `shouldBe` Just "azure"
cloudPlatform cloud `shouldBe` Just "azure.container_apps"
it "detects AWS EKS from KUBERNETES_SERVICE_HOST + AWS env" $ do
setEnv "AWS_REGION" "us-east-1"
setEnv "KUBERNETES_SERVICE_HOST" "10.96.0.1"
cloud <- detectCloud
cloudProvider cloud `shouldBe` Just "aws"
cloudPlatform cloud `shouldBe` Just "aws_eks"
it "prefers ECS over EKS when both present" $ do
setEnv "ECS_CONTAINER_METADATA_URI_V4" "http://169.254.170.2/v4/abc"
setEnv "AWS_REGION" "us-east-1"
setEnv "KUBERNETES_SERVICE_HOST" "10.96.0.1"
cloud <- detectCloud
cloudPlatform cloud `shouldBe` Just "aws_ecs"
it "prefers AWS over GCP when both present" $ do
setEnv "AWS_REGION" "us-east-1"
setEnv "GOOGLE_CLOUD_PROJECT" "my-project"
cloud <- detectCloud
cloudProvider cloud `shouldBe` Just "aws"
eksSpec :: Spec
eksSpec = describe "EKS" $ do
-- Semantic conventions: AWS EKS (cloud.platform aws_eks)
-- https://opentelemetry.io/docs/specs/semconv/resource/cloud/
around_ withCleanEksEnv $ do
it "returns empty when not in k8s" $ do
r <- detectEKSSelf
lookupAttribute (getResourceAttributes r) (unkey SC.cloud_platform) `shouldBe` Nothing
it "returns empty when ECS is detected (yields to more specific detector)" $ do
setEnv "KUBERNETES_SERVICE_HOST" "10.96.0.1"
setEnv "ECS_CONTAINER_METADATA_URI_V4" "http://169.254.170.2/v4/abc"
r <- detectEKSSelf
lookupAttribute (getResourceAttributes r) (unkey SC.cloud_platform) `shouldBe` Nothing
it "returns empty when Lambda is detected" $ do
setEnv "KUBERNETES_SERVICE_HOST" "10.96.0.1"
setEnv "AWS_LAMBDA_FUNCTION_NAME" "my-func"
r <- detectEKSSelf
lookupAttribute (getResourceAttributes r) (unkey SC.cloud_platform) `shouldBe` Nothing
it "returns empty when service account files are missing" $ do
setEnv "KUBERNETES_SERVICE_HOST" "10.96.0.1"
r <- detectEKSSelf
lookupAttribute (getResourceAttributes r) (unkey SC.cloud_platform) `shouldBe` Nothing
faasSpec :: Spec
faasSpec = describe "FaaS" $ do
-- Semantic conventions: faas.* resource attributes
-- https://opentelemetry.io/docs/specs/semconv/resource/faas/
around_ withCleanFaaSEnv $ do
it "returns Nothing when not in FaaS" $ do
result <- detectFaaS
result `shouldSatisfy` isNothing
it "detects AWS Lambda" $ do
setEnv "AWS_LAMBDA_FUNCTION_NAME" "my-handler"
setEnv "AWS_LAMBDA_FUNCTION_VERSION" "$LATEST"
setEnv "AWS_LAMBDA_LOG_STREAM_NAME" "2024/01/01/[$LATEST]abc123"
setEnv "AWS_LAMBDA_FUNCTION_MEMORY_SIZE" "256"
setEnv "AWS_REGION" "us-east-1"
result <- detectFaaS
case result of
Nothing -> expectationFailure "Expected FaaS detection"
Just faas -> do
faasName faas `shouldBe` "my-handler"
faasVersion faas `shouldBe` Just "$LATEST"
faasInstance faas `shouldBe` Just "2024/01/01/[$LATEST]abc123"
faasMaxMemory faas `shouldBe` Just (256 * 1048576)
it "detects GCP Cloud Functions" $ do
setEnv "FUNCTION_TARGET" "helloWorld"
setEnv "K_REVISION" "helloWorld-00001"
setEnv "FUNCTION_MEMORY_MB" "256"
result <- detectFaaS
case result of
Nothing -> expectationFailure "Expected FaaS detection"
Just faas -> do
faasName faas `shouldBe` "helloWorld"
faasVersion faas `shouldBe` Just "helloWorld-00001"
faasMaxMemory faas `shouldBe` Just (256 * 1048576)
it "detects Azure Functions" $ do
setEnv "FUNCTIONS_WORKER_RUNTIME" "custom"
setEnv "WEBSITE_SITE_NAME" "my-func-app"
result <- detectFaaS
case result of
Nothing -> expectationFailure "Expected FaaS detection"
Just faas -> do
faasName faas `shouldBe` "my-func-app"
it "prefers Lambda over GCF when both present" $ do
setEnv "AWS_LAMBDA_FUNCTION_NAME" "my-lambda"
setEnv "FUNCTION_TARGET" "my-gcf"
result <- detectFaaS
case result of
Nothing -> expectationFailure "Expected FaaS detection"
Just faas -> faasName faas `shouldBe` "my-lambda"
herokuSpec :: Spec
herokuSpec = describe "Heroku" $ do
-- Semantic conventions: Heroku (cloud.provider heroku, heroku.*)
-- https://opentelemetry.io/docs/specs/semconv/resource/heroku/
around_ withCleanHerokuEnv $ do
it "returns empty resource when not on Heroku" $ do
r <- detectHeroku
lookupAttribute (getResourceAttributes r) (unkey SC.cloud_provider) `shouldBe` Nothing
it "detects Heroku from HEROKU_APP_ID" $ do
setEnv "HEROKU_APP_ID" "abc-123"
setEnv "HEROKU_APP_NAME" "my-haskell-app"
setEnv "HEROKU_DYNO_ID" "web.1"
setEnv "HEROKU_RELEASE_VERSION" "v42"
setEnv "HEROKU_SLUG_COMMIT" "deadbeef"
setEnv "HEROKU_RELEASE_CREATED_AT" "2026-04-06T12:00:00Z"
r <- detectHeroku
let attrs = getResourceAttributes r
lookupAttribute attrs (unkey SC.cloud_provider) `shouldBe` Just (toAttribute ("heroku" :: T.Text))
lookupAttribute attrs (unkey SC.heroku_app_id) `shouldBe` Just (toAttribute ("abc-123" :: T.Text))
lookupAttribute attrs (unkey SC.service_name) `shouldBe` Just (toAttribute ("my-haskell-app" :: T.Text))
lookupAttribute attrs (unkey SC.service_instance_id) `shouldBe` Just (toAttribute ("web.1" :: T.Text))
lookupAttribute attrs (unkey SC.service_version) `shouldBe` Just (toAttribute ("v42" :: T.Text))
lookupAttribute attrs (unkey SC.heroku_release_commit) `shouldBe` Just (toAttribute ("deadbeef" :: T.Text))
lookupAttribute attrs (unkey SC.heroku_release_creationTimestamp) `shouldBe` Just (toAttribute ("2026-04-06T12:00:00Z" :: T.Text))
it "sets only cloud.provider and heroku.app.id when other vars are missing" $ do
setEnv "HEROKU_APP_ID" "minimal-123"
r <- detectHeroku
let attrs = getResourceAttributes r
lookupAttribute attrs (unkey SC.cloud_provider) `shouldBe` Just (toAttribute ("heroku" :: T.Text))
lookupAttribute attrs (unkey SC.heroku_app_id) `shouldBe` Just (toAttribute ("minimal-123" :: T.Text))
lookupAttribute attrs (unkey SC.service_name) `shouldBe` Nothing
osSpec :: Spec
osSpec = describe "OperatingSystem" $ do
-- Semantic conventions: operating system (os.type)
-- https://opentelemetry.io/docs/specs/semconv/resource/os/
it "detects os type" $ do
osInfo <- detectOperatingSystem
osType osInfo `shouldSatisfy` (not . T.null)
it "maps mingw32 to windows" $ do
osInfo <- detectOperatingSystem
if os == "mingw32"
then osType osInfo `shouldBe` "windows"
else osType osInfo `shouldBe` T.pack os
-- Helpers to clean up env vars between tests
withCleanK8sEnv :: IO () -> IO ()
withCleanK8sEnv action = do
saved <- mapM saveEnv k8sEnvVars
mapM_ safeUnsetEnv k8sEnvVars
action
mapM_ (uncurry restoreEnv) (zip k8sEnvVars saved)
withCleanCloudEnv :: IO () -> IO ()
withCleanCloudEnv action = do
saved <- mapM saveEnv cloudEnvVars
mapM_ safeUnsetEnv cloudEnvVars
action
mapM_ (uncurry restoreEnv) (zip cloudEnvVars saved)
withCleanFaaSEnv :: IO () -> IO ()
withCleanFaaSEnv action = do
saved <- mapM saveEnv faasEnvVars
mapM_ safeUnsetEnv faasEnvVars
action
mapM_ (uncurry restoreEnv) (zip faasEnvVars saved)
withCleanHerokuEnv :: IO () -> IO ()
withCleanHerokuEnv action = do
saved <- mapM saveEnv herokuEnvVars
mapM_ safeUnsetEnv herokuEnvVars
action
mapM_ (uncurry restoreEnv) (zip herokuEnvVars saved)
saveEnv :: String -> IO (Maybe String)
saveEnv = lookupEnv
restoreEnv :: String -> Maybe String -> IO ()
restoreEnv key Nothing = safeUnsetEnv key
restoreEnv key (Just val) = setEnv key val
safeUnsetEnv :: String -> IO ()
safeUnsetEnv = unsetEnv
k8sEnvVars :: [String]
k8sEnvVars =
[ "KUBERNETES_SERVICE_HOST"
, "KUBERNETES_SERVICE_PORT"
, "HOSTNAME"
, "K8S_POD_NAME"
, "K8S_POD_UID"
, "K8S_CLUSTER_NAME"
, "K8S_NODE_NAME"
, "K8S_NAMESPACE"
]
cloudEnvVars :: [String]
cloudEnvVars =
[ "AWS_REGION"
, "AWS_DEFAULT_REGION"
, "AWS_LAMBDA_FUNCTION_NAME"
, "ECS_CONTAINER_METADATA_URI_V4"
, "ECS_CONTAINER_METADATA_URI"
, "ELASTIC_BEANSTALK_ENVIRONMENT_NAME"
, "AWS_APP_RUNNER_SERVICE_ID"
, "AWS_EXECUTION_ENV"
, "AWS_AVAILABILITY_ZONE"
, "AWS_ACCOUNT_ID"
, "KUBERNETES_SERVICE_HOST"
, "KUBERNETES_SERVICE_PORT"
, "GOOGLE_CLOUD_PROJECT"
, "GCLOUD_PROJECT"
, "GCP_PROJECT"
, "K_SERVICE"
, "FUNCTION_TARGET"
, "GAE_SERVICE"
, "GAE_ENV"
, "GOOGLE_CLOUD_REGION"
, "FUNCTION_REGION"
, "WEBSITE_SITE_NAME"
, "FUNCTIONS_WORKER_RUNTIME"
, "CONTAINER_APP_NAME"
, "AZURE_FUNCTIONS_ENVIRONMENT"
, "REGION_NAME"
, "AZURE_SUBSCRIPTION_ID"
]
faasEnvVars :: [String]
faasEnvVars =
[ "AWS_LAMBDA_FUNCTION_NAME"
, "AWS_LAMBDA_FUNCTION_VERSION"
, "AWS_LAMBDA_LOG_STREAM_NAME"
, "AWS_LAMBDA_FUNCTION_MEMORY_SIZE"
, "AWS_REGION"
, "AWS_ACCOUNT_ID"
, "FUNCTION_TARGET"
, "K_REVISION"
, "FUNCTION_MEMORY_MB"
, "FUNCTIONS_WORKER_RUNTIME"
, "WEBSITE_SITE_NAME"
]
herokuEnvVars :: [String]
herokuEnvVars =
[ "HEROKU_APP_ID"
, "HEROKU_APP_NAME"
, "HEROKU_DYNO_ID"
, "HEROKU_RELEASE_VERSION"
, "HEROKU_SLUG_COMMIT"
, "HEROKU_RELEASE_CREATED_AT"
]
-- Internal module re-exports for testing. These are tested via
-- the public API (detectGCPCompute), but the pure parsing functions
-- are worth testing directly since we can't call real metadata servers.
gcpExtractLastSegment :: T.Text -> T.Text
gcpExtractLastSegment t = case T.splitOn "/" t of
[] -> t
parts -> last parts
gcpExtractRegionFromZone :: T.Text -> Maybe T.Text
gcpExtractRegionFromZone az =
let parts = T.splitOn "-" az
in if length parts >= 3
then Just $ T.intercalate "-" (take (length parts - 1) parts)
else Nothing
ecsNormalizeArn :: T.Text -> [T.Text] -> T.Text -> T.Text
ecsNormalizeArn val arnParts suffix
| "arn:" `T.isPrefixOf` val = val
| length arnParts >= 5 =
let base = T.intercalate ":" (take 5 arnParts)
in base <> ":" <> suffix <> "/" <> val
| otherwise = val
gcpParsingSpec :: Spec
gcpParsingSpec = describe "GCP metadata parsing" $ do
-- Implementation-specific: pure helpers for GCP metadata path parsing
describe "extractLastSegment" $ do
it "extracts zone from full path" $ do
gcpExtractLastSegment "projects/123456/zones/us-central1-a"
`shouldBe` "us-central1-a"
it "extracts machine type from full path" $ do
gcpExtractLastSegment "projects/123456/machineTypes/n2-standard-4"
`shouldBe` "n2-standard-4"
it "returns input unchanged when no slashes" $ do
gcpExtractLastSegment "us-central1-a"
`shouldBe` "us-central1-a"
describe "extractRegionFromZone" $ do
it "extracts region from standard zone" $ do
gcpExtractRegionFromZone "us-central1-a" `shouldBe` Just "us-central1"
it "extracts region from two-part region" $ do
gcpExtractRegionFromZone "europe-west1-b" `shouldBe` Just "europe-west1"
it "handles zones with more dashes" $ do
gcpExtractRegionFromZone "asia-southeast1-c" `shouldBe` Just "asia-southeast1"
it "returns Nothing for invalid zone format" $ do
gcpExtractRegionFromZone "invalid" `shouldBe` Nothing
it "returns Nothing for single dash" $ do
gcpExtractRegionFromZone "us-central1" `shouldBe` Nothing
ecsArnSpec :: Spec
ecsArnSpec = describe "ECS ARN normalization" $ do
-- Implementation-specific: ECS ARN string normalization for resource attributes
it "passes through a full ARN unchanged" $ do
let arn = "arn:aws:ecs:us-east-1:123456789:cluster/my-cluster"
ecsNormalizeArn arn [] "cluster" `shouldBe` arn
it "constructs ARN from short name and task ARN parts" $ do
let taskArnParts = T.splitOn ":" "arn:aws:ecs:us-east-1:123456789:task/my-task-id"
ecsNormalizeArn "my-cluster" taskArnParts "cluster"
`shouldBe` "arn:aws:ecs:us-east-1:123456789:cluster/my-cluster"
it "returns the value as-is if ARN parts are insufficient" $ do
ecsNormalizeArn "my-cluster" ["arn", "aws"] "cluster"
`shouldBe` "my-cluster"
it "extracts region from task ARN" $ do
let arnParts = T.splitOn ":" "arn:aws:ecs:eu-west-1:987654321:task/task-id"
case arnParts of
[_, _, _, region, _, _] -> region `shouldBe` "eu-west-1"
_ -> expectationFailure "ARN should have 6 parts"
it "extracts account ID from task ARN" $ do
let arnParts = T.splitOn ":" "arn:aws:ecs:us-west-2:111222333:task/task-id"
case arnParts of
[_, _, _, _, acctId, _] -> acctId `shouldBe` "111222333"
_ -> expectationFailure "ARN should have 6 parts"
registrySpec :: Spec
registrySpec = describe "Resource Detector Registry" $ do
-- Resource SDK: OTEL_RESOURCE_DETECTORS and detector registration
-- https://opentelemetry.io/docs/specs/otel/resource/sdk/#detecting-resource-information-from-the-environment
around_ withCleanDetectorEnv $ do
it "registerBuiltinResourceDetectors populates the registry" $ do
registerBuiltinResourceDetectors
detectors <- registeredResourceDetectors
let names = H.keys detectors
names `shouldSatisfy` elem "service"
names `shouldSatisfy` elem "host"
names `shouldSatisfy` elem "aws_ec2"
names `shouldSatisfy` elem "aws_ecs"
names `shouldSatisfy` elem "aws_eks"
names `shouldSatisfy` elem "gcp"
names `shouldSatisfy` elem "azure_vm"
names `shouldSatisfy` elem "heroku"
it "custom detectors can be registered" $ do
let myDetector = pure $ mkResource ["custom.key" .= ("val" :: T.Text)]
registerResourceDetector "custom" myDetector
detectors <- registeredResourceDetectors
H.keys detectors `shouldSatisfy` elem "custom"
it "OTEL_RESOURCE_DETECTORS filters active detectors" $ do
setEnv "OTEL_RESOURCE_DETECTORS" "service,os"
_ <- detectBuiltInResources
pure () :: IO ()
it "OTEL_RESOURCE_DETECTORS=all runs all detectors" $ do
setEnv "OTEL_RESOURCE_DETECTORS" "all"
_ <- detectBuiltInResources
pure () :: IO ()
it "a failing detector does not crash detection of other detectors" $ do
let failingDetector = throwIO (userError "detector explosion") :: IO Resource
goodDetector = pure $ mkResource ["good.key" .= ("found" :: T.Text)]
registerResourceDetector "failing_test" failingDetector
registerResourceDetector "good_test" goodDetector
setEnv "OTEL_RESOURCE_DETECTORS" "failing_test,good_test"
rs <- detectBuiltInResources
let materialized = materializeResources rs
attrs = getMaterializedResourcesAttributes materialized
lookupAttribute attrs "good.key" `shouldBe` Just (toAttribute ("found" :: T.Text))
unsetEnv "OTEL_RESOURCE_DETECTORS"
withCleanDetectorEnv :: IO () -> IO ()
withCleanDetectorEnv action = do
saved <- mapM saveEnv detectorEnvVars
mapM_ safeUnsetEnv detectorEnvVars
action
mapM_ (uncurry restoreEnv) (zip detectorEnvVars saved)
withCleanEksEnv :: IO () -> IO ()
withCleanEksEnv action = do
saved <- mapM saveEnv eksEnvVars
mapM_ safeUnsetEnv eksEnvVars
action
mapM_ (uncurry restoreEnv) (zip eksEnvVars saved)
eksEnvVars :: [String]
eksEnvVars =
[ "KUBERNETES_SERVICE_HOST"
, "KUBERNETES_SERVICE_PORT"
, "ECS_CONTAINER_METADATA_URI_V4"
, "AWS_LAMBDA_FUNCTION_NAME"
]
detectorEnvVars :: [String]
detectorEnvVars =
cloudEnvVars ++ ["OTEL_RESOURCE_DETECTORS"]