hs-opentelemetry-sdk-1.0.0.0: src/OpenTelemetry/Resource/Detector/AWS/EC2.hs
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : OpenTelemetry.Resource.Detector.AWS.EC2
Copyright : (c) Ian Duncan, 2024
License : BSD-3
Description : Detect EC2 instance resource attributes via IMDS
Maintainer : Ian Duncan
Stability : experimental
Queries the EC2 Instance Metadata Service (IMDS) to populate rich resource
attributes for services running on EC2 instances.
Uses IMDSv2 (session-oriented, token-based) with a 2-second timeout.
Falls back to IMDSv1 if token acquisition fails. Returns an empty
'Resource' if not running on EC2.
Populates: @cloud.provider@, @cloud.platform@, @cloud.region@,
@cloud.availability_zone@, @cloud.account_id@, @host.id@, @host.type@,
@host.name@, @host.image.id@.
@since 0.1.0.2
-}
module OpenTelemetry.Resource.Detector.AWS.EC2 (
detectEC2,
detectEC2Self,
) where
import Data.Aeson (FromJSON (..), withObject, (.:?))
import Data.Text (Text)
import OpenTelemetry.Attributes.Key (unkey)
import OpenTelemetry.Resource (Resource, mkResource, (.=), (.=?))
import OpenTelemetry.Resource.Detector.Metadata
import qualified OpenTelemetry.SemanticConventions as SC
import System.Environment (lookupEnv)
imdsBase :: String
imdsBase = "http://169.254.169.254/latest"
tokenEndpoint :: String
tokenEndpoint = imdsBase ++ "/api/token"
metadataPath :: String -> String
metadataPath p = imdsBase ++ "/meta-data/" ++ p
{- | Self-contained EC2 detector suitable for the resource detector registry.
Checks for AWS env vars first, creates its own 'MetadataClient', then
queries IMDS. Returns an empty resource if not on AWS or IMDS is unreachable.
-}
detectEC2Self :: IO Resource
detectEC2Self = do
mRegion <- lookupEnv "AWS_REGION"
mDefault <- lookupEnv "AWS_DEFAULT_REGION"
mExecEnv <- lookupEnv "AWS_EXECUTION_ENV"
-- Also check for ECS. If on ECS, the ECS detector is more specific
mEcs <- lookupEnv "ECS_CONTAINER_METADATA_URI_V4"
let isNonEcsAws = any (/= Nothing) [mRegion, mDefault, mExecEnv] && mEcs == Nothing
if isNonEcsAws
then do
client <- newMetadataClient
detectEC2 client
else pure $ mkResource []
{- | Detect EC2 instance attributes via IMDS with an existing client.
Returns an empty resource if IMDS is unreachable.
-}
detectEC2 :: MetadataClient -> IO Resource
detectEC2 client = do
mToken <- acquireIMDSv2Token client
let fetch path = fetchWithToken client mToken (metadataPath path)
mInstanceId <- fetch "instance-id"
case mInstanceId of
Nothing -> pure $ mkResource []
Just instanceId -> do
mInstanceType <- fetch "instance-type"
mAmiId <- fetch "ami-id"
mHostname <- fetch "hostname"
mAz <- fetch "placement/availability-zone"
mRegion <- fetch "placement/region"
mAcctId <- fetchAccountId client mToken
pure $
mkResource
[ unkey SC.cloud_provider .= ("aws" :: Text)
, unkey SC.cloud_platform .= ("aws_ec2" :: Text)
, unkey SC.cloud_region .=? mRegion
, unkey SC.cloud_availabilityZone .=? mAz
, unkey SC.cloud_account_id .=? mAcctId
, unkey SC.host_id .= instanceId
, unkey SC.host_type .=? mInstanceType
, unkey SC.host_name .=? mHostname
, unkey SC.host_image_id .=? mAmiId
]
acquireIMDSv2Token :: MetadataClient -> IO (Maybe Text)
acquireIMDSv2Token client =
putForText
client
tokenEndpoint
[("X-aws-ec2-metadata-token-ttl-seconds", "21600")]
fetchWithToken :: MetadataClient -> Maybe Text -> String -> IO (Maybe Text)
fetchWithToken client mToken url = case mToken of
Just token -> fetchTextWithHeaders client url [("X-aws-ec2-metadata-token", token)]
Nothing -> fetchText client url
data IdentityDocument = IdentityDocument
{ idAccountId :: Maybe Text
}
instance FromJSON IdentityDocument where
parseJSON = withObject "IdentityDocument" $ \o ->
IdentityDocument <$> o .:? "accountId"
fetchAccountId :: MetadataClient -> Maybe Text -> IO (Maybe Text)
fetchAccountId client mToken = do
let url = imdsBase ++ "/dynamic/instance-identity/document"
mDoc <- case mToken of
Just token ->
fetchJSONWithHeaders client url [("X-aws-ec2-metadata-token", token)]
Nothing ->
fetchJSON client url
pure $ mDoc >>= idAccountId