From 5335cbe523a98e106f4ce8b8b712fbad4c21f939 Mon Sep 17 00:00:00 2001 From: yiyuanh Date: Tue, 25 Mar 2025 15:37:07 -0700 Subject: [PATCH 1/6] feat: contribute ec2 resource detector --- .github/workflows/ci-contrib.yml | 1 + resources/aws/Gemfile | 20 ++ resources/aws/LICENSE | 201 ++++++++++++++++++ resources/aws/README.md | 58 +++++ resources/aws/Rakefile | 28 +++ .../opentelemetry-resource-detector-aws.rb | 7 + .../lib/opentelemetry/resource/detector.rb | 23 ++ .../opentelemetry/resource/detector/aws.rb | 27 +++ .../resource/detector/aws/ec2.rb | 137 ++++++++++++ .../resource/detector/aws/version.rb | 15 ++ ...pentelemetry-resource-detector-aws.gemspec | 36 ++++ .../resource/detector/aws/ec2_test.rb | 188 ++++++++++++++++ .../resource/detector/aws_test.rb | 41 ++++ resources/aws/test/test_helper.rb | 15 ++ 14 files changed, 797 insertions(+) create mode 100644 resources/aws/Gemfile create mode 100644 resources/aws/LICENSE create mode 100644 resources/aws/README.md create mode 100644 resources/aws/Rakefile create mode 100644 resources/aws/lib/opentelemetry-resource-detector-aws.rb create mode 100644 resources/aws/lib/opentelemetry/resource/detector.rb create mode 100644 resources/aws/lib/opentelemetry/resource/detector/aws.rb create mode 100644 resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb create mode 100644 resources/aws/lib/opentelemetry/resource/detector/aws/version.rb create mode 100644 resources/aws/opentelemetry-resource-detector-aws.gemspec create mode 100644 resources/aws/test/opentelemetry/resource/detector/aws/ec2_test.rb create mode 100644 resources/aws/test/opentelemetry/resource/detector/aws_test.rb create mode 100644 resources/aws/test/test_helper.rb diff --git a/.github/workflows/ci-contrib.yml b/.github/workflows/ci-contrib.yml index eeb666d552..65e48a6afb 100644 --- a/.github/workflows/ci-contrib.yml +++ b/.github/workflows/ci-contrib.yml @@ -110,6 +110,7 @@ jobs: fail-fast: false matrix: gem: + - resource-detector-aws - resource-detector-azure - resource-detector-container - resource-detector-google_cloud_platform diff --git a/resources/aws/Gemfile b/resources/aws/Gemfile new file mode 100644 index 0000000000..19b8a7bce2 --- /dev/null +++ b/resources/aws/Gemfile @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +source 'https://rubygems.org' + +gemspec + +group :test do + gem 'minitest', '~> 5.0' + gem 'simplecov', '~> 0.17.1' + gem 'webmock', '~> 3.7.0' +end + +group :development do + gem 'rubocop', '~> 1.42.0' + gem 'yard', '~> 0.9' +end diff --git a/resources/aws/LICENSE b/resources/aws/LICENSE new file mode 100644 index 0000000000..1ef7dad2c5 --- /dev/null +++ b/resources/aws/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright The OpenTelemetry Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/resources/aws/README.md b/resources/aws/README.md new file mode 100644 index 0000000000..66edaad35f --- /dev/null +++ b/resources/aws/README.md @@ -0,0 +1,58 @@ +# OpenTelemetry::Resource::Detector::AWS + +The `opentelemetry-resource-detector-aws` gem provides an AWS resource detector for OpenTelemetry. + +## What is OpenTelemetry? + +OpenTelemetry is an open source observability framework, providing a general-purpose API, SDK, and related tools required for the instrumentation of cloud-native software, frameworks, and libraries. + +OpenTelemetry provides a single set of APIs, libraries, agents, and collector services to capture distributed traces and metrics from your application. You can analyze them using Prometheus, Jaeger, and other observability tools. + +## How does this gem fit in? + +The `opentelemetry-resource-detector-aws` gem provides a means of retrieving a resource for supported environments following the resource semantic conventions. When running on AWS platforms, this detector automatically identifies and populates resource attributes with relevant metadata from the environment. + +## Installation + +Install the gem using: + +```console +gem install opentelemetry-sdk +gem install opentelemetry-resource-detector-aws +``` + +Or, if you use Bundler, include `opentelemetry-sdk` and `opentelemetry-resource-detector-aws` in your `Gemfile`. + +## Usage + +```rb +require 'opentelemetry/sdk' +require 'opentelemetry/resource/detector' + +OpenTelemetry::SDK.configure do |c| + c.resource = OpenTelemetry::Resource::Detector::AWS.detect +end +``` + +## Supported AWS Platforms + +### AWS EC2 Detector + +Populates `cloud` and `host` for processes running on Amazon EC2, including abstractions such as ECS on EC2. Notably, it does not populate anything on AWS Fargate. + +| Resource Attribute | Description | +|--------------------|-------------| +| `cloud.account.id` | Value of `accountId` from `/latest/dynamic/instance-identity/document` request | +| `cloud.availability_zone` | Value of `availabilityZone` from `/latest/dynamic/instance-identity/document` request | +| `cloud.platform` | The cloud platform. In this context, it's always "aws_ec2" | +| `cloud.provider` | The cloud provider. In this context, it's always "aws" | +| `cloud.region` | Value of `region` from `/latest/dynamic/instance-identity/document` request | +| `host.id` | Value of `instanceId` from `/latest/dynamic/instance-identity/document` request | +| `host.name` | Value of hostname from `/latest/meta-data/hostname` request | +| `host.type` | Value of `instanceType` from `/latest/dynamic/instance-identity/document` request | + +Additional AWS platforms (ECS, EKS, Lambda) will be supported in future versions. + +## License + +The `opentelemetry-resource-detector-aws` gem is distributed under the Apache 2.0 license. See LICENSE for more information. diff --git a/resources/aws/Rakefile b/resources/aws/Rakefile new file mode 100644 index 0000000000..8fc4e3c2ce --- /dev/null +++ b/resources/aws/Rakefile @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'bundler/gem_tasks' +require 'rake/testtask' +require 'yard' +require 'rubocop/rake_task' + +RuboCop::RakeTask.new + +Rake::TestTask.new :test do |t| + t.libs << 'test' + t.libs << 'lib' + t.test_files = FileList['test/**/*_test.rb'] +end + +YARD::Rake::YardocTask.new do |t| + t.stats_options = ['--list-undoc'] +end + +if RUBY_ENGINE == 'truffleruby' + task default: %i[test] +else + task default: %i[test rubocop yard] +end \ No newline at end of file diff --git a/resources/aws/lib/opentelemetry-resource-detector-aws.rb b/resources/aws/lib/opentelemetry-resource-detector-aws.rb new file mode 100644 index 0000000000..a740ef21cc --- /dev/null +++ b/resources/aws/lib/opentelemetry-resource-detector-aws.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require_relative 'opentelemetry/resource/detector' diff --git a/resources/aws/lib/opentelemetry/resource/detector.rb b/resources/aws/lib/opentelemetry/resource/detector.rb new file mode 100644 index 0000000000..40ee6985f0 --- /dev/null +++ b/resources/aws/lib/opentelemetry/resource/detector.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/sdk' +require 'opentelemetry/resource/detector/aws' +require 'opentelemetry/resource/detector/aws/version' + +# OpenTelemetry is an open source observability framework, providing a +# general-purpose API, SDK, and related tools required for the instrumentation +# of cloud-native software, frameworks, and libraries. +# +# The OpenTelemetry module provides global accessors for telemetry objects. +# See the documentation for the `opentelemetry-api` gem for details. +module OpenTelemetry + module Resource + # Detector contains the resource detectors + module Detector + end + end +end diff --git a/resources/aws/lib/opentelemetry/resource/detector/aws.rb b/resources/aws/lib/opentelemetry/resource/detector/aws.rb new file mode 100644 index 0000000000..03eb995e34 --- /dev/null +++ b/resources/aws/lib/opentelemetry/resource/detector/aws.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/resource/detector/aws/ec2' + +module OpenTelemetry + module Resource + module Detector + # AWS contains detect class method for determining AWS environment resource attributes + module AWS + extend self + + def detect + # This will be a composite of all the AWS platform detectors + ec2_resource = EC2.detect + + # For now, return the EC2 resource directly + # In the future, we'll implement detection for EC2, ECS, EKS, etc. + return ec2_resource + end + end + end + end +end diff --git a/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb b/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb new file mode 100644 index 0000000000..4bc3b14fd3 --- /dev/null +++ b/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'net/http' +require 'json' + +module OpenTelemetry + module Resource + module Detector + module AWS + # EC2 contains detect class method for determining EC2 resource attributes + module EC2 + extend self + + # EC2 metadata service endpoints and constants + EC2_METADATA_HOST = '169.254.169.254' + TOKEN_ENDPOINT = '/latest/api/token' + IDENTITY_DOCUMENT_ENDPOINT = '/latest/dynamic/instance-identity/document' + HOSTNAME_ENDPOINT = '/latest/meta-data/hostname' + + TOKEN_HEADER = 'X-aws-ec2-metadata-token' + TOKEN_TTL_HEADER = 'X-aws-ec2-metadata-token-ttl-seconds' + TOKEN_TTL_VALUE = '60' + + # Timeout in seconds for HTTP requests + HTTP_TIMEOUT = 1 + + def detect + # Placeholder for EC2 implementation + resource_attributes = {} + + begin + # Get IMDSv2 token - this will fail quickly if not on EC2 + token = fetch_token + return OpenTelemetry::SDK::Resources::Resource.create({}) if token.nil? + + # Get instance identity document which contains most metadata + identity = fetch_identity_document(token) + return OpenTelemetry::SDK::Resources::Resource.create({}) if identity.nil? + + hostname = fetch_hostname(token) + + # Set resource attributes from the identity document + resource_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_PROVIDER] = 'aws' + resource_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_PLATFORM] = 'aws_ec2' + resource_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_ACCOUNT_ID] = identity['accountId'] + resource_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_REGION] = identity['region'] + resource_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_AVAILABILITY_ZONE] = identity['availabilityZone'] + + resource_attributes[OpenTelemetry::SemanticConventions::Resource::HOST_ID] = identity['instanceId'] + resource_attributes[OpenTelemetry::SemanticConventions::Resource::HOST_TYPE] = identity['instanceType'] + resource_attributes[OpenTelemetry::SemanticConventions::Resource::HOST_NAME] = hostname + rescue StandardError => e + OpenTelemetry.logger.debug("EC2 resource detection failed: #{e.message}") + return OpenTelemetry::SDK::Resources::Resource.create({}) + end + + # Filter out nil or empty values + resource_attributes.delete_if { |_key, value| value.nil? || value.empty? } + OpenTelemetry::SDK::Resources::Resource.create(resource_attributes) + end + + private + + # Fetches an IMDSv2 token from the EC2 metadata service + # + # @return [String, nil] The token or nil if the request failed + def fetch_token + uri = URI.parse("http://#{EC2_METADATA_HOST}#{TOKEN_ENDPOINT}") + request = Net::HTTP::Put.new(uri) + request[TOKEN_TTL_HEADER] = TOKEN_TTL_VALUE + + response = make_request(uri, request) + return nil unless response.is_a?(Net::HTTPSuccess) + + response.body + end + + # Fetches the instance identity document which contains EC2 instance metadata + # + # @param token [String] IMDSv2 token + # @return [Hash, nil] Parsed identity document or nil if the request failed + def fetch_identity_document(token) + uri = URI.parse("http://#{EC2_METADATA_HOST}#{IDENTITY_DOCUMENT_ENDPOINT}") + request = Net::HTTP::Get.new(uri) + request[TOKEN_HEADER] = token + + response = make_request(uri, request) + return nil unless response.is_a?(Net::HTTPSuccess) + + begin + JSON.parse(response.body) + rescue JSON::ParserError + nil + end + end + + # Fetches the EC2 instance hostname + # + # @param token [String] IMDSv2 token + # @return [String, nil] The hostname or nil if the request failed + def fetch_hostname(token) + uri = URI.parse("http://#{EC2_METADATA_HOST}#{HOSTNAME_ENDPOINT}") + request = Net::HTTP::Get.new(uri) + request[TOKEN_HEADER] = token + + response = make_request(uri, request) + return nil unless response.is_a?(Net::HTTPSuccess) + + response.body + end + + # Makes an HTTP request with timeout handling + # + # @param uri [URI] The request URI + # @param request [Net::HTTP::Request] The request to perform + # @return [Net::HTTPResponse, nil] The response or nil if the request failed + def make_request(uri, request) + http = Net::HTTP.new(uri.host, uri.port) + http.open_timeout = HTTP_TIMEOUT + http.read_timeout = HTTP_TIMEOUT + + begin + http.request(request) + rescue StandardError => e + OpenTelemetry.logger.debug("EC2 metadata service request failed: #{e.message}") + nil + end + end + end + end + end + end +end diff --git a/resources/aws/lib/opentelemetry/resource/detector/aws/version.rb b/resources/aws/lib/opentelemetry/resource/detector/aws/version.rb new file mode 100644 index 0000000000..aa487a12ed --- /dev/null +++ b/resources/aws/lib/opentelemetry/resource/detector/aws/version.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Resource + module Detector + module AWS + VERSION = '0.1.0' + end + end + end +end diff --git a/resources/aws/opentelemetry-resource-detector-aws.gemspec b/resources/aws/opentelemetry-resource-detector-aws.gemspec new file mode 100644 index 0000000000..e6418582d6 --- /dev/null +++ b/resources/aws/opentelemetry-resource-detector-aws.gemspec @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'opentelemetry/resource/detector/aws/version' + +Gem::Specification.new do |spec| + spec.name = 'opentelemetry-resource-detector-aws' + spec.version = OpenTelemetry::Resource::Detector::AWS::VERSION + spec.authors = ['OpenTelemetry Authors'] + spec.email = ['cncf-opentelemetry-contributors@lists.cncf.io'] + + spec.summary = 'AWS resource detector for OpenTelemetry' + spec.description = 'AWS resource detector for OpenTelemetry' + spec.homepage = 'https://github.com/open-telemetry/opentelemetry-ruby-contrib' + spec.license = 'Apache-2.0' + + spec.files = Dir.glob('lib/**/*.rb') + + ['LICENSE', 'README.md'] + spec.require_paths = ['lib'] + spec.required_ruby_version = '>= 2.5.0' + + spec.add_dependency 'opentelemetry-sdk', '~> 1.0' + + spec.add_development_dependency 'bundler', '>= 1.17' + spec.add_development_dependency 'minitest', '~> 5.0' + spec.add_development_dependency 'rake', '~> 12.0' + spec.add_development_dependency 'rubocop', '~> 1.42.0' + spec.add_development_dependency 'simplecov', '~> 0.17' + spec.add_development_dependency 'webmock', '~> 3.7' + spec.add_development_dependency 'yard', '~> 0.9' +end diff --git a/resources/aws/test/opentelemetry/resource/detector/aws/ec2_test.rb b/resources/aws/test/opentelemetry/resource/detector/aws/ec2_test.rb new file mode 100644 index 0000000000..2088492627 --- /dev/null +++ b/resources/aws/test/opentelemetry/resource/detector/aws/ec2_test.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Resource::Detector::AWS::EC2 do + let(:detector) { OpenTelemetry::Resource::Detector::AWS::EC2 } + let(:ec2_metadata_host) { '169.254.169.254' } + let(:token_path) { '/latest/api/token' } + let(:identity_document_path) { '/latest/dynamic/instance-identity/document' } + let(:hostname_path) { '/latest/meta-data/hostname' } + + let(:mock_token) { 'mock-token-123456' } + let(:mock_identity_document) do + { + accountId: '123456789012', + architecture: 'x86_64', + availabilityZone: 'mock-west-2a', + billingProducts: nil, + devpayProductCodes: nil, + marketplaceProductCodes: nil, + imageId: 'ami-0957cee1854021123', + instanceId: 'i-1234ab56cd7e89f01', + instanceType: 't2.micro-mock', + kernelId: nil, + pendingTime: '2021-07-13T21:53:41Z', + privateIp: '172.12.34.567', + ramdiskId: nil, + region: 'mock-west-2', + version: '2017-09-30' + } + end + let(:mock_hostname) { 'ip-172-12-34-567.mock-west-2.compute.internal' } + + describe '.detect' do + let(:detected_resource) { detector.detect } + let(:detected_resource_attributes) { detected_resource.attribute_enumerator.to_h } + + before do + WebMock.disable_net_connect! + end + + after do + WebMock.allow_net_connect! + end + + describe 'when running on EC2 with successful responses' do + before do + # Stub token request + stub_request(:put, "http://#{ec2_metadata_host}#{token_path}") + .with(headers: { 'X-aws-ec2-metadata-token-ttl-seconds' => '60' }) + .to_return(status: 200, body: mock_token) + + # Stub identity document request + stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") + .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) + .to_return(status: 200, body: mock_identity_document.to_json) + + # Stub hostname request + stub_request(:get, "http://#{ec2_metadata_host}#{hostname_path}") + .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) + .to_return(status: 200, body: mock_hostname) + end + + let(:expected_resource_attributes) do + { + 'cloud.provider' => 'aws', + 'cloud.platform' => 'aws_ec2', + 'cloud.account.id' => '123456789012', + 'cloud.region' => 'mock-west-2', + 'cloud.availability_zone' => 'mock-west-2a', + 'host.id' => 'i-1234ab56cd7e89f01', + 'host.type' => 't2.micro-mock', + 'host.name' => 'ip-172-12-34-567.mock-west-2.compute.internal' + } + end + + it 'returns a resource with EC2 attributes' do + _(detected_resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + _(detected_resource_attributes).must_equal(expected_resource_attributes) + end + end + + describe 'when token request fails' do + before do + # Simulate connection timeout + stub_request(:put, "http://#{ec2_metadata_host}#{token_path}") + .to_timeout + end + + it 'returns an empty resource' do + _(detected_resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + _(detected_resource_attributes).must_equal({}) + end + end + + describe 'when token request returns error code' do + before do + # Simulate 403 error + stub_request(:put, "http://#{ec2_metadata_host}#{token_path}") + .to_return(status: 403, body: 'Forbidden') + end + + it 'returns an empty resource' do + _(detected_resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + _(detected_resource_attributes).must_equal({}) + end + end + + describe 'when identity document request fails' do + before do + # Successful token request + stub_request(:put, "http://#{ec2_metadata_host}#{token_path}") + .with(headers: { 'X-aws-ec2-metadata-token-ttl-seconds' => '60' }) + .to_return(status: 200, body: mock_token) + + # Identity document request fails + stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") + .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) + .to_return(status: 500, body: 'Internal Server Error') + end + + it 'returns an empty resource' do + _(detected_resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + _(detected_resource_attributes).must_equal({}) + end + end + + describe 'when identity document is not valid JSON' do + before do + # Successful token request + stub_request(:put, "http://#{ec2_metadata_host}#{token_path}") + .with(headers: { 'X-aws-ec2-metadata-token-ttl-seconds' => '60' }) + .to_return(status: 200, body: mock_token) + + # Identity document is invalid JSON + stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") + .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) + .to_return(status: 200, body: '{not valid json') + end + + it 'returns an empty resource' do + _(detected_resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + _(detected_resource_attributes).must_equal({}) + end + end + + describe 'when hostname request fails' do + before do + # Successful token request + stub_request(:put, "http://#{ec2_metadata_host}#{token_path}") + .with(headers: { 'X-aws-ec2-metadata-token-ttl-seconds' => '60' }) + .to_return(status: 200, body: mock_token) + + # Successful identity document request + stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") + .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) + .to_return(status: 200, body: mock_identity_document.to_json) + + # Hostname request times out + stub_request(:get, "http://#{ec2_metadata_host}#{hostname_path}") + .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) + .to_timeout + end + + let(:expected_resource_attributes) do + { + 'cloud.provider' => 'aws', + 'cloud.platform' => 'aws_ec2', + 'cloud.account.id' => '123456789012', + 'cloud.region' => 'mock-west-2', + 'cloud.availability_zone' => 'mock-west-2a', + 'host.id' => 'i-1234ab56cd7e89f01', + 'host.type' => 't2.micro-mock', + # host.name is missing because the request failed + } + end + + it 'returns a resource without the hostname' do + _(detected_resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + _(detected_resource_attributes).must_equal(expected_resource_attributes) + 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 new file mode 100644 index 0000000000..9b56a71944 --- /dev/null +++ b/resources/aws/test/opentelemetry/resource/detector/aws_test.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Resource::Detector::AWS do + let(:detector) { OpenTelemetry::Resource::Detector::AWS } + + describe '.detect' do + before do + WebMock.disable_net_connect! + # You'll add stubs for AWS endpoints here + stub_request(:put, "http://169.254.169.254/latest/api/token"). + with( + headers: { + 'Accept'=>'*/*', + 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', + 'Host'=>'169.254.169.254', + 'User-Agent'=>'Ruby', + 'X-Aws-Ec2-Metadata-Token-Ttl-Seconds'=>'60' + }). + to_return(status: 404, body: "Not Found", headers: {}) + end + + after do + WebMock.allow_net_connect! + end + + let(:detected_resource) { detector.detect } + let(:detected_resource_attributes) { detected_resource.attribute_enumerator.to_h } + let(:expected_resource_attributes) { {} } + + it 'returns an empty resource' do + _(detected_resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + _(detected_resource_attributes).must_equal(expected_resource_attributes) + end + end +end diff --git a/resources/aws/test/test_helper.rb b/resources/aws/test/test_helper.rb new file mode 100644 index 0000000000..2e1595479f --- /dev/null +++ b/resources/aws/test/test_helper.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'simplecov' +require 'bundler/setup' +Bundler.require(:default, :development, :test) + +require 'opentelemetry-resource-detector-aws' +require 'minitest/autorun' +require 'webmock/minitest' + +OpenTelemetry.logger = Logger.new($stderr, level: ENV.fetch('OTEL_LOG_LEVEL', 'fatal').to_sym) \ No newline at end of file From fdbe3a159a786d6d2a77b84941dbaa410f746e96 Mon Sep 17 00:00:00 2001 From: yiyuanh Date: Wed, 26 Mar 2025 09:40:35 -0700 Subject: [PATCH 2/6] fix ci and address comments --- resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb | 5 ++++- resources/aws/opentelemetry-resource-detector-aws.gemspec | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb b/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb index 4bc3b14fd3..fa3add1ee3 100644 --- a/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb +++ b/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb @@ -6,6 +6,7 @@ require 'net/http' require 'json' +require 'opentelemetry/common' module OpenTelemetry module Resource @@ -124,7 +125,9 @@ def make_request(uri, request) http.read_timeout = HTTP_TIMEOUT begin - http.request(request) + OpenTelemetry::Common::Utilities.untraced do + http.request(request) + end rescue StandardError => e OpenTelemetry.logger.debug("EC2 metadata service request failed: #{e.message}") nil diff --git a/resources/aws/opentelemetry-resource-detector-aws.gemspec b/resources/aws/opentelemetry-resource-detector-aws.gemspec index e6418582d6..2a03481707 100644 --- a/resources/aws/opentelemetry-resource-detector-aws.gemspec +++ b/resources/aws/opentelemetry-resource-detector-aws.gemspec @@ -25,6 +25,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.5.0' spec.add_dependency 'opentelemetry-sdk', '~> 1.0' + spec.add_dependency 'base64', '>= 0.1.0' spec.add_development_dependency 'bundler', '>= 1.17' spec.add_development_dependency 'minitest', '~> 5.0' From f11d75d9d09119659ff6371ae2272338ae036795 Mon Sep 17 00:00:00 2001 From: yiyuanh Date: Wed, 26 Mar 2025 10:37:19 -0700 Subject: [PATCH 3/6] ci fix pt.2 --- resources/aws/Gemfile | 19 ++++++++++++------- resources/aws/README.md | 1 + ...pentelemetry-resource-detector-aws.gemspec | 1 - 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/resources/aws/Gemfile b/resources/aws/Gemfile index 19b8a7bce2..62260f7999 100644 --- a/resources/aws/Gemfile +++ b/resources/aws/Gemfile @@ -9,12 +9,17 @@ source 'https://rubygems.org' gemspec group :test do + gem 'bundler', '~> 2.4' gem 'minitest', '~> 5.0' - gem 'simplecov', '~> 0.17.1' - gem 'webmock', '~> 3.7.0' -end - -group :development do - gem 'rubocop', '~> 1.42.0' + gem 'rake', '~> 13.0' + gem 'rubocop', '~> 1.73.2' + gem 'rubocop-performance', '~> 1.24.0' + gem 'simplecov', '~> 0.22.0' + gem 'webmock', '~> 3.24' gem 'yard', '~> 0.9' -end + + if RUBY_VERSION >= '3.4' + gem 'base64' + gem 'mutex_m' + end +end \ No newline at end of file diff --git a/resources/aws/README.md b/resources/aws/README.md index 66edaad35f..1bb168ca36 100644 --- a/resources/aws/README.md +++ b/resources/aws/README.md @@ -38,6 +38,7 @@ end ### AWS EC2 Detector + Populates `cloud` and `host` for processes running on Amazon EC2, including abstractions such as ECS on EC2. Notably, it does not populate anything on AWS Fargate. | Resource Attribute | Description | diff --git a/resources/aws/opentelemetry-resource-detector-aws.gemspec b/resources/aws/opentelemetry-resource-detector-aws.gemspec index 2a03481707..e6418582d6 100644 --- a/resources/aws/opentelemetry-resource-detector-aws.gemspec +++ b/resources/aws/opentelemetry-resource-detector-aws.gemspec @@ -25,7 +25,6 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.5.0' spec.add_dependency 'opentelemetry-sdk', '~> 1.0' - spec.add_dependency 'base64', '>= 0.1.0' spec.add_development_dependency 'bundler', '>= 1.17' spec.add_development_dependency 'minitest', '~> 5.0' From 58e6eb41a54eff3039a958a15600808b10e88e12 Mon Sep 17 00:00:00 2001 From: yiyuanh Date: Wed, 26 Mar 2025 19:38:00 -0700 Subject: [PATCH 4/6] fix rubocop offenses --- resources/aws/Gemfile | 2 +- resources/aws/Rakefile | 2 +- .../opentelemetry/resource/detector/aws.rb | 3 +-- .../resource/detector/aws/ec2.rb | 4 ++-- ...pentelemetry-resource-detector-aws.gemspec | 8 -------- .../resource/detector/aws/ec2_test.rb | 16 ++++++++-------- .../resource/detector/aws_test.rb | 19 ++++++++++--------- resources/aws/test/test_helper.rb | 2 +- 8 files changed, 24 insertions(+), 32 deletions(-) diff --git a/resources/aws/Gemfile b/resources/aws/Gemfile index 62260f7999..1f0af4df2e 100644 --- a/resources/aws/Gemfile +++ b/resources/aws/Gemfile @@ -22,4 +22,4 @@ group :test do gem 'base64' gem 'mutex_m' end -end \ No newline at end of file +end diff --git a/resources/aws/Rakefile b/resources/aws/Rakefile index 8fc4e3c2ce..1a64ba842e 100644 --- a/resources/aws/Rakefile +++ b/resources/aws/Rakefile @@ -25,4 +25,4 @@ if RUBY_ENGINE == 'truffleruby' task default: %i[test] else task default: %i[test rubocop yard] -end \ No newline at end of file +end diff --git a/resources/aws/lib/opentelemetry/resource/detector/aws.rb b/resources/aws/lib/opentelemetry/resource/detector/aws.rb index 03eb995e34..ac5060b07c 100644 --- a/resources/aws/lib/opentelemetry/resource/detector/aws.rb +++ b/resources/aws/lib/opentelemetry/resource/detector/aws.rb @@ -15,11 +15,10 @@ module AWS def detect # This will be a composite of all the AWS platform detectors - ec2_resource = EC2.detect + EC2.detect # For now, return the EC2 resource directly # In the future, we'll implement detection for EC2, ECS, EKS, etc. - return ec2_resource end end end diff --git a/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb b/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb index fa3add1ee3..111ec8e849 100644 --- a/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb +++ b/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb @@ -50,7 +50,7 @@ def detect resource_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_ACCOUNT_ID] = identity['accountId'] resource_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_REGION] = identity['region'] resource_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_AVAILABILITY_ZONE] = identity['availabilityZone'] - + resource_attributes[OpenTelemetry::SemanticConventions::Resource::HOST_ID] = identity['instanceId'] resource_attributes[OpenTelemetry::SemanticConventions::Resource::HOST_TYPE] = identity['instanceType'] resource_attributes[OpenTelemetry::SemanticConventions::Resource::HOST_NAME] = hostname @@ -65,7 +65,7 @@ def detect end private - + # Fetches an IMDSv2 token from the EC2 metadata service # # @return [String, nil] The token or nil if the request failed diff --git a/resources/aws/opentelemetry-resource-detector-aws.gemspec b/resources/aws/opentelemetry-resource-detector-aws.gemspec index e6418582d6..8ca4c6a688 100644 --- a/resources/aws/opentelemetry-resource-detector-aws.gemspec +++ b/resources/aws/opentelemetry-resource-detector-aws.gemspec @@ -25,12 +25,4 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.5.0' spec.add_dependency 'opentelemetry-sdk', '~> 1.0' - - spec.add_development_dependency 'bundler', '>= 1.17' - spec.add_development_dependency 'minitest', '~> 5.0' - spec.add_development_dependency 'rake', '~> 12.0' - spec.add_development_dependency 'rubocop', '~> 1.42.0' - spec.add_development_dependency 'simplecov', '~> 0.17' - spec.add_development_dependency 'webmock', '~> 3.7' - spec.add_development_dependency 'yard', '~> 0.9' end diff --git a/resources/aws/test/opentelemetry/resource/detector/aws/ec2_test.rb b/resources/aws/test/opentelemetry/resource/detector/aws/ec2_test.rb index 2088492627..a75bde58bc 100644 --- a/resources/aws/test/opentelemetry/resource/detector/aws/ec2_test.rb +++ b/resources/aws/test/opentelemetry/resource/detector/aws/ec2_test.rb @@ -12,7 +12,7 @@ let(:token_path) { '/latest/api/token' } let(:identity_document_path) { '/latest/dynamic/instance-identity/document' } let(:hostname_path) { '/latest/meta-data/hostname' } - + let(:mock_token) { 'mock-token-123456' } let(:mock_identity_document) do { @@ -53,12 +53,12 @@ stub_request(:put, "http://#{ec2_metadata_host}#{token_path}") .with(headers: { 'X-aws-ec2-metadata-token-ttl-seconds' => '60' }) .to_return(status: 200, body: mock_token) - + # Stub identity document request stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) .to_return(status: 200, body: mock_identity_document.to_json) - + # Stub hostname request stub_request(:get, "http://#{ec2_metadata_host}#{hostname_path}") .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) @@ -116,7 +116,7 @@ stub_request(:put, "http://#{ec2_metadata_host}#{token_path}") .with(headers: { 'X-aws-ec2-metadata-token-ttl-seconds' => '60' }) .to_return(status: 200, body: mock_token) - + # Identity document request fails stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) @@ -135,7 +135,7 @@ stub_request(:put, "http://#{ec2_metadata_host}#{token_path}") .with(headers: { 'X-aws-ec2-metadata-token-ttl-seconds' => '60' }) .to_return(status: 200, body: mock_token) - + # Identity document is invalid JSON stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) @@ -154,12 +154,12 @@ stub_request(:put, "http://#{ec2_metadata_host}#{token_path}") .with(headers: { 'X-aws-ec2-metadata-token-ttl-seconds' => '60' }) .to_return(status: 200, body: mock_token) - + # Successful identity document request stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) .to_return(status: 200, body: mock_identity_document.to_json) - + # Hostname request times out stub_request(:get, "http://#{ec2_metadata_host}#{hostname_path}") .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) @@ -174,7 +174,7 @@ 'cloud.region' => 'mock-west-2', 'cloud.availability_zone' => 'mock-west-2a', 'host.id' => 'i-1234ab56cd7e89f01', - 'host.type' => 't2.micro-mock', + 'host.type' => 't2.micro-mock' # host.name is missing because the request failed } end diff --git a/resources/aws/test/opentelemetry/resource/detector/aws_test.rb b/resources/aws/test/opentelemetry/resource/detector/aws_test.rb index 9b56a71944..f26c04eb74 100644 --- a/resources/aws/test/opentelemetry/resource/detector/aws_test.rb +++ b/resources/aws/test/opentelemetry/resource/detector/aws_test.rb @@ -13,16 +13,17 @@ before do WebMock.disable_net_connect! # You'll add stubs for AWS endpoints here - stub_request(:put, "http://169.254.169.254/latest/api/token"). - with( + stub_request(:put, 'http://169.254.169.254/latest/api/token') + .with( headers: { - 'Accept'=>'*/*', - 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', - 'Host'=>'169.254.169.254', - 'User-Agent'=>'Ruby', - 'X-Aws-Ec2-Metadata-Token-Ttl-Seconds'=>'60' - }). - to_return(status: 404, body: "Not Found", headers: {}) + 'Accept' => '*/*', + 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', + 'Host' => '169.254.169.254', + 'User-Agent' => 'Ruby', + 'X-Aws-Ec2-Metadata-Token-Ttl-Seconds' => '60' + } + ) + .to_return(status: 404, body: 'Not Found', headers: {}) end after do diff --git a/resources/aws/test/test_helper.rb b/resources/aws/test/test_helper.rb index 2e1595479f..7d5b4d5bdd 100644 --- a/resources/aws/test/test_helper.rb +++ b/resources/aws/test/test_helper.rb @@ -12,4 +12,4 @@ require 'minitest/autorun' require 'webmock/minitest' -OpenTelemetry.logger = Logger.new($stderr, level: ENV.fetch('OTEL_LOG_LEVEL', 'fatal').to_sym) \ No newline at end of file +OpenTelemetry.logger = Logger.new($stderr, level: ENV.fetch('OTEL_LOG_LEVEL', 'fatal').to_sym) From e064eeee23361c11a8e8ee62d375d6f642b066e9 Mon Sep 17 00:00:00 2001 From: yiyuanh Date: Fri, 28 Mar 2025 09:56:19 -0700 Subject: [PATCH 5/6] add fallback mechanism in IMDSv1 case --- .../resource/detector/aws/ec2.rb | 19 +++-- .../resource/detector/aws/ec2_test.rb | 85 +++++++++++++++++-- .../resource/detector/aws_test.rb | 19 ++--- 3 files changed, 94 insertions(+), 29 deletions(-) diff --git a/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb b/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb index 111ec8e849..f59b99af8a 100644 --- a/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb +++ b/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb @@ -30,17 +30,18 @@ module EC2 HTTP_TIMEOUT = 1 def detect - # Placeholder for EC2 implementation + # Implementation for EC2 detection supporting both IMDSv1 and IMDSv2 resource_attributes = {} begin - # Get IMDSv2 token - this will fail quickly if not on EC2 + # Attempt to get IMDSv2 token - this will fail if IMDSv2 is not supported + # but we'll still try IMDSv1 in that case token = fetch_token - return OpenTelemetry::SDK::Resources::Resource.create({}) if token.nil? # Get instance identity document which contains most metadata - identity = fetch_identity_document(token) - return OpenTelemetry::SDK::Resources::Resource.create({}) if identity.nil? + # Will try with token (IMDSv2) or without token (IMDSv1) + identity = fetch_identity_document(token) || {} + return OpenTelemetry::SDK::Resources::Resource.create({}) if identity.empty? hostname = fetch_hostname(token) @@ -82,12 +83,12 @@ def fetch_token # Fetches the instance identity document which contains EC2 instance metadata # - # @param token [String] IMDSv2 token + # @param token [String, nil] IMDSv2 token (optional for IMDSv1) # @return [Hash, nil] Parsed identity document or nil if the request failed def fetch_identity_document(token) uri = URI.parse("http://#{EC2_METADATA_HOST}#{IDENTITY_DOCUMENT_ENDPOINT}") request = Net::HTTP::Get.new(uri) - request[TOKEN_HEADER] = token + request[TOKEN_HEADER] = token if token response = make_request(uri, request) return nil unless response.is_a?(Net::HTTPSuccess) @@ -101,12 +102,12 @@ def fetch_identity_document(token) # Fetches the EC2 instance hostname # - # @param token [String] IMDSv2 token + # @param token [String, nil] IMDSv2 token (optional for IMDSv1) # @return [String, nil] The hostname or nil if the request failed def fetch_hostname(token) uri = URI.parse("http://#{EC2_METADATA_HOST}#{HOSTNAME_ENDPOINT}") request = Net::HTTP::Get.new(uri) - request[TOKEN_HEADER] = token + request[TOKEN_HEADER] = token if token response = make_request(uri, request) return nil unless response.is_a?(Net::HTTPSuccess) diff --git a/resources/aws/test/opentelemetry/resource/detector/aws/ec2_test.rb b/resources/aws/test/opentelemetry/resource/detector/aws/ec2_test.rb index a75bde58bc..9644d8edf9 100644 --- a/resources/aws/test/opentelemetry/resource/detector/aws/ec2_test.rb +++ b/resources/aws/test/opentelemetry/resource/detector/aws/ec2_test.rb @@ -86,27 +86,73 @@ describe 'when token request fails' do before do - # Simulate connection timeout + # Simulate connection timeout for token request (IMDSv2) stub_request(:put, "http://#{ec2_metadata_host}#{token_path}") .to_timeout + + # Stub IMDSv1 fallback request for identity document (without token) + stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") + .with(headers: { 'Accept' => '*/*' }) + .to_return(status: 200, body: mock_identity_document.to_json) + + # Stub IMDSv1 fallback request for hostname (without token) + stub_request(:get, "http://#{ec2_metadata_host}#{hostname_path}") + .with(headers: { 'Accept' => '*/*' }) + .to_return(status: 200, body: mock_hostname) end - it 'returns an empty resource' do + let(:expected_resource_attributes) do + { + 'cloud.provider' => 'aws', + 'cloud.platform' => 'aws_ec2', + 'cloud.account.id' => '123456789012', + 'cloud.region' => 'mock-west-2', + 'cloud.availability_zone' => 'mock-west-2a', + 'host.id' => 'i-1234ab56cd7e89f01', + 'host.type' => 't2.micro-mock', + 'host.name' => 'ip-172-12-34-567.mock-west-2.compute.internal' + } + end + + it 'falls back to IMDSv1 and returns a resource with EC2 attributes' do _(detected_resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) - _(detected_resource_attributes).must_equal({}) + _(detected_resource_attributes).must_equal(expected_resource_attributes) end end describe 'when token request returns error code' do before do - # Simulate 403 error + # Simulate 403 error for token request stub_request(:put, "http://#{ec2_metadata_host}#{token_path}") .to_return(status: 403, body: 'Forbidden') + + # Stub IMDSv1 fallback request for identity document (without token) + stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") + .with(headers: { 'Accept' => '*/*' }) + .to_return(status: 200, body: mock_identity_document.to_json) + + # Stub IMDSv1 fallback request for hostname (without token) + stub_request(:get, "http://#{ec2_metadata_host}#{hostname_path}") + .with(headers: { 'Accept' => '*/*' }) + .to_return(status: 200, body: mock_hostname) end - it 'returns an empty resource' do + let(:expected_resource_attributes) do + { + 'cloud.provider' => 'aws', + 'cloud.platform' => 'aws_ec2', + 'cloud.account.id' => '123456789012', + 'cloud.region' => 'mock-west-2', + 'cloud.availability_zone' => 'mock-west-2a', + 'host.id' => 'i-1234ab56cd7e89f01', + 'host.type' => 't2.micro-mock', + 'host.name' => 'ip-172-12-34-567.mock-west-2.compute.internal' + } + end + + it 'falls back to IMDSv1 and returns a resource with EC2 attributes' do _(detected_resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) - _(detected_resource_attributes).must_equal({}) + _(detected_resource_attributes).must_equal(expected_resource_attributes) end end @@ -117,10 +163,15 @@ .with(headers: { 'X-aws-ec2-metadata-token-ttl-seconds' => '60' }) .to_return(status: 200, body: mock_token) - # Identity document request fails + # Identity document request with token fails (IMDSv2) stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) .to_return(status: 500, body: 'Internal Server Error') + + # Identity document request without token also fails (IMDSv1 fallback) + stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") + .with(headers: { 'Accept' => '*/*' }) + .to_return(status: 500, body: 'Internal Server Error') end it 'returns an empty resource' do @@ -136,10 +187,15 @@ .with(headers: { 'X-aws-ec2-metadata-token-ttl-seconds' => '60' }) .to_return(status: 200, body: mock_token) - # Identity document is invalid JSON + # Identity document is invalid JSON (IMDSv2) stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) .to_return(status: 200, body: '{not valid json') + + # Identity document is also invalid JSON when accessed via IMDSv1 + stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") + .with(headers: { 'Accept' => '*/*' }) + .to_return(status: 200, body: '{not valid json') end it 'returns an empty resource' do @@ -155,15 +211,26 @@ .with(headers: { 'X-aws-ec2-metadata-token-ttl-seconds' => '60' }) .to_return(status: 200, body: mock_token) - # Successful identity document request + # Successful identity document request with IMDSv2 stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) .to_return(status: 200, body: mock_identity_document.to_json) + # Also stub the IMDSv1 fallback for identity document (without token) + # This ensures we don't get unexpected requests even if code paths change + stub_request(:get, "http://#{ec2_metadata_host}#{identity_document_path}") + .with(headers: { 'Accept' => '*/*' }) + .to_return(status: 200, body: mock_identity_document.to_json) + # Hostname request times out stub_request(:get, "http://#{ec2_metadata_host}#{hostname_path}") .with(headers: { 'X-aws-ec2-metadata-token' => mock_token }) .to_timeout + + # Also stub the IMDSv1 fallback for hostname (without token) + stub_request(:get, "http://#{ec2_metadata_host}#{hostname_path}") + .with(headers: { 'Accept' => '*/*' }) + .to_timeout end let(:expected_resource_attributes) do diff --git a/resources/aws/test/opentelemetry/resource/detector/aws_test.rb b/resources/aws/test/opentelemetry/resource/detector/aws_test.rb index f26c04eb74..549e55a436 100644 --- a/resources/aws/test/opentelemetry/resource/detector/aws_test.rb +++ b/resources/aws/test/opentelemetry/resource/detector/aws_test.rb @@ -12,18 +12,15 @@ describe '.detect' do before do WebMock.disable_net_connect! - # You'll add stubs for AWS endpoints here + # Ensure we stub any potential requests to EC2 metadata service + # Simulate failed token request stub_request(:put, 'http://169.254.169.254/latest/api/token') - .with( - headers: { - 'Accept' => '*/*', - 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', - 'Host' => '169.254.169.254', - 'User-Agent' => 'Ruby', - 'X-Aws-Ec2-Metadata-Token-Ttl-Seconds' => '60' - } - ) - .to_return(status: 404, body: 'Not Found', headers: {}) + .to_timeout + + # Simulate failed identity document request + stub_request(:get, 'http://169.254.169.254/latest/dynamic/instance-identity/document') + .with(headers: { 'Accept' => '*/*' }) + .to_return(status: 404, body: 'Not Found') end after do From 224417daa1751dde606c392ff46e0eef96a12e55 Mon Sep 17 00:00:00 2001 From: yiyuanh Date: Fri, 4 Apr 2025 15:36:44 -0700 Subject: [PATCH 6/6] update versions and add constant for otel resource prefix --- resources/aws/Gemfile | 2 +- .../resource/detector/aws/ec2.rb | 21 +++++++++++-------- .../resource/detector/aws/version.rb | 2 +- ...pentelemetry-resource-detector-aws.gemspec | 2 +- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/resources/aws/Gemfile b/resources/aws/Gemfile index 1f0af4df2e..b55c4be964 100644 --- a/resources/aws/Gemfile +++ b/resources/aws/Gemfile @@ -12,7 +12,7 @@ group :test do gem 'bundler', '~> 2.4' gem 'minitest', '~> 5.0' gem 'rake', '~> 13.0' - gem 'rubocop', '~> 1.73.2' + gem 'rubocop', '~> 1.75.2' gem 'rubocop-performance', '~> 1.24.0' gem 'simplecov', '~> 0.22.0' gem 'webmock', '~> 3.24' diff --git a/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb b/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb index f59b99af8a..a83e28bcb7 100644 --- a/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb +++ b/resources/aws/lib/opentelemetry/resource/detector/aws/ec2.rb @@ -29,6 +29,9 @@ module EC2 # Timeout in seconds for HTTP requests HTTP_TIMEOUT = 1 + # Create a constant for resource semantic conventions + RESOURCE = OpenTelemetry::SemanticConventions::Resource + def detect # Implementation for EC2 detection supporting both IMDSv1 and IMDSv2 resource_attributes = {} @@ -46,15 +49,15 @@ def detect hostname = fetch_hostname(token) # Set resource attributes from the identity document - resource_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_PROVIDER] = 'aws' - resource_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_PLATFORM] = 'aws_ec2' - resource_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_ACCOUNT_ID] = identity['accountId'] - resource_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_REGION] = identity['region'] - resource_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_AVAILABILITY_ZONE] = identity['availabilityZone'] - - resource_attributes[OpenTelemetry::SemanticConventions::Resource::HOST_ID] = identity['instanceId'] - resource_attributes[OpenTelemetry::SemanticConventions::Resource::HOST_TYPE] = identity['instanceType'] - resource_attributes[OpenTelemetry::SemanticConventions::Resource::HOST_NAME] = hostname + resource_attributes[RESOURCE::CLOUD_PROVIDER] = 'aws' + resource_attributes[RESOURCE::CLOUD_PLATFORM] = 'aws_ec2' + resource_attributes[RESOURCE::CLOUD_ACCOUNT_ID] = identity['accountId'] + resource_attributes[RESOURCE::CLOUD_REGION] = identity['region'] + resource_attributes[RESOURCE::CLOUD_AVAILABILITY_ZONE] = identity['availabilityZone'] + + resource_attributes[RESOURCE::HOST_ID] = identity['instanceId'] + resource_attributes[RESOURCE::HOST_TYPE] = identity['instanceType'] + resource_attributes[RESOURCE::HOST_NAME] = hostname rescue StandardError => e OpenTelemetry.logger.debug("EC2 resource detection failed: #{e.message}") return OpenTelemetry::SDK::Resources::Resource.create({}) diff --git a/resources/aws/lib/opentelemetry/resource/detector/aws/version.rb b/resources/aws/lib/opentelemetry/resource/detector/aws/version.rb index aa487a12ed..6700b50f05 100644 --- a/resources/aws/lib/opentelemetry/resource/detector/aws/version.rb +++ b/resources/aws/lib/opentelemetry/resource/detector/aws/version.rb @@ -8,7 +8,7 @@ module OpenTelemetry module Resource module Detector module AWS - VERSION = '0.1.0' + VERSION = '0.0.0' end end end diff --git a/resources/aws/opentelemetry-resource-detector-aws.gemspec b/resources/aws/opentelemetry-resource-detector-aws.gemspec index 8ca4c6a688..591a6627f9 100644 --- a/resources/aws/opentelemetry-resource-detector-aws.gemspec +++ b/resources/aws/opentelemetry-resource-detector-aws.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |spec| spec.files = Dir.glob('lib/**/*.rb') + ['LICENSE', 'README.md'] spec.require_paths = ['lib'] - spec.required_ruby_version = '>= 2.5.0' + spec.required_ruby_version = ">= #{File.read(File.expand_path('../../gemspecs/RUBY_REQUIREMENT', __dir__))}" spec.add_dependency 'opentelemetry-sdk', '~> 1.0' end