diff --git a/resources/aws/README.md b/resources/aws/README.md index e20610ead6..d2c42a92e1 100644 --- a/resources/aws/README.md +++ b/resources/aws/README.md @@ -31,11 +31,12 @@ require 'opentelemetry/resource/detector' OpenTelemetry::SDK.configure do |c| # Specify which AWS resource detectors to use - c.resource = OpenTelemetry::Resource::Detector::AWS.detect([:ec2, :ecs]) + c.resource = OpenTelemetry::Resource::Detector::AWS.detect([:ec2, :ecs, :lambda]) # Or use just one detector c.resource = OpenTelemetry::Resource::Detector::AWS.detect([:ec2]) c.resource = OpenTelemetry::Resource::Detector::AWS.detect([:ecs]) + c.resource = OpenTelemetry::Resource::Detector::AWS.detect([:lambda]) end ``` @@ -75,7 +76,19 @@ Populates `cloud`, `container`, and AWS ECS-specific attributes for processes ru | `aws.log.stream.names` | The CloudWatch log stream names (if awslogs driver is used) | | `aws.log.stream.arns` | The CloudWatch log stream ARNs (if awslogs driver is used) | -Additional AWS platforms (EKS, Lambda) will be supported in future versions. +### AWS Lambda Detector +Populates `cloud` and `faas` (Function as a Service) attributes for processes running on AWS Lambda. +| Resource Attribute | Description | +|--------------------|-------------| +| `cloud.platform` | The cloud platform. In this context, it's always "aws_lambda" | +| `cloud.provider` | The cloud provider. In this context, it's always "aws" | +| `cloud.region` | The AWS region from the `AWS_REGION` environment variable | +| `faas.name` | The Lambda function name from the `AWS_LAMBDA_FUNCTION_NAME` environment variable | +| `faas.version` | The Lambda function version from the `AWS_LAMBDA_FUNCTION_VERSION` environment variable | +| `faas.instance` | The Lambda function instance ID from the `AWS_LAMBDA_LOG_STREAM_NAME` environment variable | +| `faas.max_memory` | The Lambda function memory size in MB from the `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` environment variable | + +Additional AWS platforms (EKS) will be supported in future versions. ## License diff --git a/resources/aws/lib/opentelemetry/resource/detector/aws.rb b/resources/aws/lib/opentelemetry/resource/detector/aws.rb index c8c0a421bb..0b06ed7315 100644 --- a/resources/aws/lib/opentelemetry/resource/detector/aws.rb +++ b/resources/aws/lib/opentelemetry/resource/detector/aws.rb @@ -6,6 +6,7 @@ require 'opentelemetry/resource/detector/aws/ec2' require 'opentelemetry/resource/detector/aws/ecs' +require 'opentelemetry/resource/detector/aws/lambda' module OpenTelemetry module Resource @@ -29,6 +30,8 @@ def detect(detectors = []) EC2.detect when :ecs ECS.detect + when :lambda + Lambda.detect else OpenTelemetry.logger.warn("Unknown AWS resource detector: #{detector}") OpenTelemetry::SDK::Resources::Resource.create({}) diff --git a/resources/aws/lib/opentelemetry/resource/detector/aws/lambda.rb b/resources/aws/lib/opentelemetry/resource/detector/aws/lambda.rb new file mode 100644 index 0000000000..8f3a83e0ef --- /dev/null +++ b/resources/aws/lib/opentelemetry/resource/detector/aws/lambda.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Resource + module Detector + module AWS + # Lambda contains detect class method for determining Lambda resource attributes + module Lambda + extend self + + # Create a constant for resource semantic conventions + RESOURCE = OpenTelemetry::SemanticConventions::Resource + + def detect + # Return empty resource if not running on Lambda + return OpenTelemetry::SDK::Resources::Resource.create({}) unless lambda_environment? + + resource_attributes = {} + + begin + # Set Lambda-specific attributes from environment variables + resource_attributes[RESOURCE::CLOUD_PROVIDER] = 'aws' + resource_attributes[RESOURCE::CLOUD_PLATFORM] = 'aws_lambda' + resource_attributes[RESOURCE::CLOUD_REGION] = ENV.fetch('AWS_REGION', nil) + resource_attributes[RESOURCE::FAAS_NAME] = ENV.fetch('AWS_LAMBDA_FUNCTION_NAME', nil) + resource_attributes[RESOURCE::FAAS_VERSION] = ENV.fetch('AWS_LAMBDA_FUNCTION_VERSION', nil) + resource_attributes[RESOURCE::FAAS_INSTANCE] = ENV.fetch('AWS_LAMBDA_LOG_STREAM_NAME', nil) + + # Convert memory size to integer + resource_attributes[RESOURCE::FAAS_MAX_MEMORY] = ENV['AWS_LAMBDA_FUNCTION_MEMORY_SIZE'].to_i if ENV['AWS_LAMBDA_FUNCTION_MEMORY_SIZE'] + rescue StandardError => e + OpenTelemetry.handle_error(exception: e, message: 'Lambda resource detection failed') + return OpenTelemetry::SDK::Resources::Resource.create({}) + end + + # Filter out nil or empty values + # Note: we need to handle integers differently since they don't respond to empty? + resource_attributes.delete_if do |_key, value| + value.nil? || (value.respond_to?(:empty?) && value.empty?) + end + + OpenTelemetry::SDK::Resources::Resource.create(resource_attributes) + end + + private + + # Determines if the current environment is AWS Lambda + # + # @return [Boolean] true if running on AWS Lambda + def lambda_environment? + # Check for Lambda-specific environment variables + !ENV['AWS_LAMBDA_FUNCTION_NAME'].nil? && + !ENV['AWS_LAMBDA_FUNCTION_VERSION'].nil? && + !ENV['AWS_LAMBDA_LOG_STREAM_NAME'].nil? + end + end + end + end + end +end diff --git a/resources/aws/test/opentelemetry/resource/detector/aws/lambda_test.rb b/resources/aws/test/opentelemetry/resource/detector/aws/lambda_test.rb new file mode 100644 index 0000000000..c94fdcad21 --- /dev/null +++ b/resources/aws/test/opentelemetry/resource/detector/aws/lambda_test.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Resource::Detector::AWS::Lambda do + let(:detector) { OpenTelemetry::Resource::Detector::AWS::Lambda } + + describe '.detect' do + before do + # Store original environment variables + @original_env = ENV.to_hash + ENV.clear + end + + after do + # Restore original environment + ENV.replace(@original_env) + end + + it 'returns empty resource when not running on Lambda' do + resource = detector.detect + _(resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + _(resource.attribute_enumerator.to_h).must_equal({}) + end + + describe 'when running on Lambda' do + before do + # Set Lambda environment variables + ENV['AWS_LAMBDA_FUNCTION_NAME'] = 'my-function' + ENV['AWS_LAMBDA_FUNCTION_VERSION'] = '$LATEST' + ENV['AWS_LAMBDA_LOG_STREAM_NAME'] = '2021/01/01/[$LATEST]abcdef123456' + ENV['AWS_REGION'] = 'us-west-2' + ENV['AWS_LAMBDA_FUNCTION_MEMORY_SIZE'] = '512' + end + + it 'detects Lambda resources' do + resource = detector.detect + + _(resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + attributes = resource.attribute_enumerator.to_h + + # Check Lambda-specific attributes + _(attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_PROVIDER]).must_equal('aws') + _(attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_PLATFORM]).must_equal('aws_lambda') + _(attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_REGION]).must_equal('us-west-2') + _(attributes[OpenTelemetry::SemanticConventions::Resource::FAAS_NAME]).must_equal('my-function') + _(attributes[OpenTelemetry::SemanticConventions::Resource::FAAS_VERSION]).must_equal('$LATEST') + _(attributes[OpenTelemetry::SemanticConventions::Resource::FAAS_INSTANCE]).must_equal('2021/01/01/[$LATEST]abcdef123456') + _(attributes[OpenTelemetry::SemanticConventions::Resource::FAAS_MAX_MEMORY]).must_equal(512) + end + + it 'handles missing memory size' do + ENV.delete('AWS_LAMBDA_FUNCTION_MEMORY_SIZE') + + resource = detector.detect + attributes = resource.attribute_enumerator.to_h + + _(attributes).wont_include(OpenTelemetry::SemanticConventions::Resource::FAAS_MAX_MEMORY) + end + end + + describe 'when partial Lambda environment is detected' do + before do + # Set only some Lambda environment variables + ENV['AWS_LAMBDA_FUNCTION_NAME'] = 'my-function' + # Missing AWS_LAMBDA_FUNCTION_VERSION + ENV['AWS_LAMBDA_LOG_STREAM_NAME'] = '2021/01/01/[$LATEST]abcdef123456' + end + + it 'returns empty resource' do + resource = detector.detect + _(resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + _(resource.attribute_enumerator.to_h).must_equal({}) + end + end + end +end diff --git a/resources/aws/test/opentelemetry/resource/detector/aws_test.rb b/resources/aws/test/opentelemetry/resource/detector/aws_test.rb index 85cb65ce76..08703f744b 100644 --- a/resources/aws/test/opentelemetry/resource/detector/aws_test.rb +++ b/resources/aws/test/opentelemetry/resource/detector/aws_test.rb @@ -9,6 +9,8 @@ describe OpenTelemetry::Resource::Detector::AWS do let(:detector) { OpenTelemetry::Resource::Detector::AWS } + RESOURCE = OpenTelemetry::SemanticConventions::Resource + describe '.detect' do before do WebMock.disable_net_connect! @@ -22,10 +24,13 @@ .with(headers: { 'Accept' => '*/*' }) .to_return(status: 404, body: 'Not Found') - # Clear environment variables for ECS + # Clear environment variables for ECS and Lambda @original_env = ENV.to_hash ENV.delete('ECS_CONTAINER_METADATA_URI') ENV.delete('ECS_CONTAINER_METADATA_URI_V4') + ENV.delete('AWS_LAMBDA_FUNCTION_NAME') + ENV.delete('AWS_LAMBDA_FUNCTION_VERSION') + ENV.delete('AWS_LAMBDA_LOG_STREAM_NAME') end after do @@ -53,6 +58,10 @@ def assert_detection_result(detectors) assert_detection_result([:ecs]) end + it 'returns an empty resource when Lambda detection fails' do + assert_detection_result([:lambda]) + end + it 'returns an empty resource with unknown detector' do assert_detection_result([:unknown]) end @@ -93,14 +102,14 @@ def assert_detection_result(detectors) resource = detector.detect([:ec2]) attributes = resource.attribute_enumerator.to_h - _(attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_PROVIDER]).must_equal('aws') - _(attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_PLATFORM]).must_equal('aws_ec2') - _(attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_ACCOUNT_ID]).must_equal('123456789012') - _(attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_REGION]).must_equal('us-west-2') - _(attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_AVAILABILITY_ZONE]).must_equal('us-west-2b') - _(attributes[OpenTelemetry::SemanticConventions::Resource::HOST_ID]).must_equal('i-1234567890abcdef0') - _(attributes[OpenTelemetry::SemanticConventions::Resource::HOST_TYPE]).must_equal('m5.xlarge') - _(attributes[OpenTelemetry::SemanticConventions::Resource::HOST_NAME]).must_equal(hostname) + _(attributes[RESOURCE::CLOUD_PROVIDER]).must_equal('aws') + _(attributes[RESOURCE::CLOUD_PLATFORM]).must_equal('aws_ec2') + _(attributes[RESOURCE::CLOUD_ACCOUNT_ID]).must_equal('123456789012') + _(attributes[RESOURCE::CLOUD_REGION]).must_equal('us-west-2') + _(attributes[RESOURCE::CLOUD_AVAILABILITY_ZONE]).must_equal('us-west-2b') + _(attributes[RESOURCE::HOST_ID]).must_equal('i-1234567890abcdef0') + _(attributes[RESOURCE::HOST_TYPE]).must_equal('m5.xlarge') + _(attributes[RESOURCE::HOST_NAME]).must_equal(hostname) end describe 'with succesefful ECS detection' do @@ -147,8 +156,8 @@ def assert_detection_result(detectors) resource = detector.detect([:ecs]) attributes = resource.attribute_enumerator.to_h - _(attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_PROVIDER]).must_equal('aws') - _(attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_PLATFORM]).must_equal('aws_ecs') + _(attributes[RESOURCE::CLOUD_PROVIDER]).must_equal('aws') + _(attributes[RESOURCE::CLOUD_PLATFORM]).must_equal('aws_ecs') end end end @@ -164,13 +173,56 @@ def assert_detection_result(detectors) attributes = resource.attribute_enumerator.to_h # Should include attributes from both detectors - _(attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_PROVIDER]).must_equal('aws') - _(attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_PLATFORM]).must_equal('aws_ecs') + _(attributes[RESOURCE::CLOUD_PROVIDER]).must_equal('aws') + _(attributes[RESOURCE::CLOUD_PLATFORM]).must_equal('aws_ecs') _(attributes['ec2.instance.id']).must_equal('i-1234567890abcdef0') end end end end + + describe 'with successful Lambda detection' do + before do + # Set Lambda environment variables + ENV['AWS_LAMBDA_FUNCTION_NAME'] = 'my-function' + ENV['AWS_LAMBDA_FUNCTION_VERSION'] = '$LATEST' + ENV['AWS_LAMBDA_LOG_STREAM_NAME'] = '2025/01/01/[$LATEST]abcdef123456' + ENV['AWS_REGION'] = 'us-east-1' + ENV['AWS_LAMBDA_FUNCTION_MEMORY_SIZE'] = '512' + end + + it 'detects Lambda resources when specified' do + resource = detector.detect([:lambda]) + attributes = resource.attribute_enumerator.to_h + + _(attributes[RESOURCE::CLOUD_PROVIDER]).must_equal('aws') + _(attributes[RESOURCE::CLOUD_PLATFORM]).must_equal('aws_lambda') + _(attributes[RESOURCE::CLOUD_REGION]).must_equal('us-east-1') + _(attributes[RESOURCE::FAAS_NAME]).must_equal('my-function') + _(attributes[RESOURCE::FAAS_VERSION]).must_equal('$LATEST') + _(attributes[RESOURCE::FAAS_INSTANCE]).must_equal('2025/01/01/[$LATEST]abcdef123456') + _(attributes[RESOURCE::FAAS_MAX_MEMORY]).must_equal(512) + end + + it 'detects multiple resources when specified' do + # Create a mock EC2 resource + ec2_resource = OpenTelemetry::SDK::Resources::Resource.create({ + RESOURCE::HOST_ID => 'i-1234567890abcdef0' + }) + + # Stub EC2 detection to return the mock resource + OpenTelemetry::Resource::Detector::AWS::EC2.stub :detect, ec2_resource do + resource = detector.detect(%i[ec2 lambda]) + attributes = resource.attribute_enumerator.to_h + + # Should include attributes from both detectors + _(attributes[RESOURCE::CLOUD_PROVIDER]).must_equal('aws') + _(attributes[RESOURCE::CLOUD_PLATFORM]).must_equal('aws_lambda') + _(attributes[RESOURCE::FAAS_NAME]).must_equal('my-function') + _(attributes[RESOURCE::HOST_ID]).must_equal('i-1234567890abcdef0') + end + end + end end end end