From 303bbc828ea797a57bdc03abe8eb5d2fb035b4af Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Thu, 12 Jun 2025 13:57:55 -0400 Subject: [PATCH 1/7] NH-113148: 7.0.0 release --- .github/workflows/run_unit_tests.yml | 10 +- gemfiles/test_gems.gemfile | 2 +- init.rb | 9 - .../templates/solarwinds_apm_initializer.rb | 31 +--- lib/solarwinds_apm/config.rb | 6 + lib/solarwinds_apm/support.rb | 1 - .../support/aws_resource_detector.rb | 12 -- .../support/resource_detector.rb | 2 +- .../resource_detector/aws/beanstalk.rb | 51 ----- .../support/resource_detector/aws/ec2.rb | 145 --------------- .../support/resource_detector/aws/ecs.rb | 173 ----------------- .../support/resource_detector/aws/eks.rb | 174 ------------------ .../support/resource_detector/aws/lambda.rb | 66 ------- lib/solarwinds_apm/version.rb | 2 +- solarwinds_apm.gemspec | 7 +- test/test_setup.sh | 9 +- test/tmp.cert | 3 - 17 files changed, 21 insertions(+), 682 deletions(-) delete mode 100644 init.rb delete mode 100644 lib/solarwinds_apm/support/aws_resource_detector.rb delete mode 100644 lib/solarwinds_apm/support/resource_detector/aws/beanstalk.rb delete mode 100644 lib/solarwinds_apm/support/resource_detector/aws/ec2.rb delete mode 100644 lib/solarwinds_apm/support/resource_detector/aws/ecs.rb delete mode 100644 lib/solarwinds_apm/support/resource_detector/aws/eks.rb delete mode 100644 lib/solarwinds_apm/support/resource_detector/aws/lambda.rb delete mode 100644 test/tmp.cert diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index e28ca595..9dde86a8 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -21,17 +21,17 @@ jobs: strategy: fail-fast: false matrix: - ruby: ['3.2.2','3.1.0','3.0.6','2.7.5'] + ruby: ['3.4.2', '3.3.2', '3.2.2','3.1.0'] os: ['bullseye'] include: + - ruby: '3.4.2' + os: 'alpine3.21' + - ruby: '3.3.3' + os: 'alpine3.20' - ruby: '3.2.2' os: 'alpine3.17' - ruby: '3.1.0' os: 'alpine3.15' - - ruby: '3.0.6' - os: 'alpine3.16' - - ruby: '2.7.5' - os: 'alpine3.15' container: image: ruby:${{ matrix.ruby }}-${{ matrix.os }} diff --git a/gemfiles/test_gems.gemfile b/gemfiles/test_gems.gemfile index 87ce4359..58a3d5b5 100644 --- a/gemfiles/test_gems.gemfile +++ b/gemfiles/test_gems.gemfile @@ -35,6 +35,7 @@ group :development, :test do gem 'simplecov-console' gem 'webmock' if RUBY_VERSION >= '2.0.0' gem 'grpc', '~> 1.71' + gem 'base64' if RUBY_VERSION >= '3.4.0' gem 'opentelemetry-sdk' gem 'opentelemetry-instrumentation-all' @@ -45,7 +46,6 @@ group :development, :test do gem 'opentelemetry-test-helpers' gem 'opentelemetry-resource-detector-azure' - # aws resource detector requires ruby >= 3.1.0 gem 'opentelemetry-resource-detector-aws' if RUBY_VERSION >= '3.1.0' gem 'opentelemetry-resource-detector-container' end diff --git a/init.rb b/init.rb deleted file mode 100644 index fbe235e9..00000000 --- a/init.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -# © 2023 SolarWinds Worldwide, LLC. All rights reserved. -# -# 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. - -require 'solarwinds_apm' diff --git a/lib/rails/generators/solarwinds_apm/templates/solarwinds_apm_initializer.rb b/lib/rails/generators/solarwinds_apm/templates/solarwinds_apm_initializer.rb index c850965b..d2a48d73 100644 --- a/lib/rails/generators/solarwinds_apm/templates/solarwinds_apm_initializer.rb +++ b/lib/rails/generators/solarwinds_apm/templates/solarwinds_apm_initializer.rb @@ -16,8 +16,7 @@ if defined?(SolarWindsAPM::Config) - # :service_key, :hostname_alias, :http_proxy, and :debug_level - # are startup settings and can't be changed afterwards. + # :service_key and :debug_level are startup settings and can't be changed afterwards. # # Set SW_APM_SERVICE_KEY @@ -29,34 +28,6 @@ # # SolarWindsAPM::Config[:service_key] = '0123456789abcde0123456789abcde0123456789abcde0123456789abcde1234:my_service' - # - # Set SW_APM_HOSTNAME_ALIAS - # This setting will be overridden if SW_APM_HOSTNAME_ALIAS is set as an environment variable - # - # SolarWindsAPM::Config[:hostname_alias] = 'alias_name' - - # - # Set Proxy for SolarWinds - # This setting will be overridden if SW_APM_PROXY is set as an environment variable. - # - # Please configure http_proxy if a proxy needs to be used to communicate with - # the SolarWinds APM collector. - # The format should either be http://: for a proxy - # server that does not require authentication, or - # http://:@: for a proxy server that - # requires basic authentication. - # - # Note that while HTTP is the only type of connection supported, the traffic - # to SolarWinds is still encrypted using SSL/TLS. - # - # It is recommended to configure the proxy in this file or as SW_APM_PROXY - # environment variable. However, the agent's underlying network library will - # use a system-wide proxy defined in the environment variables grpc_proxy, - # https_proxy or http_proxy if no SolarWindsAPM-specific configuration is set. - # Please refer to gRPC environment variables for more information. - # - # SolarWindsAPM::Config[:http_proxy] = http://: - # # Set SW_APM_DEBUG_LEVEL # This setting will be overridden if SW_APM_DEBUG_LEVEL is set as an environment variable. diff --git a/lib/solarwinds_apm/config.rb b/lib/solarwinds_apm/config.rb index 080fc635..fda6f965 100644 --- a/lib/solarwinds_apm/config.rb +++ b/lib/solarwinds_apm/config.rb @@ -226,6 +226,12 @@ def self.[]=(key, value) when :tag_sql enable_disable_config('SW_APM_TAG_SQL', key, value, false, bool: true) + when :http_proxy + SolarWindsAPM.logger.warn { ':http_proxy is depreciated' } + + when :hostname_alias + SolarWindsAPM.logger.warn { ':hostname_alias is depreciated' } + else @@config[key.to_sym] = value diff --git a/lib/solarwinds_apm/support.rb b/lib/solarwinds_apm/support.rb index add83e2d..13ffb4ec 100644 --- a/lib/solarwinds_apm/support.rb +++ b/lib/solarwinds_apm/support.rb @@ -16,4 +16,3 @@ require_relative 'support/utils' require_relative 'support/otlp_endpoint' require_relative 'support/resource_detector' -require_relative 'support/aws_resource_detector' diff --git a/lib/solarwinds_apm/support/aws_resource_detector.rb b/lib/solarwinds_apm/support/aws_resource_detector.rb deleted file mode 100644 index 0a07f337..00000000 --- a/lib/solarwinds_apm/support/aws_resource_detector.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -# © 2025 SolarWinds Worldwide, LLC. All rights reserved. -# -# 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. - -require_relative 'resource_detector/aws/ecs' -require_relative 'resource_detector/aws/eks' -require_relative 'resource_detector/aws/lambda' -require_relative 'resource_detector/aws/beanstalk' diff --git a/lib/solarwinds_apm/support/resource_detector.rb b/lib/solarwinds_apm/support/resource_detector.rb index a4490095..29904231 100644 --- a/lib/solarwinds_apm/support/resource_detector.rb +++ b/lib/solarwinds_apm/support/resource_detector.rb @@ -13,7 +13,7 @@ require 'securerandom' require 'opentelemetry/resource/detector/azure' require 'opentelemetry/resource/detector/container' -require_relative 'resource_detector/aws/ec2' +require 'opentelemetry/resource/detector/aws/ec2' module SolarWindsAPM # ResourceDetector diff --git a/lib/solarwinds_apm/support/resource_detector/aws/beanstalk.rb b/lib/solarwinds_apm/support/resource_detector/aws/beanstalk.rb deleted file mode 100644 index ac1b1d63..00000000 --- a/lib/solarwinds_apm/support/resource_detector/aws/beanstalk.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -# © 2023 SolarWinds Worldwide, LLC. All rights reserved. -# -# 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. - -require 'net/http' -require 'uri' -require 'json' -require 'socket' - -module SolarWindsAPM - module ResourceDetector - # Beanstalk - module Beanstalk - module_function - - DEFAULT_BEANSTALK_CONF_PATH = '/var/elasticbeanstalk/xray/environment.conf' - WIN_OS_BEANSTALK_CONF_PATH = 'C:\\Program Files\\Amazon\\XRay\\environment.conf' - - def detect - beanstalk_config_path = if RUBY_PLATFORM.include?('mingw32') || RUBY_PLATFORM.include?('mswin') - WIN_OS_BEANSTALK_CONF_PATH - else - DEFAULT_BEANSTALK_CONF_PATH - end - - attribute = gather_data(beanstalk_config_path) - ::OpenTelemetry::SDK::Resources::Resource.create(attribute) - end - - def gather_data(config_path) - raw_data = File.read(config_path, encoding: 'utf-8') - parsed_data = JSON.parse(raw_data) - { - ::OpenTelemetry::SemanticConventions::Resource::CLOUD_PROVIDER => 'aws', - ::OpenTelemetry::SemanticConventions::Resource::CLOUD_PLATFORM => 'aws_elastic_beanstalk', - ::OpenTelemetry::SemanticConventions::Resource::SERVICE_NAME => 'aws_elastic_beanstalk', - ::OpenTelemetry::SemanticConventions::Resource::SERVICE_NAMESPACE => parsed_data['environment_name'], - ::OpenTelemetry::SemanticConventions::Resource::SERVICE_VERSION => parsed_data['version_label'], - ::OpenTelemetry::SemanticConventions::Resource::SERVICE_INSTANCE_ID => parsed_data['deployment_id'] - } - rescue StandardError => e - SolarWindsAPM.logger.debug { "Gather data for AWS Elastic Beanstalk resource detector failed: #{e.message}" } - {} - end - end - end -end diff --git a/lib/solarwinds_apm/support/resource_detector/aws/ec2.rb b/lib/solarwinds_apm/support/resource_detector/aws/ec2.rb deleted file mode 100644 index 882225dd..00000000 --- a/lib/solarwinds_apm/support/resource_detector/aws/ec2.rb +++ /dev/null @@ -1,145 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -require 'net/http' -require 'json' -require 'opentelemetry/common' -require 'opentelemetry/semantic_conventions/resource' - -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 - - # Create a constant for resource semantic conventions - RESOURCE = ::OpenTelemetry::SemanticConventions::Resource - - def detect - # Implementation for EC2 detection supporting both IMDSv1 and IMDSv2 - resource_attributes = {} - - begin - # 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 - - # Get instance identity document which contains most metadata - # 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) - - # Set resource attributes from the identity document - 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.handle_error(exception: e, message: 'EC2 resource detection failed') - 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, 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 if 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, 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 if 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 - ::OpenTelemetry::Common::Utilities.untraced do - http.request(request) - end - rescue StandardError - ::OpenTelemetry.logger.debug { 'EC2 metadata service request failed' } - nil - end - end - end - end - end - end -end diff --git a/lib/solarwinds_apm/support/resource_detector/aws/ecs.rb b/lib/solarwinds_apm/support/resource_detector/aws/ecs.rb deleted file mode 100644 index 490c6759..00000000 --- a/lib/solarwinds_apm/support/resource_detector/aws/ecs.rb +++ /dev/null @@ -1,173 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -require 'net/http' -require 'json' -require 'socket' -require 'opentelemetry/common' -require 'opentelemetry/semantic_conventions/resource' - -module OpenTelemetry - module Resource - module Detector - module AWS - # ECS contains detect class method for determining the ECS resource attributes - module ECS - extend self - - # Container ID length from cgroup file - CONTAINER_ID_LENGTH = 64 - - # HTTP request timeout in seconds - HTTP_TIMEOUT = 5 - - # Create a constant for resource semantic conventions - RESOURCE = ::OpenTelemetry::SemanticConventions::Resource - - def detect - # Return empty resource if not running on ECS - metadata_uri = ENV.fetch('ECS_CONTAINER_METADATA_URI', nil) - metadata_uri_v4 = ENV.fetch('ECS_CONTAINER_METADATA_URI_V4', nil) - - return ::OpenTelemetry::SDK::Resources::Resource.create({}) if metadata_uri.nil? && metadata_uri_v4.nil? - - resource_attributes = {} - container_id = fetch_container_id - - # Base ECS resource attributes - resource_attributes[RESOURCE::CLOUD_PROVIDER] = 'aws' - resource_attributes[RESOURCE::CLOUD_PLATFORM] = 'aws_ecs' - resource_attributes[RESOURCE::CONTAINER_NAME] = Socket.gethostname - resource_attributes[RESOURCE::CONTAINER_ID] = container_id unless container_id.empty? - - # If v4 endpoint is not available, return basic resource - return ::OpenTelemetry::SDK::Resources::Resource.create(resource_attributes) if metadata_uri_v4.nil? - - begin - # Fetch container and task metadata - container_metadata = JSON.parse(http_get(metadata_uri_v4.to_s)) - task_metadata = JSON.parse(http_get("#{metadata_uri_v4}/task")) - - task_arn = task_metadata['TaskARN'] - base_arn = task_arn[0..task_arn.rindex(':') - 1] - - cluster = task_metadata['Cluster'] - cluster_arn = cluster.start_with?('arn:') ? cluster : "#{base_arn}:cluster/#{cluster}" - - # Set ECS-specific attributes - resource_attributes[RESOURCE::AWS_ECS_CONTAINER_ARN] = container_metadata['ContainerARN'] - resource_attributes[RESOURCE::AWS_ECS_CLUSTER_ARN] = cluster_arn - resource_attributes[RESOURCE::AWS_ECS_LAUNCHTYPE] = task_metadata['LaunchType'].downcase - resource_attributes[RESOURCE::AWS_ECS_TASK_ARN] = task_arn - resource_attributes[RESOURCE::AWS_ECS_TASK_FAMILY] = task_metadata['Family'] - resource_attributes[RESOURCE::AWS_ECS_TASK_REVISION] = task_metadata['Revision'] - - # Add logging attributes if awslogs is used - logs_attributes = get_logs_resource(container_metadata) - resource_attributes.merge!(logs_attributes) - rescue StandardError => e - ::OpenTelemetry.handle_error(exception: e, message: 'ECS resource detection failed') - 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 container ID from /proc/self/cgroup file - # - # @return [String] The container ID or empty string if not found - def fetch_container_id - begin - File.open('/proc/self/cgroup', 'r') do |file| - file.each_line do |line| - line = line.strip - # Look for container ID (64 chars) at the end of the line - return line[-CONTAINER_ID_LENGTH..] if line.length > CONTAINER_ID_LENGTH - end - end - rescue Errno::ENOENT => e - ::OpenTelemetry.handle_error(exception: e, message: 'Failed to get container ID on ECS') - end - - '' - end - - # Extracting logging-related resource attributes - # - # @param container_metadata [Hash] Container metadata from ECS metadata endpoint - # @returhn [Hash] Resource attributes for logging configuration - def get_logs_resource(container_metadata) - log_attributes = {} - - if container_metadata['LogDriver'] == 'awslogs' - log_options = container_metadata['LogOptions'] - - if log_options - logs_region = log_options['awslogs-region'] - logs_group_name = log_options['awslogs-group'] - logs_stream_name = log_options['awslogs-stream'] - - container_arn = container_metadata['ContainerARN'] - - # Parse region from ARN if not specified in log options - if logs_region.nil? || logs_region.empty? - region_match = container_arn.match(/arn:aws:ecs:([^:]+):.*/) - logs_region = region_match[1] if region_match - end - - # Parse account ID from ARN - account_match = container_arn.match(/arn:aws:ecs:[^:]+:([^:]+):.*/) - aws_account = account_match[1] if account_match - - logs_group_arn = nil - logs_stream_arn = nil - - if logs_region && aws_account - logs_group_arn = "arn:aws:logs:#{logs_region}:#{aws_account}:log-group:#{logs_group_name}" if logs_group_name - - logs_stream_arn = "arn:aws:logs:#{logs_region}:#{aws_account}:log-group:#{logs_group_name}:log-stream:#{logs_stream_name}" if logs_stream_name && logs_group_name - end - - log_attributes[RESOURCE::AWS_LOG_GROUP_NAMES] = [logs_group_name].compact - log_attributes[RESOURCE::AWS_LOG_GROUP_ARNS] = [logs_group_arn].compact - log_attributes[RESOURCE::AWS_LOG_STREAM_NAMES] = [logs_stream_name].compact - log_attributes[RESOURCE::AWS_LOG_STREAM_ARNS] = [logs_stream_arn].compact - else - ::OpenTelemetry.handle_error(message: 'The metadata endpoint v4 has returned \'awslogs\' as \'LogDriver\', but there is no \'LogOptions\' data') - end - end - - log_attributes - end - - # Makes an HTTP GET request to the specified URL - # - # @param url [String] The URL to request - # @return [String] The response body - def http_get(url) - uri = URI.parse(url) - request = Net::HTTP::Get.new(uri) - - http = Net::HTTP.new(uri.host, uri.port) - http.open_timeout = HTTP_TIMEOUT - http.read_timeout = HTTP_TIMEOUT - - ::OpenTelemetry::Common::Utilities.untraced do - response = http.request(request) - raise "HTTP request failed with status #{response.code}" unless response.is_a?(Net::HTTPSuccess) - - response.body - end - end - end - end - end - end -end diff --git a/lib/solarwinds_apm/support/resource_detector/aws/eks.rb b/lib/solarwinds_apm/support/resource_detector/aws/eks.rb deleted file mode 100644 index b45df8bf..00000000 --- a/lib/solarwinds_apm/support/resource_detector/aws/eks.rb +++ /dev/null @@ -1,174 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -require 'net/http' -require 'json' -require 'openssl' -require 'uri' -require 'opentelemetry/common' -require 'opentelemetry/semantic_conventions/resource' - -module OpenTelemetry - module Resource - module Detector - module AWS - # EKS contains detect class method for determining EKS resource attributes - module EKS - extend self - - # Container ID length from cgroup file - CONTAINER_ID_LENGTH = 64 - - # HTTP request timeout in seconds - HTTP_TIMEOUT = 5 - - # Kubernetes token and certificate paths - TOKEN_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/token' - CERT_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt' - - # Kubernetes API paths - AWS_AUTH_PATH = '/api/v1/namespaces/kube-system/configmaps/aws-auth' - CLUSTER_INFO_PATH = '/api/v1/namespaces/amazon-cloudwatch/configmaps/cluster-info' - - # Create a constant for resource semantic conventions - RESOURCE = ::OpenTelemetry::SemanticConventions::Resource - - def detect - # Return empty resource if not running on K8s - return ::OpenTelemetry::SDK::Resources::Resource.create({}) unless k8s? - - resource_attributes = {} - - begin - # Get K8s credentials - cred_value = k8s_cred_value - - # Verify this is an EKS cluster - unless eks?(cred_value) - ::OpenTelemetry.logger.debug('Could not confirm process is running on EKS') - return ::OpenTelemetry::SDK::Resources::Resource.create({}) - end - - # Get cluster name and container ID - cluster_name_val = cluster_name(cred_value) - container_id_val = container_id - - if container_id_val.empty? && cluster_name_val.empty? - ::OpenTelemetry.logger.debug('Neither cluster name nor container ID found on EKS process') - return ::OpenTelemetry::SDK::Resources::Resource.create({}) - end - - # Set resource attributes - resource_attributes[RESOURCE::CLOUD_PROVIDER] = 'aws' - resource_attributes[RESOURCE::CLOUD_PLATFORM] = 'aws_eks' - resource_attributes[RESOURCE::K8S_CLUSTER_NAME] = cluster_name_val unless cluster_name_val.empty? - resource_attributes[RESOURCE::CONTAINER_ID] = container_id_val unless container_id_val.empty? - rescue StandardError => e - ::OpenTelemetry.logger.debug("EKS resource detection failed: #{e.message}") - return ::OpenTelemetry::SDK::Resources::Resource.create({}) - end - - resource_attributes.delete_if { |_key, value| value.nil? || value.empty? } - ::OpenTelemetry::SDK::Resources::Resource.create(resource_attributes) - end - - private - - # Check if running on K8s - # - # @return [Boolean] true if running on K8s - def k8s? - File.exist?(TOKEN_PATH) && File.exist?(CERT_PATH) - end - - # Get K8s token - # - # @return [String] K8s token - # @raise [StandardError] if token could not be read - def k8s_cred_value - token = File.read(TOKEN_PATH).strip - "Bearer #{token}" - rescue StandardError => e - ::OpenTelemetry.logger.debug("Failed to get k8s token: #{e.message}") - raise e - end - - # Check if running on EKS - # - # @param cred_value [String] K8s credentials - # @return [Boolean] true if running on EKS - def eks?(cred_value) - # Just try to to access the aws-auth configmap - # If it exists and we can access it, we're on EKS - aws_http_request(AWS_AUTH_PATH, cred_value) - true - rescue StandardError - false - end - - # Get EKS cluster name - # - # @param cred_value [String] K8s credentials - # @return [String] Cluster name or empty string if not found - def cluster_name(cred_value) - begin - response = aws_http_request(CLUSTER_INFO_PATH, cred_value) - cluster_info = JSON.parse(response) - return cluster_info['data']['cluster.name'] if cluster_info['data'] && cluster_info['data']['cluster.name'] - rescue StandardError => e - ::OpenTelemetry.logger.debug("Cannot get cluster name on EKS: #{e.message}") - end - '' - end - - # Get container ID from cgroup file - # - # @return [String] Container ID or empty string if not found - def container_id - begin - File.open('/proc/self/cgroup', 'r') do |file| - file.each_line do |line| - line = line.strip - # Look for container ID (64 chars) at the end of the line - return line[-CONTAINER_ID_LENGTH..] if line.length > CONTAINER_ID_LENGTH - end - end - rescue StandardError => e - ::OpenTelemetry.logger.debug("Failed to get container ID on EKS: #{e.message}") - end - '' - end - - # Make HTTP GET request to K8s API - # - # @param path [String] API path - # @param cred_value [String] Authorization header value - # @return [String] Response body - # @raise [StandardError] if request fails - def aws_http_request(path, cred_value) - uri = URI.parse("https://kubernetes.default.svc#{path}") - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_PEER - http.ca_file = CERT_PATH - http.open_timeout = HTTP_TIMEOUT - http.read_timeout = HTTP_TIMEOUT - - request = Net::HTTP::Get.new(uri) - request['Authorization'] = cred_value - - ::OpenTelemetry::Common::Utilities.untraced do - response = http.request(request) - raise "HTTP request failed with status #{response.code}" unless response.is_a?(Net::HTTPSuccess) - - response.body - end - end - end - end - end - end -end diff --git a/lib/solarwinds_apm/support/resource_detector/aws/lambda.rb b/lib/solarwinds_apm/support/resource_detector/aws/lambda.rb deleted file mode 100644 index 942c95a5..00000000 --- a/lib/solarwinds_apm/support/resource_detector/aws/lambda.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -# Copyright The OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -require 'opentelemetry/semantic_conventions/resource' - -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/lib/solarwinds_apm/version.rb b/lib/solarwinds_apm/version.rb index 338756ca..b20cafd3 100644 --- a/lib/solarwinds_apm/version.rb +++ b/lib/solarwinds_apm/version.rb @@ -14,7 +14,7 @@ module Version MAJOR = 7 # breaking, MINOR = 0 # feature, PATCH = 0 # fix => BFF - PRE = 'prev1' + PRE = nil STRING = [MAJOR, MINOR, PATCH, PRE].compact.join('.') end diff --git a/solarwinds_apm.gemspec b/solarwinds_apm.gemspec index fa955975..2ef34e66 100644 --- a/solarwinds_apm.gemspec +++ b/solarwinds_apm.gemspec @@ -30,10 +30,11 @@ Gem::Specification.new do |s| s.add_dependency('opentelemetry-exporter-otlp-metrics', '>= 0.3.0') s.add_dependency('opentelemetry-instrumentation-all', '>= 0.31.0') s.add_dependency('opentelemetry-metrics-sdk', '>= 0.2.0') - s.add_dependency('opentelemetry-resource-detector-azure', '>= 0.1.0') - s.add_dependency('opentelemetry-resource-detector-container', '>= 0.1.0') + s.add_dependency('opentelemetry-resource-detector-aws', '>= 0.1.0') + s.add_dependency('opentelemetry-resource-detector-azure', '>= 0.2.0') + s.add_dependency('opentelemetry-resource-detector-container', '>= 0.2.0') s.add_dependency('opentelemetry-sdk', '>= 1.2.0') - s.required_ruby_version = '>= 3.0.0' + s.required_ruby_version = '>= 3.1.0' s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } end diff --git a/test/test_setup.sh b/test/test_setup.sh index b42badab..41fb1db0 100755 --- a/test/test_setup.sh +++ b/test/test_setup.sh @@ -2,11 +2,6 @@ # Copyright (c) SolarWinds, LLC. # All rights reserved. -if grep -q "gem 'opentelemetry-metrics-sdk'" gemfiles/test_gems.gemfile && [ "$RUBY_VERSION" = '2.7.5' ]; then - echo "Skip test for 2.7.5 if using otel metrics" - exit 0 -fi - echo "Start Run Setup" if [ -r /etc/alpine-release ]; then if [ "$(uname -m)" = "aarch64" ]; then @@ -14,11 +9,11 @@ if [ -r /etc/alpine-release ]; then echo "Tests do not work on aarch64 alpine, skipping." exit else - apk update && apk add --upgrade git ruby-dev g++ make curl bash perl zlib-dev linux-headers shared-mime-info sqlite-dev yaml-dev grpc gcompat libc6-compat + apk update && apk add --upgrade git ruby-dev g++ make curl bash perl zlib-dev linux-headers shared-mime-info yaml-dev grpc gcompat libc6-compat fi elif [ -r /etc/debian_version ]; then # this is for ubuntu (> 22.04) and debian - apt-get update && apt-get install -y git ruby-dev g++ make curl zlib1g-dev shared-mime-info libsqlite3-dev xz-utils + apt-get update && apt-get install -y git ruby-dev g++ make curl zlib1g-dev shared-mime-info xz-utils libyaml-dev fi echo "Finished Setup" diff --git a/test/tmp.cert b/test/tmp.cert deleted file mode 100644 index 38b1a937..00000000 --- a/test/tmp.cert +++ /dev/null @@ -1,3 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID8TCCAtmgAwIBAgIJAMoDz7Npas2/MA0GCSqGSIb3DQEBCwUAMIGOMQswCQYD ------END CERTIFICATE----- \ No newline at end of file From b2c17fb791d03837758f8df2362726593e1e374e Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Tue, 17 Jun 2025 12:41:11 -0400 Subject: [PATCH 2/7] update otlp processor for lambda layer --- lib/solarwinds_apm/opentelemetry/otlp_processor.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/solarwinds_apm/opentelemetry/otlp_processor.rb b/lib/solarwinds_apm/opentelemetry/otlp_processor.rb index dcb4e7b7..8b66436b 100644 --- a/lib/solarwinds_apm/opentelemetry/otlp_processor.rb +++ b/lib/solarwinds_apm/opentelemetry/otlp_processor.rb @@ -67,6 +67,20 @@ def on_finish(span) SolarWindsAPM.logger.info { "[#{self.class}/#{__method__}] error processing span on_finish: #{e.message}" } end + # @param [optional Numeric] timeout An optional timeout in seconds. + # @return [Integer] Export::SUCCESS if no error occurred, Export::FAILURE if + # a non-specific failure occurred, Export::TIMEOUT if a timeout occurred. + def force_flush(timeout: nil) # rubocop:disable Lint/UnusedMethodArgument + ::OpenTelemetry::SDK::Trace::Export::SUCCESS + end + + # @param [optional Numeric] timeout An optional timeout in seconds. + # @return [Integer] Export::SUCCESS if no error occurred, Export::FAILURE if + # a non-specific failure occurred, Export::TIMEOUT if a timeout occurred. + def shutdown(timeout: nil) # rubocop:disable Lint/UnusedMethodArgument + ::OpenTelemetry::SDK::Trace::Export::SUCCESS + end + private def meter_attributes(span) From 3d9e907632c128952491cc113729bb248729116d Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Thu, 19 Jun 2025 12:07:14 -0400 Subject: [PATCH 3/7] guard the non-enry-span for transaction naming --- lib/solarwinds_apm/opentelemetry/otlp_processor.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/solarwinds_apm/opentelemetry/otlp_processor.rb b/lib/solarwinds_apm/opentelemetry/otlp_processor.rb index 8b66436b..e3aef2e2 100644 --- a/lib/solarwinds_apm/opentelemetry/otlp_processor.rb +++ b/lib/solarwinds_apm/opentelemetry/otlp_processor.rb @@ -45,6 +45,8 @@ def on_start(span, parent_context) end def on_finishing(span) + return if non_entry_span(span: span) + @transaction_name = calculate_transaction_names(span) span.set_attribute(SW_TRANSACTION_NAME, @transaction_name) @txn_manager.delete_root_context_h(span.context.hex_trace_id) From 7a1c32618f116ad439c7eaf842bd1c84fe358ced Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Tue, 24 Jun 2025 14:22:24 -0400 Subject: [PATCH 4/7] exponential histogram for response time and semconv opt in for http --- .../opentelemetry/otlp_processor.rb | 15 ++++++++++++++- lib/solarwinds_apm/otel_native_config.rb | 3 +++ test/minitest_helper.rb | 7 +++++++ test/opentelemetry/otlp_processor_sampled_test.rb | 1 + test/opentelemetry/otlp_processor_test.rb | 1 + .../otlp_processor_unsampled_test.rb | 1 + 6 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/solarwinds_apm/opentelemetry/otlp_processor.rb b/lib/solarwinds_apm/opentelemetry/otlp_processor.rb index e3aef2e2..a1476ac4 100644 --- a/lib/solarwinds_apm/opentelemetry/otlp_processor.rb +++ b/lib/solarwinds_apm/opentelemetry/otlp_processor.rb @@ -26,7 +26,7 @@ class OTLPProcessor def initialize(txn_manager) @txn_manager = txn_manager @meters = { 'sw.apm.request.metrics' => ::OpenTelemetry.meter_provider.meter('sw.apm.request.metrics') } - @metrics = { response_time: @meters['sw.apm.request.metrics'].create_histogram('trace.service.response_time', unit: 'ms', description: 'Duration of each entry span for the service, typically meaning the time taken to process an inbound request.') } + @metrics = init_response_time_metrics @transaction_name = nil end @@ -85,6 +85,19 @@ def shutdown(timeout: nil) # rubocop:disable Lint/UnusedMethodArgument private + def init_response_time_metrics + # add the ExponentialBucketHistogram view + if defined? ::OpenTelemetry::Exporter::OTLP::Metrics && Gem::Version.new(::OpenTelemetry::Exporter::OTLP::Metrics::VERSION) >= Gem::Version.new('0.5.0') + ::OpenTelemetry.meter_provider.add_view('trace.service.response_time', + aggregation: ::OpenTelemetry::SDK::Metrics::Aggregation::ExponentialBucketHistogram.new(max_scale: 20), + type: :histogram, + unit: 'ms') + end + + instrument = @meters['sw.apm.request.metrics'].create_histogram('trace.service.response_time', unit: 'ms', description: 'Duration of each entry span for the service, typically meaning the time taken to process an inbound request.') + { response_time: instrument } + end + def meter_attributes(span) meter_attrs = { SW_IS_ERROR => error?(span) == 1, diff --git a/lib/solarwinds_apm/otel_native_config.rb b/lib/solarwinds_apm/otel_native_config.rb index 2b12ae36..d3d89c61 100644 --- a/lib/solarwinds_apm/otel_native_config.rb +++ b/lib/solarwinds_apm/otel_native_config.rb @@ -62,6 +62,9 @@ def self.initialize ENV["OTEL_EXPORTER_OTLP_#{signal}_COMPRESSION"] = 'gzip' if ENV["OTEL_EXPORTER_OTLP_#{signal}_COMPRESSION"].to_s.empty? && ENV['OTEL_EXPORTER_OTLP_COMPRESSION'].to_s.empty? end + # set http stable semconv + ENV['OTEL_SEMCONV_STABILITY_OPT_IN'] = 'http' if ENV['OTEL_SEMCONV_STABILITY_OPT_IN'].to_s.empty? + # set delta temporality ENV['OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE'] = 'delta' if ENV['OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE'].to_s.empty? diff --git a/test/minitest_helper.rb b/test/minitest_helper.rb index ca5d835d..370dc227 100644 --- a/test/minitest_helper.rb +++ b/test/minitest_helper.rb @@ -361,3 +361,10 @@ def initialize end VERSION = '999.0.0' end + +module DisableAddView + def init_response_time_metrics + instrument = @meters['sw.apm.request.metrics'].create_histogram('trace.service.response_time', unit: 'ms', description: 'Duration of each entry span for the service, typically meaning the time taken to process an inbound request.') + { response_time: instrument } + end +end diff --git a/test/opentelemetry/otlp_processor_sampled_test.rb b/test/opentelemetry/otlp_processor_sampled_test.rb index aeb4d961..20b10665 100644 --- a/test/opentelemetry/otlp_processor_sampled_test.rb +++ b/test/opentelemetry/otlp_processor_sampled_test.rb @@ -15,6 +15,7 @@ puts "\n\033[1m=== OTLP PROCESSOR TEST: #{RUBY_VERSION} #{File.basename(__FILE__)} #{Time.now.strftime('%Y-%m-%d %H:%M')} ===\033[0m\n" before do + SolarWindsAPM::OpenTelemetry::OTLPProcessor.prepend(DisableAddView) txn_manager = SolarWindsAPM::TxnNameManager.new @processor = SolarWindsAPM::OpenTelemetry::OTLPProcessor.new(txn_manager) end diff --git a/test/opentelemetry/otlp_processor_test.rb b/test/opentelemetry/otlp_processor_test.rb index e4fa2f78..695ddaa9 100644 --- a/test/opentelemetry/otlp_processor_test.rb +++ b/test/opentelemetry/otlp_processor_test.rb @@ -13,6 +13,7 @@ describe 'SolarWindsOTLPProcessor' do before do + SolarWindsAPM::OpenTelemetry::OTLPProcessor.prepend(DisableAddView) @txn_manager = SolarWindsAPM::TxnNameManager.new @processor = SolarWindsAPM::OpenTelemetry::OTLPProcessor.new(@txn_manager) end diff --git a/test/opentelemetry/otlp_processor_unsampled_test.rb b/test/opentelemetry/otlp_processor_unsampled_test.rb index 007d00bb..562f178c 100644 --- a/test/opentelemetry/otlp_processor_unsampled_test.rb +++ b/test/opentelemetry/otlp_processor_unsampled_test.rb @@ -15,6 +15,7 @@ puts "\n\033[1m=== OTLP PROCESSOR TEST: #{RUBY_VERSION} #{File.basename(__FILE__)} #{Time.now.strftime('%Y-%m-%d %H:%M')} ===\033[0m\n" before do + SolarWindsAPM::OpenTelemetry::OTLPProcessor.prepend(DisableAddView) txn_manager = SolarWindsAPM::TxnNameManager.new @processor = SolarWindsAPM::OpenTelemetry::OTLPProcessor.new(txn_manager) end From 72234fd2da8a2349497136d0c310d3777feab8ee Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Mon, 30 Jun 2025 12:42:30 -0400 Subject: [PATCH 5/7] fix the issue that setting become nil --- lib/solarwinds_apm/sampling/oboe_sampler.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solarwinds_apm/sampling/oboe_sampler.rb b/lib/solarwinds_apm/sampling/oboe_sampler.rb index fd11b8e7..93ce26b4 100644 --- a/lib/solarwinds_apm/sampling/oboe_sampler.rb +++ b/lib/solarwinds_apm/sampling/oboe_sampler.rb @@ -337,7 +337,7 @@ def get_settings(params) time_now = Time.now.to_i * 1000 if time_now > expiry @logger.debug { 'settings expired, removing' } - @settings = nil + @settings = {} return end sampling_setting = SolarWindsAPM::SamplingSettings.merge(@settings, local_settings(params)) From 17dac5a27ff5e74fcd2a54546bdf99b3b2715902 Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Wed, 2 Jul 2025 13:52:11 -0400 Subject: [PATCH 6/7] enforce delta tmp for response metrics --- lib/solarwinds_apm/opentelemetry/otlp_processor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solarwinds_apm/opentelemetry/otlp_processor.rb b/lib/solarwinds_apm/opentelemetry/otlp_processor.rb index a1476ac4..f776e340 100644 --- a/lib/solarwinds_apm/opentelemetry/otlp_processor.rb +++ b/lib/solarwinds_apm/opentelemetry/otlp_processor.rb @@ -89,7 +89,7 @@ def init_response_time_metrics # add the ExponentialBucketHistogram view if defined? ::OpenTelemetry::Exporter::OTLP::Metrics && Gem::Version.new(::OpenTelemetry::Exporter::OTLP::Metrics::VERSION) >= Gem::Version.new('0.5.0') ::OpenTelemetry.meter_provider.add_view('trace.service.response_time', - aggregation: ::OpenTelemetry::SDK::Metrics::Aggregation::ExponentialBucketHistogram.new(max_scale: 20), + aggregation: ::OpenTelemetry::SDK::Metrics::Aggregation::ExponentialBucketHistogram.new(aggregation_temporality: :delta), type: :histogram, unit: 'ms') end From 3f12945993139c3ad9b616fc557269f91052b63c Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Wed, 6 Aug 2025 11:15:34 -0400 Subject: [PATCH 7/7] remove < 3.0.0 related gem --- Gemfile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index a6125ce1..05899a31 100644 --- a/Gemfile +++ b/Gemfile @@ -5,10 +5,7 @@ source 'https://rubygems.org' gem 'rake' group :development, :test do - if RUBY_VERSION < '3.0.0' - gem 'ffi', '<= 1.16.3' # set this version due to ffi 1.17.0 need rubygems version > 3 (https://rubygems.org/gems/ffi/versions/1.17.0-arm-linux-musl) - gem 'google-protobuf', '< 3.25.4' - elsif RUBY_VERSION < '3.1.0' + if RUBY_VERSION < '3.1.0' gem 'ffi', '<= 1.17' else gem 'ffi'