diff --git a/opentelemetry-aws/CHANGELOG.md b/opentelemetry-aws/CHANGELOG.md index 60974e78..72a2483d 100644 --- a/opentelemetry-aws/CHANGELOG.md +++ b/opentelemetry-aws/CHANGELOG.md @@ -2,6 +2,10 @@ ## vNext +### Added + +- `LambdaResourceDetector` has been added to the crate to detect AWS Lambda attributes. To enable it in your code, use the feature `detector-aws-lambda`. + ### v0.12.0 - Bump opentelemetry and opentelemetry_sdk versions to 0.24.0 diff --git a/opentelemetry-aws/Cargo.toml b/opentelemetry-aws/Cargo.toml index 7d7b6bbd..3db4863d 100644 --- a/opentelemetry-aws/Cargo.toml +++ b/opentelemetry-aws/Cargo.toml @@ -21,11 +21,13 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = ["trace"] trace = ["opentelemetry/trace", "opentelemetry_sdk/trace"] +detector-aws-lambda = ["dep:opentelemetry-semantic-conventions"] [dependencies] once_cell = "1.12" opentelemetry = { workspace = true } opentelemetry_sdk = { workspace = true, optional = true } +opentelemetry-semantic-conventions = { workspace = true, optional = true } [dev-dependencies] opentelemetry_sdk = { workspace = true, features = ["testing"] } @@ -33,3 +35,4 @@ opentelemetry-http = { workspace = true } opentelemetry-stdout = { workspace = true, features = ["trace"] } hyper = { version = "1.4.1" } tokio = { version = "1.0", features = ["macros", "rt"] } +sealed_test = "1.1.0" diff --git a/opentelemetry-aws/src/detector/lambda.rs b/opentelemetry-aws/src/detector/lambda.rs new file mode 100644 index 00000000..ddcd5ca7 --- /dev/null +++ b/opentelemetry-aws/src/detector/lambda.rs @@ -0,0 +1,95 @@ +use opentelemetry::KeyValue; +use opentelemetry_sdk::resource::ResourceDetector; +use opentelemetry_sdk::Resource; +use opentelemetry_semantic_conventions as semconv; +use std::env; +use std::time::Duration; + +// For a complete list of reserved environment variables in Lambda, see: +// https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html +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"; + +/// Resource detector that collects resource information from AWS Lambda environment. +pub struct LambdaResourceDetector; + +impl ResourceDetector for LambdaResourceDetector { + fn detect(&self, _: Duration) -> Resource { + let lambda_name = env::var(AWS_LAMBDA_FUNCTION_NAME_ENV_VAR).unwrap_or_default(); + // If no lambda name is provided, it means that + // we're not on a Lambda environment, so we return empty resource. + if lambda_name.is_empty() { + return Resource::empty(); + } + + let aws_region = env::var(AWS_REGION_ENV_VAR).unwrap_or_default(); + let function_version = env::var(AWS_LAMBDA_FUNCTION_VERSION_ENV_VAR).unwrap_or_default(); + let function_memory_limit = env::var(AWS_LAMBDA_MEMORY_LIMIT_ENV_VAR).unwrap_or_default(); + // Instance attributes corresponds to the log stream name for AWS Lambda; + // See the FaaS resource specification for more details. + let instance = env::var(AWS_LAMBDA_LOG_STREAM_NAME_ENV_VAR).unwrap_or_default(); + + let attributes = [ + KeyValue::new(semconv::resource::CLOUD_PROVIDER, "aws"), + KeyValue::new(semconv::resource::CLOUD_REGION, aws_region), + KeyValue::new(semconv::resource::FAAS_INSTANCE, instance), + KeyValue::new(semconv::resource::FAAS_NAME, lambda_name), + KeyValue::new(semconv::resource::FAAS_VERSION, function_version), + KeyValue::new(semconv::resource::FAAS_MAX_MEMORY, function_memory_limit), + ]; + + Resource::new(attributes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use sealed_test::prelude::*; + use std::env::{remove_var, set_var}; + + #[sealed_test] + fn test_aws_lambda_detector() { + set_var(AWS_LAMBDA_FUNCTION_NAME_ENV_VAR, "my-lambda-function"); + set_var(AWS_REGION_ENV_VAR, "eu-west-3"); + set_var(AWS_LAMBDA_FUNCTION_VERSION_ENV_VAR, "$LATEST"); + set_var( + AWS_LAMBDA_LOG_STREAM_NAME_ENV_VAR, + "2023/01/01/[$LATEST]5d1edb9e525d486696cf01a3503487bc", + ); + set_var(AWS_LAMBDA_MEMORY_LIMIT_ENV_VAR, "128"); + + let expected = Resource::new([ + KeyValue::new(semconv::resource::CLOUD_PROVIDER, "aws"), + KeyValue::new(semconv::resource::CLOUD_REGION, "eu-west-3"), + KeyValue::new( + semconv::resource::FAAS_INSTANCE, + "2023/01/01/[$LATEST]5d1edb9e525d486696cf01a3503487bc", + ), + KeyValue::new(semconv::resource::FAAS_NAME, "my-lambda-function"), + KeyValue::new(semconv::resource::FAAS_VERSION, "$LATEST"), + KeyValue::new(semconv::resource::FAAS_MAX_MEMORY, "128"), + ]); + + let detector = LambdaResourceDetector {}; + let got = detector.detect(Duration::from_secs(0)); + + assert_eq!(expected, got); + + remove_var(AWS_LAMBDA_FUNCTION_NAME_ENV_VAR); + remove_var(AWS_REGION_ENV_VAR); + remove_var(AWS_LAMBDA_FUNCTION_VERSION_ENV_VAR); + remove_var(AWS_LAMBDA_LOG_STREAM_NAME_ENV_VAR); + remove_var(AWS_LAMBDA_MEMORY_LIMIT_ENV_VAR); + } + + #[sealed_test] + fn test_aws_lambda_detector_returns_empty_if_no_lambda_environment() { + let detector = LambdaResourceDetector {}; + let got = detector.detect(Duration::from_secs(0)); + assert_eq!(Resource::empty(), got); + } +} diff --git a/opentelemetry-aws/src/detector/mod.rs b/opentelemetry-aws/src/detector/mod.rs new file mode 100644 index 00000000..b0c12d15 --- /dev/null +++ b/opentelemetry-aws/src/detector/mod.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "detector-aws-lambda")] +mod lambda; +#[cfg(feature = "detector-aws-lambda")] +pub use lambda::LambdaResourceDetector; diff --git a/opentelemetry-aws/src/lib.rs b/opentelemetry-aws/src/lib.rs index 11ec9166..7094c25e 100644 --- a/opentelemetry-aws/src/lib.rs +++ b/opentelemetry-aws/src/lib.rs @@ -1 +1,2 @@ +pub mod detector; pub mod trace;