packages feed

hs-opentelemetry-sdk-1.0.0.0: src/OpenTelemetry/Resource/Cloud/Detector.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
Module      :  OpenTelemetry.Resource.Cloud.Detector
Copyright   :  (c) Ian Duncan, 2024
License     :  BSD-3
Description :  Auto-detect cloud provider resource attributes from environment variables
Maintainer  :  Ian Duncan
Stability   :  experimental
Portability :  non-portable (GHC extensions)

Detects cloud provider, region, and platform from well-known environment
variables set by AWS, GCP, and Azure runtimes. This covers the common case
where services run on managed compute (ECS, Lambda, Cloud Run, App Service,
etc.) and the runtime injects env vars.

For full instance metadata (account ID, instance ID, availability zone),
use IMDS-based detection. That requires an HTTP client and is left to
a future @hs-opentelemetry-resource-detector-aws@ (or similar) package.

@since 0.1.0.2
-}
module OpenTelemetry.Resource.Cloud.Detector (
  detectCloud,
) where

import Data.Maybe (fromMaybe)
import OpenTelemetry.Resource.Cloud (Cloud (..))
import OpenTelemetry.Resource.Detector.Internal (firstEnv, lookupEnvText)


-- | @since 0.0.1.0
detectCloud :: IO Cloud
detectCloud =
  fmap (fromMaybe emptyCloud) $
    detectAWS `orElse` detectGCP `orElse` detectAzure
  where
    orElse a b =
      a >>= \case
        Just r -> pure (Just r)
        Nothing -> b


emptyCloud :: Cloud
emptyCloud =
  Cloud
    { cloudProvider = Nothing
    , cloudAccountId = Nothing
    , cloudRegion = Nothing
    , cloudAvailabilityZone = Nothing
    , cloudPlatform = Nothing
    , cloudResourceId = Nothing
    }


-- AWS detection via environment variables injected by ECS, Lambda, Beanstalk, etc.
-- When KUBERNETES_SERVICE_HOST is present and no more specific platform matches,
-- falls back to aws_eks as a heuristic.
detectAWS :: IO (Maybe Cloud)
detectAWS = do
  mRegion <- firstEnv ["AWS_REGION", "AWS_DEFAULT_REGION"]
  mLambda <- lookupEnvText "AWS_LAMBDA_FUNCTION_NAME"
  mEcs <- lookupEnvText "ECS_CONTAINER_METADATA_URI_V4"
  mEcsLegacy <- lookupEnvText "ECS_CONTAINER_METADATA_URI"
  mBeanstalk <- lookupEnvText "ELASTIC_BEANSTALK_ENVIRONMENT_NAME"
  mAppRunner <- lookupEnvText "AWS_APP_RUNNER_SERVICE_ID"
  mExecEnv <- lookupEnvText "AWS_EXECUTION_ENV"

  let isAws =
        any
          (/= Nothing)
          [mRegion, mLambda, mEcs, mEcsLegacy, mBeanstalk, mAppRunner, mExecEnv]

  if not isAws
    then pure Nothing
    else do
      mAz <- lookupEnvText "AWS_AVAILABILITY_ZONE"
      mAcct <- lookupEnvText "AWS_ACCOUNT_ID"
      mK8sHost <- lookupEnvText "KUBERNETES_SERVICE_HOST"
      let platform = case () of
            _
              | mLambda /= Nothing -> Just "aws_lambda"
              | mEcs /= Nothing || mEcsLegacy /= Nothing -> Just "aws_ecs"
              | mBeanstalk /= Nothing -> Just "aws_elastic_beanstalk"
              | mAppRunner /= Nothing -> Just "aws_app_runner"
              | mExecEnv == Just "AWS_ECS_EC2" -> Just "aws_ecs"
              | mExecEnv == Just "AWS_ECS_FARGATE" -> Just "aws_ecs"
              | mK8sHost /= Nothing -> Just "aws_eks"
              | otherwise -> Nothing
      pure $
        Just
          Cloud
            { cloudProvider = Just "aws"
            , cloudAccountId = mAcct
            , cloudRegion = mRegion
            , cloudAvailabilityZone = mAz
            , cloudPlatform = platform
            , cloudResourceId = Nothing
            }


-- GCP detection via environment variables set by Cloud Run, Cloud Functions,
-- App Engine, and Compute Engine.
detectGCP :: IO (Maybe Cloud)
detectGCP = do
  mProject <- firstEnv ["GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT", "GCP_PROJECT"]
  mCloudRun <- lookupEnvText "K_SERVICE"
  mCloudFn <- lookupEnvText "FUNCTION_TARGET"
  mAppEngine <- lookupEnvText "GAE_SERVICE"
  mGaeEnv <- lookupEnvText "GAE_ENV"

  let isGcp =
        any
          (/= Nothing)
          [mProject, mCloudRun, mCloudFn, mAppEngine, mGaeEnv]

  if not isGcp
    then pure Nothing
    else do
      mRegion <- firstEnv ["GOOGLE_CLOUD_REGION", "FUNCTION_REGION"]
      let platform = case () of
            _
              | mCloudRun /= Nothing -> Just "gcp_cloud_run"
              | mCloudFn /= Nothing -> Just "gcp_cloud_functions"
              | mAppEngine /= Nothing || mGaeEnv /= Nothing -> Just "gcp_app_engine"
              | otherwise -> Nothing
      pure $
        Just
          Cloud
            { cloudProvider = Just "gcp"
            , cloudAccountId = mProject
            , cloudRegion = mRegion
            , cloudAvailabilityZone = Nothing
            , cloudPlatform = platform
            , cloudResourceId = Nothing
            }


-- Azure detection via environment variables set by App Service, Functions, etc.
detectAzure :: IO (Maybe Cloud)
detectAzure = do
  mWebsite <- lookupEnvText "WEBSITE_SITE_NAME"
  mFunctions <- lookupEnvText "FUNCTIONS_WORKER_RUNTIME"
  mContainerApp <- lookupEnvText "CONTAINER_APP_NAME"
  mAzureEnv <- lookupEnvText "AZURE_FUNCTIONS_ENVIRONMENT"

  let isAzure =
        any
          (/= Nothing)
          [mWebsite, mFunctions, mContainerApp, mAzureEnv]

  if not isAzure
    then pure Nothing
    else do
      mRegion <- lookupEnvText "REGION_NAME"
      mSub <- lookupEnvText "AZURE_SUBSCRIPTION_ID"
      let platform
            | mFunctions /= Nothing || mAzureEnv /= Nothing = Just "azure.functions"
            | mContainerApp /= Nothing = Just "azure.container_apps"
            | mWebsite /= Nothing = Just "azure.app_service"
            | otherwise = Nothing
      pure $
        Just
          Cloud
            { cloudProvider = Just "azure"
            , cloudAccountId = mSub
            , cloudRegion = mRegion
            , cloudAvailabilityZone = Nothing
            , cloudPlatform = platform
            , cloudResourceId = Nothing
            }