Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions resources/aws/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions resources/aws/lib/opentelemetry/resource/detector/aws.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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({})
Expand Down
64 changes: 64 additions & 0 deletions resources/aws/lib/opentelemetry/resource/detector/aws/lambda.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
78 changes: 65 additions & 13 deletions resources/aws/test/opentelemetry/resource/detector/aws_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading