use opentelemetry::KeyValue;
use opentelemetry_sdk::{resource::ResourceDetector, Resource};
use opentelemetry_semantic_conventions::attribute as semco;
use super::{
imds::{ImdsClient, ImdsError, ImdsProvider},
utils::{info_on_error, opt_kv, warn_on_error},
};
const DETECTOR: &str = "aws_ec2";
pub struct Ec2ResourceDetector;
impl ResourceDetector for Ec2ResourceDetector {
fn detect(&self) -> Resource {
Self::detect_from(ImdsClient::new())
}
}
impl Ec2ResourceDetector {
fn detect_from<P: ImdsProvider>(imds: Result<P, ImdsError>) -> Resource {
let Some(imds) = info_on_error(DETECTOR, imds) else {
return Resource::builder_empty().build();
};
let Some(document) = info_on_error(DETECTOR, imds.get_identity_document()) else {
return Resource::builder_empty().build();
};
let attribute_options = [
Some(KeyValue::new(semco::CLOUD_PROVIDER, "aws")),
Some(KeyValue::new(semco::CLOUD_PLATFORM, "aws_ec2")),
document
.host_arch()
.map(|arch| KeyValue::new(semco::HOST_ARCH, arch)),
opt_kv(semco::CLOUD_ACCOUNT_ID, document.account_id),
opt_kv(semco::CLOUD_REGION, document.region),
opt_kv(semco::CLOUD_AVAILABILITY_ZONE, document.availability_zone),
opt_kv(semco::HOST_ID, document.instance_id),
opt_kv(semco::HOST_TYPE, document.instance_type),
opt_kv(semco::HOST_IMAGE_ID, document.image_id),
opt_kv(
semco::HOST_NAME,
warn_on_error(DETECTOR, imds.get("hostname")),
),
];
Resource::builder_empty()
.with_attributes(attribute_options.into_iter().flatten())
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::detector::imds::tests::FakeImdsClient;
const FULL_DOC: &str = r#"
{
"accountId": "123456789012",
"region": "us-east-1",
"availabilityZone": "us-east-1a",
"instanceId": "i-0abcdef1234567890",
"instanceType": "m5.xlarge",
"imageId": "ami-0abcdef1234567890",
"architecture": "x86_64"
}
"#;
const SPARSE_DOC_ARM64: &str = r#"
{
"instanceId": "i-0abcdef1234567890",
"architecture": "arm64"
}
"#;
const DOC_UNKNOWN_ARCH: &str = r#"
{
"accountId": "123456789012",
"region": "us-west-2",
"instanceId": "i-0aaa",
"architecture": "mips"
}
"#;
#[test]
fn detect_from_full_document_and_hostname() {
let fake = FakeImdsClient::new()
.with_document(FULL_DOC)
.with_get("hostname", "ip-10-0-1-2.ec2.internal");
let resource = Ec2ResourceDetector::detect_from(Ok(fake));
let expected = Resource::builder_empty()
.with_attributes([
KeyValue::new(semco::CLOUD_PROVIDER, "aws"),
KeyValue::new(semco::CLOUD_PLATFORM, "aws_ec2"),
KeyValue::new(semco::HOST_ARCH, "amd64"),
KeyValue::new(semco::CLOUD_ACCOUNT_ID, "123456789012"),
KeyValue::new(semco::CLOUD_REGION, "us-east-1"),
KeyValue::new(semco::CLOUD_AVAILABILITY_ZONE, "us-east-1a"),
KeyValue::new(semco::HOST_ID, "i-0abcdef1234567890"),
KeyValue::new(semco::HOST_TYPE, "m5.xlarge"),
KeyValue::new(semco::HOST_IMAGE_ID, "ami-0abcdef1234567890"),
KeyValue::new(semco::HOST_NAME, "ip-10-0-1-2.ec2.internal"),
])
.build();
assert_eq!(resource, expected);
}
#[test]
fn detect_from_construction_failure_returns_empty() {
let resource =
Ec2ResourceDetector::detect_from::<FakeImdsClient>(Err(ImdsError::EmptyAuthToken));
assert_eq!(resource, Resource::builder_empty().build());
}
#[test]
fn detect_from_document_error_returns_empty() {
let fake = FakeImdsClient::new();
let resource = Ec2ResourceDetector::detect_from(Ok(fake));
assert_eq!(resource, Resource::builder_empty().build());
}
#[test]
fn detect_from_partial_document_omits_missing_fields() {
let fake = FakeImdsClient::new()
.with_document(SPARSE_DOC_ARM64)
.with_get("hostname", "ip-10-0-0-1.ec2.internal");
let resource = Ec2ResourceDetector::detect_from(Ok(fake));
let expected = Resource::builder_empty()
.with_attributes([
KeyValue::new(semco::CLOUD_PROVIDER, "aws"),
KeyValue::new(semco::CLOUD_PLATFORM, "aws_ec2"),
KeyValue::new(semco::HOST_ARCH, "arm64"),
KeyValue::new(semco::HOST_ID, "i-0abcdef1234567890"),
KeyValue::new(semco::HOST_NAME, "ip-10-0-0-1.ec2.internal"),
])
.build();
assert_eq!(resource, expected);
}
#[test]
fn detect_from_unknown_arch_omitted() {
let fake = FakeImdsClient::new().with_document(DOC_UNKNOWN_ARCH);
let resource = Ec2ResourceDetector::detect_from(Ok(fake));
let expected = Resource::builder_empty()
.with_attributes([
KeyValue::new(semco::CLOUD_PROVIDER, "aws"),
KeyValue::new(semco::CLOUD_PLATFORM, "aws_ec2"),
KeyValue::new(semco::CLOUD_ACCOUNT_ID, "123456789012"),
KeyValue::new(semco::CLOUD_REGION, "us-west-2"),
KeyValue::new(semco::HOST_ID, "i-0aaa"),
])
.build();
assert_eq!(resource, expected);
}
}