use opentelemetry::KeyValue;
use opentelemetry_sdk::resource::ResourceDetector;
use opentelemetry_sdk::Resource;
use opentelemetry_semantic_conventions::attribute as semco;
use std::env;
use super::utils::{info_on_error, non_empty, opt_kv, opt_kv_array, warn_on_error};
const DETECTOR: &str = "aws_lambda";
const AWS_LAMBDA_FUNCTION_NAME_ENV_VAR: &str = "AWS_LAMBDA_FUNCTION_NAME";
const AWS_REGION_ENV_VAR: &str = "AWS_REGION";
const AWS_LAMBDA_FUNCTION_VERSION_ENV_VAR: &str = "AWS_LAMBDA_FUNCTION_VERSION";
const AWS_LAMBDA_LOG_STREAM_NAME_ENV_VAR: &str = "AWS_LAMBDA_LOG_STREAM_NAME";
const AWS_LAMBDA_MEMORY_LIMIT_ENV_VAR: &str = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE";
const AWS_LAMBDA_LOG_GROUP_NAME_ENV_VAR: &str = "AWS_LAMBDA_LOG_GROUP_NAME";
#[cfg(target_os = "linux")]
const ACCOUNT_ID_SYMLINK_PATH: &str = "/tmp/.otel-aws-account-id";
pub struct LambdaResourceDetector;
impl ResourceDetector for LambdaResourceDetector {
fn detect(&self) -> Resource {
#[cfg(target_os = "linux")]
return Self::detect_with_symlink_path(ACCOUNT_ID_SYMLINK_PATH);
#[cfg(not(target_os = "linux"))]
Self::build_resource(vec![])
}
}
impl LambdaResourceDetector {
#[cfg(target_os = "linux")]
fn detect_with_symlink_path(symlink_path: impl AsRef<std::path::Path>) -> Resource {
let mut extra = vec![];
if let Ok(account_id) = std::fs::read_link(symlink_path) {
if let Some(account_id_str) = account_id.to_str() {
if account_id_str.len() == 12 && account_id_str.chars().all(|c| c.is_ascii_digit())
{
extra.push(KeyValue::new(
semco::CLOUD_ACCOUNT_ID,
account_id_str.to_string(),
));
}
}
}
Self::build_resource(extra)
}
fn build_resource(extra_attributes: Vec<KeyValue>) -> Resource {
let Some(lambda_name) =
info_on_error(DETECTOR, env::var(AWS_LAMBDA_FUNCTION_NAME_ENV_VAR)).and_then(non_empty)
else {
return Resource::builder_empty().build();
};
let function_memory_limit =
warn_on_error(DETECTOR, env::var(AWS_LAMBDA_MEMORY_LIMIT_ENV_VAR))
.and_then(|s| warn_on_error(DETECTOR, s.parse::<i64>()))
.map(|mb| KeyValue::new(semco::FAAS_MAX_MEMORY, mb * 1024 * 1024));
let log_group_names = warn_on_error(DETECTOR, env::var(AWS_LAMBDA_LOG_GROUP_NAME_ENV_VAR))
.and_then(non_empty);
let attribute_options = [
Some(KeyValue::new(semco::CLOUD_PROVIDER, "aws")),
Some(KeyValue::new(semco::CLOUD_PLATFORM, "aws_lambda")),
Some(KeyValue::new(semco::FAAS_NAME, lambda_name)),
opt_kv(
semco::CLOUD_REGION,
warn_on_error(DETECTOR, env::var(AWS_REGION_ENV_VAR)),
),
opt_kv(
semco::FAAS_VERSION,
warn_on_error(DETECTOR, env::var(AWS_LAMBDA_FUNCTION_VERSION_ENV_VAR)),
),
opt_kv(
semco::FAAS_INSTANCE,
warn_on_error(DETECTOR, env::var(AWS_LAMBDA_LOG_STREAM_NAME_ENV_VAR)),
),
function_memory_limit,
opt_kv_array(semco::AWS_LOG_GROUP_NAMES, log_group_names),
];
Resource::builder_empty()
.with_attributes(attribute_options.into_iter().flatten())
.with_attributes(extra_attributes)
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use opentelemetry::{Array, StringValue, Value};
use sealed_test::prelude::*;
#[sealed_test]
fn test_aws_lambda_detector() {
temp_env::with_vars(
[
(AWS_LAMBDA_FUNCTION_NAME_ENV_VAR, Some("my-lambda-function")),
(AWS_REGION_ENV_VAR, Some("eu-west-3")),
(AWS_LAMBDA_FUNCTION_VERSION_ENV_VAR, Some("$LATEST")),
(
AWS_LAMBDA_LOG_STREAM_NAME_ENV_VAR,
Some("2023/01/01/[$LATEST]5d1edb9e525d486696cf01a3503487bc"),
),
(AWS_LAMBDA_MEMORY_LIMIT_ENV_VAR, Some("128")),
(
AWS_LAMBDA_LOG_GROUP_NAME_ENV_VAR,
Some("/aws/lambda/my-lambda-function"),
),
],
|| {
let expected = Resource::builder_empty()
.with_attributes([
KeyValue::new(semco::CLOUD_PROVIDER, "aws"),
KeyValue::new(semco::CLOUD_PLATFORM, "aws_lambda"),
KeyValue::new(semco::CLOUD_REGION, "eu-west-3"),
KeyValue::new(
semco::FAAS_INSTANCE,
"2023/01/01/[$LATEST]5d1edb9e525d486696cf01a3503487bc",
),
KeyValue::new(semco::FAAS_NAME, "my-lambda-function"),
KeyValue::new(semco::FAAS_VERSION, "$LATEST"),
KeyValue::new(semco::FAAS_MAX_MEMORY, 128 * 1024 * 1024),
KeyValue::new(
semco::AWS_LOG_GROUP_NAMES,
Value::Array(Array::from(vec![StringValue::from(
"/aws/lambda/my-lambda-function".to_string(),
)])),
),
])
.build();
let detector = LambdaResourceDetector {};
let got = detector.detect();
assert_eq!(expected, got);
},
);
}
#[sealed_test]
fn test_aws_lambda_detector_returns_empty_if_no_lambda_environment() {
let detector = LambdaResourceDetector {};
let got = detector.detect();
assert_eq!(Resource::builder_empty().build(), got);
}
#[cfg(target_os = "linux")]
#[sealed_test]
fn test_aws_lambda_detector_with_account_id_symlink() {
let symlink_path = std::env::temp_dir().join(".otel-aws-account-id-test");
let _ = std::fs::remove_file(&symlink_path);
std::os::unix::fs::symlink("123456789012", &symlink_path).unwrap();
temp_env::with_vars(
[
(AWS_LAMBDA_FUNCTION_NAME_ENV_VAR, Some("my-lambda-function")),
(AWS_REGION_ENV_VAR, Some("eu-west-3")),
(AWS_LAMBDA_FUNCTION_VERSION_ENV_VAR, Some("$LATEST")),
(
AWS_LAMBDA_LOG_STREAM_NAME_ENV_VAR,
Some("2023/01/01/[$LATEST]5d1edb9e525d486696cf01a3503487bc"),
),
(AWS_LAMBDA_MEMORY_LIMIT_ENV_VAR, Some("128")),
(
AWS_LAMBDA_LOG_GROUP_NAME_ENV_VAR,
Some("/aws/lambda/my-lambda-function"),
),
],
|| {
let got = LambdaResourceDetector::detect_with_symlink_path(&symlink_path);
let account_id = got
.iter()
.find(|(k, _)| k.as_str() == semco::CLOUD_ACCOUNT_ID);
assert!(
account_id.is_some(),
"cloud.account.id attribute should be present"
);
assert_eq!(account_id.unwrap().1.as_str(), "123456789012");
},
);
let _ = std::fs::remove_file(&symlink_path);
}
#[cfg(target_os = "linux")]
#[sealed_test]
fn test_aws_lambda_detector_with_corrupted_symlink_target() {
let symlink_path = std::env::temp_dir().join(".otel-aws-account-id-corrupted-test");
let _ = std::fs::remove_file(&symlink_path);
std::os::unix::fs::symlink("not-an-account-id!!", &symlink_path).unwrap();
temp_env::with_vars(
[
(AWS_LAMBDA_FUNCTION_NAME_ENV_VAR, Some("my-lambda-function")),
(AWS_REGION_ENV_VAR, Some("eu-west-3")),
(AWS_LAMBDA_FUNCTION_VERSION_ENV_VAR, Some("$LATEST")),
(
AWS_LAMBDA_LOG_STREAM_NAME_ENV_VAR,
Some("2023/01/01/[$LATEST]5d1edb9e525d486696cf01a3503487bc"),
),
(AWS_LAMBDA_MEMORY_LIMIT_ENV_VAR, Some("128")),
(
AWS_LAMBDA_LOG_GROUP_NAME_ENV_VAR,
Some("/aws/lambda/my-lambda-function"),
),
],
|| {
let got = LambdaResourceDetector::detect_with_symlink_path(&symlink_path);
let account_id = got
.iter()
.find(|(k, _)| k.as_str() == semco::CLOUD_ACCOUNT_ID);
assert!(
account_id.is_none(),
"cloud.account.id should not be set for a corrupted symlink target"
);
},
);
let _ = std::fs::remove_file(&symlink_path);
}
#[cfg(target_os = "linux")]
#[sealed_test]
fn test_aws_lambda_detector_missing_symlink_no_panic() {
let symlink_path = std::env::temp_dir().join(".otel-aws-account-id-nonexistent");
let _ = std::fs::remove_file(&symlink_path);
temp_env::with_vars(
[
(AWS_LAMBDA_FUNCTION_NAME_ENV_VAR, Some("my-lambda-function")),
(AWS_REGION_ENV_VAR, Some("eu-west-3")),
(AWS_LAMBDA_FUNCTION_VERSION_ENV_VAR, Some("$LATEST")),
(
AWS_LAMBDA_LOG_STREAM_NAME_ENV_VAR,
Some("2023/01/01/[$LATEST]5d1edb9e525d486696cf01a3503487bc"),
),
(AWS_LAMBDA_MEMORY_LIMIT_ENV_VAR, Some("128")),
(
AWS_LAMBDA_LOG_GROUP_NAME_ENV_VAR,
Some("/aws/lambda/my-lambda-function"),
),
],
|| {
let got = LambdaResourceDetector::detect_with_symlink_path(&symlink_path);
let account_id = got
.iter()
.find(|(k, _)| k.as_str() == semco::CLOUD_ACCOUNT_ID);
assert!(
account_id.is_none(),
"cloud.account.id attribute should not be present when symlink is missing"
);
},
);
}
}