solarwinds_apm 6.1.1 → 7.0.0.prev1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +53 -4
  3. data/lib/rails/generators/solarwinds_apm/templates/solarwinds_apm_initializer.rb +0 -4
  4. data/lib/solarwinds_apm/api/current_trace_info.rb +10 -6
  5. data/lib/solarwinds_apm/api/custom_instrumentation.rb +80 -0
  6. data/lib/solarwinds_apm/api/custom_metrics.rb +8 -25
  7. data/lib/solarwinds_apm/api/tracing.rb +12 -27
  8. data/lib/solarwinds_apm/api/transaction_name.rb +6 -10
  9. data/lib/solarwinds_apm/api.rb +2 -0
  10. data/lib/solarwinds_apm/config.rb +1 -1
  11. data/lib/solarwinds_apm/constants.rb +1 -0
  12. data/lib/solarwinds_apm/noop/api.rb +5 -2
  13. data/lib/solarwinds_apm/noop.rb +0 -24
  14. data/lib/solarwinds_apm/opentelemetry/otlp_processor.rb +90 -69
  15. data/lib/solarwinds_apm/opentelemetry/solarwinds_propagator.rb +0 -2
  16. data/lib/solarwinds_apm/opentelemetry/solarwinds_response_propagator.rb +5 -4
  17. data/lib/solarwinds_apm/opentelemetry.rb +5 -7
  18. data/lib/solarwinds_apm/otel_native_config.rb +177 -0
  19. data/lib/solarwinds_apm/patch/README.md +15 -0
  20. data/lib/solarwinds_apm/patch/tag_sql/sw_dbo_utils.rb +35 -0
  21. data/lib/solarwinds_apm/patch/tag_sql/sw_mysql2_patch.rb +1 -12
  22. data/lib/solarwinds_apm/patch/tag_sql/sw_pg_patch.rb +39 -0
  23. data/lib/solarwinds_apm/patch/tag_sql_patch.rb +2 -0
  24. data/lib/solarwinds_apm/{noop/metadata.rb → sampling/dice.rb} +19 -17
  25. data/lib/solarwinds_apm/sampling/http_sampler.rb +87 -0
  26. data/lib/solarwinds_apm/sampling/json_sampler.rb +52 -0
  27. data/lib/solarwinds_apm/sampling/metrics.rb +38 -0
  28. data/lib/solarwinds_apm/sampling/oboe_sampler.rb +348 -0
  29. data/lib/solarwinds_apm/sampling/sampler.rb +197 -0
  30. data/lib/solarwinds_apm/sampling/sampling_constants.rb +127 -0
  31. data/lib/solarwinds_apm/sampling/sampling_patch.rb +49 -0
  32. data/lib/solarwinds_apm/sampling/setting_example.txt +1 -0
  33. data/lib/solarwinds_apm/{noop/context.rb → sampling/settings.rb} +14 -25
  34. data/lib/solarwinds_apm/sampling/token_bucket.rb +126 -0
  35. data/lib/solarwinds_apm/sampling/trace_options.rb +100 -0
  36. data/lib/solarwinds_apm/{patch.rb → sampling.rb} +20 -4
  37. data/lib/solarwinds_apm/{noop/span.rb → support/aws_resource_detector.rb} +5 -18
  38. data/lib/solarwinds_apm/support/logger_formatter.rb +1 -1
  39. data/lib/solarwinds_apm/support/logging_log_event.rb +1 -1
  40. data/lib/solarwinds_apm/support/lumberjack_formatter.rb +1 -1
  41. data/lib/solarwinds_apm/support/otlp_endpoint.rb +99 -0
  42. data/lib/solarwinds_apm/support/resource_detector/aws/beanstalk.rb +51 -0
  43. data/lib/solarwinds_apm/support/resource_detector/aws/ec2.rb +145 -0
  44. data/lib/solarwinds_apm/support/resource_detector/aws/ecs.rb +173 -0
  45. data/lib/solarwinds_apm/support/resource_detector/aws/eks.rb +174 -0
  46. data/lib/solarwinds_apm/support/resource_detector/aws/lambda.rb +66 -0
  47. data/lib/solarwinds_apm/support/resource_detector.rb +192 -0
  48. data/lib/solarwinds_apm/support/service_key_checker.rb +12 -6
  49. data/lib/solarwinds_apm/support/transaction_settings.rb +6 -0
  50. data/lib/solarwinds_apm/support/txn_name_manager.rb +54 -9
  51. data/lib/solarwinds_apm/support/utils.rb +9 -0
  52. data/lib/solarwinds_apm/support.rb +3 -4
  53. data/lib/solarwinds_apm/version.rb +4 -4
  54. data/lib/solarwinds_apm.rb +27 -73
  55. metadata +105 -50
  56. data/ext/oboe_metal/extconf.rb +0 -168
  57. data/ext/oboe_metal/lib/liboboe-1.0-aarch64.so.sha256 +0 -1
  58. data/ext/oboe_metal/lib/liboboe-1.0-alpine-aarch64.so.sha256 +0 -1
  59. data/ext/oboe_metal/lib/liboboe-1.0-alpine-x86_64.so.sha256 +0 -1
  60. data/ext/oboe_metal/lib/liboboe-1.0-lambda-aarch64.so.sha256 +0 -1
  61. data/ext/oboe_metal/lib/liboboe-1.0-lambda-x86_64.so.sha256 +0 -1
  62. data/ext/oboe_metal/lib/liboboe-1.0-x86_64.so.sha256 +0 -1
  63. data/ext/oboe_metal/src/VERSION +0 -1
  64. data/ext/oboe_metal/src/bson/bson.h +0 -220
  65. data/ext/oboe_metal/src/bson/platform_hacks.h +0 -91
  66. data/ext/oboe_metal/src/init_solarwinds_apm.cc +0 -18
  67. data/ext/oboe_metal/src/oboe.h +0 -930
  68. data/ext/oboe_metal/src/oboe_api.cpp +0 -793
  69. data/ext/oboe_metal/src/oboe_api.h +0 -621
  70. data/ext/oboe_metal/src/oboe_debug.h +0 -17
  71. data/ext/oboe_metal/src/oboe_swig_wrap.cc +0 -10954
  72. data/lib/oboe_metal.rb +0 -187
  73. data/lib/solarwinds_apm/cert/star.appoptics.com.issuer.crt +0 -24
  74. data/lib/solarwinds_apm/oboe_init_options.rb +0 -222
  75. data/lib/solarwinds_apm/opentelemetry/solarwinds_exporter.rb +0 -239
  76. data/lib/solarwinds_apm/opentelemetry/solarwinds_processor.rb +0 -174
  77. data/lib/solarwinds_apm/opentelemetry/solarwinds_sampler.rb +0 -333
  78. data/lib/solarwinds_apm/otel_config.rb +0 -174
  79. data/lib/solarwinds_apm/otel_lambda_config.rb +0 -56
  80. data/lib/solarwinds_apm/patch/dummy_patch.rb +0 -12
  81. data/lib/solarwinds_apm/support/oboe_tracing_mode.rb +0 -33
  82. data/lib/solarwinds_apm/support/support_report.rb +0 -99
  83. data/lib/solarwinds_apm/support/swomarginalia/LICENSE +0 -20
  84. data/lib/solarwinds_apm/support/swomarginalia/README.md +0 -46
  85. data/lib/solarwinds_apm/support/swomarginalia/comment.rb +0 -206
  86. data/lib/solarwinds_apm/support/swomarginalia/formatter.rb +0 -20
  87. data/lib/solarwinds_apm/support/swomarginalia/load_swomarginalia.rb +0 -55
  88. data/lib/solarwinds_apm/support/swomarginalia/railtie.rb +0 -24
  89. data/lib/solarwinds_apm/support/swomarginalia/swomarginalia.rb +0 -89
  90. data/lib/solarwinds_apm/support/transaction_cache.rb +0 -57
  91. data/lib/solarwinds_apm/support/x_trace_options.rb +0 -138
@@ -1,25 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # © 2023 SolarWinds Worldwide, LLC. All rights reserved.
3
+ # © 2025 SolarWinds Worldwide, LLC. All rights reserved.
4
4
  #
5
5
  # 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
6
6
  #
7
7
  # 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.
8
8
 
9
- ####
10
- # noop version of SolarWindsAPM::Span
11
- #
12
- module Oboe_metal # rubocop:disable Naming/ClassAndModuleCamelCase
13
- # Span
14
- class Span
15
- ##
16
- # noop version of :createHttpSpan
17
- #
18
- def self.createHttpSpan(*); end
19
-
20
- ##
21
- # noop version of :createSpan
22
- #
23
- def self.createSpan(*); end
24
- end
25
- end
9
+ require_relative 'resource_detector/aws/ecs'
10
+ require_relative 'resource_detector/aws/eks'
11
+ require_relative 'resource_detector/aws/lambda'
12
+ require_relative 'resource_detector/aws/beanstalk'
@@ -47,4 +47,4 @@ end
47
47
 
48
48
  # To use the trace context in log, ::Logger::Formatter.new must be defined
49
49
  # e.g. config.log_formatter = ::Logger::Formatter.new
50
- Logger::Formatter.prepend(SolarWindsAPM::Logger::Formatter) if SolarWindsAPM.loaded
50
+ Logger::Formatter.prepend(SolarWindsAPM::Logger::Formatter)
@@ -23,4 +23,4 @@ module SolarWindsAPM
23
23
  end
24
24
  end
25
25
 
26
- Logging::LogEvent.prepend(SolarWindsAPM::Logging::LogEvent) if SolarWindsAPM.loaded && defined?(Logging::LogEvent)
26
+ Logging::LogEvent.prepend(SolarWindsAPM::Logging::LogEvent) if defined?(Logging::LogEvent)
@@ -23,4 +23,4 @@ module SolarWindsAPM
23
23
  end
24
24
  end
25
25
 
26
- Lumberjack::LogEntry.prepend(SolarWindsAPM::Lumberjack::LogEntry) if SolarWindsAPM.loaded && defined?(Lumberjack::LogEntry)
26
+ Lumberjack::LogEntry.prepend(SolarWindsAPM::Lumberjack::LogEntry) if defined?(Lumberjack::LogEntry)
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ # © 2023 SolarWinds Worldwide, LLC. All rights reserved.
4
+ #
5
+ # 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
6
+ #
7
+ # 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.
8
+
9
+ require_relative 'service_key_checker'
10
+
11
+ module SolarWindsAPM
12
+ # OTLPEndPoint
13
+ class OTLPEndPoint
14
+ attr_reader :token, :service_name
15
+
16
+ SWO_APM_ENDPOINT_REGEX = /^apm\.collector\.([a-z]{2}-\d{2})\.([^.]+)\.solarwinds\.com(?::\d+)?$/
17
+ SWO_APM_ENDPOINT_DEFAULT = 'apm.collector.na-01.cloud.solarwinds.com:443'
18
+
19
+ SWO_OTLP_GENERAL_ENDPOINT_REGEX = %r{^https://otel\.collector\.[a-z0-9-]+\.[a-z0-9-]+\.solarwinds\.com(?::\d+)?$}
20
+ SWO_OTLP_SIGNAL_ENDPOINT_REGEX = %r{^https://otel\.collector\.[a-z0-9-]+\.[a-z0-9-]+\.solarwinds\.com(?::\d+)?/v1/(?:logs|metrics|traces)$}
21
+ SWO_OTLP_ENDPOINT_DEFAULT = 'https://otel.collector.na-01.cloud.solarwinds.com:443'
22
+
23
+ OTEL_SIGNAL_TYPE = %w[TRACES METRICS LOGS].freeze
24
+
25
+ def initialize
26
+ @token = nil
27
+ @service_name = nil
28
+ end
29
+
30
+ def config_otlp_token_and_endpoint
31
+ matches = ENV['SW_APM_COLLECTOR'].to_s.match(SWO_APM_ENDPOINT_REGEX)
32
+
33
+ resolve_get_setting_endpoint(matches)
34
+
35
+ service_key_checker = SolarWindsAPM::ServiceKeyChecker.new('ssl', SolarWindsAPM::Utils.determine_lambda)
36
+ @token = service_key_checker.token
37
+ @service_name = service_key_checker.service_name
38
+
39
+ OTEL_SIGNAL_TYPE.each do |data_type|
40
+ config_token(data_type)
41
+ configure_otlp_endpoint(data_type, matches)
42
+ end
43
+ end
44
+
45
+ # APM Libraries should only set the bearer token header as a convenience if:
46
+ # The OTEL config for exporter OTLP headers is not already set, i.e. explicitly configured by the end user, AND
47
+ # The OTLP export endpoint is SWO, i.e. host is otel.collector.*.*.solarwinds.com
48
+ def config_token(data_type)
49
+ data_type_upper = data_type.upcase
50
+
51
+ return unless @token
52
+
53
+ if ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_HEADERS"].to_s.empty? && ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_ENDPOINT"].to_s.match?(SWO_OTLP_SIGNAL_ENDPOINT_REGEX)
54
+ ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_HEADERS"] = "authorization=Bearer #{@token}"
55
+ elsif ENV['OTEL_EXPORTER_OTLP_HEADERS'].to_s.empty? && ENV['OTEL_EXPORTER_OTLP_ENDPOINT'].to_s.match?(SWO_OTLP_GENERAL_ENDPOINT_REGEX)
56
+ ENV['OTEL_EXPORTER_OTLP_HEADERS'] = "authorization=Bearer #{@token}"
57
+ end
58
+
59
+ ENV['OTEL_EXPORTER_OTLP_HEADERS'] = "authorization=Bearer #{@token}" if ENV['OTEL_EXPORTER_OTLP_HEADERS'].to_s.empty? && ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_HEADERS"].to_s.empty?
60
+ end
61
+
62
+ def configure_otlp_endpoint(data_type, matches)
63
+ data_type_upper = data_type.upcase
64
+ data_type_lower = data_type.downcase
65
+ otlp_endpoint_source = if ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_ENDPOINT"]
66
+ "#{data_type_lower}_endpoint"
67
+ elsif ENV['OTEL_EXPORTER_OTLP_ENDPOINT']
68
+ 'general_endpoint'
69
+ else
70
+ 'no_endpoint'
71
+ end
72
+
73
+ SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] otlp_endpoint_source: #{otlp_endpoint_source}" }
74
+
75
+ return unless otlp_endpoint_source == 'no_endpoint'
76
+
77
+ if matches&.size == 3
78
+ region = matches[1]
79
+ env = matches[2]
80
+ ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_ENDPOINT"] = "https://otel.collector.#{region}.#{env}.solarwinds.com:443/v1/#{data_type_lower}"
81
+ else
82
+ ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_ENDPOINT"] = "https://otel.collector.na-01.cloud.solarwinds.com:443/v1/#{data_type_lower}"
83
+ end
84
+ end
85
+
86
+ # only valid value is apm.collector.*.*.solarwinds.com
87
+ # If SW APM config for collector is not set, the fallback: apm.collector.na-01.cloud.solarwinds.com
88
+ def resolve_get_setting_endpoint(matches)
89
+ return if matches&.size == 3
90
+
91
+ unless ENV['SW_APM_COLLECTOR'].to_s.empty?
92
+ SolarWindsAPM.logger.warn do
93
+ "[#{self.class}/#{__method__}] SW_APM_COLLECTOR format invalid: #{ENV.fetch('SW_APM_COLLECTOR', nil)}. Valid format: apm.collector.*.*.solarwinds.com"
94
+ end
95
+ end
96
+ ENV['SW_APM_COLLECTOR'] = SWO_APM_ENDPOINT_DEFAULT
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # © 2023 SolarWinds Worldwide, LLC. All rights reserved.
4
+ #
5
+ # 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
6
+ #
7
+ # 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.
8
+
9
+ require 'net/http'
10
+ require 'uri'
11
+ require 'json'
12
+ require 'socket'
13
+
14
+ module SolarWindsAPM
15
+ module ResourceDetector
16
+ # Beanstalk
17
+ module Beanstalk
18
+ module_function
19
+
20
+ DEFAULT_BEANSTALK_CONF_PATH = '/var/elasticbeanstalk/xray/environment.conf'
21
+ WIN_OS_BEANSTALK_CONF_PATH = 'C:\\Program Files\\Amazon\\XRay\\environment.conf'
22
+
23
+ def detect
24
+ beanstalk_config_path = if RUBY_PLATFORM.include?('mingw32') || RUBY_PLATFORM.include?('mswin')
25
+ WIN_OS_BEANSTALK_CONF_PATH
26
+ else
27
+ DEFAULT_BEANSTALK_CONF_PATH
28
+ end
29
+
30
+ attribute = gather_data(beanstalk_config_path)
31
+ ::OpenTelemetry::SDK::Resources::Resource.create(attribute)
32
+ end
33
+
34
+ def gather_data(config_path)
35
+ raw_data = File.read(config_path, encoding: 'utf-8')
36
+ parsed_data = JSON.parse(raw_data)
37
+ {
38
+ ::OpenTelemetry::SemanticConventions::Resource::CLOUD_PROVIDER => 'aws',
39
+ ::OpenTelemetry::SemanticConventions::Resource::CLOUD_PLATFORM => 'aws_elastic_beanstalk',
40
+ ::OpenTelemetry::SemanticConventions::Resource::SERVICE_NAME => 'aws_elastic_beanstalk',
41
+ ::OpenTelemetry::SemanticConventions::Resource::SERVICE_NAMESPACE => parsed_data['environment_name'],
42
+ ::OpenTelemetry::SemanticConventions::Resource::SERVICE_VERSION => parsed_data['version_label'],
43
+ ::OpenTelemetry::SemanticConventions::Resource::SERVICE_INSTANCE_ID => parsed_data['deployment_id']
44
+ }
45
+ rescue StandardError => e
46
+ SolarWindsAPM.logger.debug { "Gather data for AWS Elastic Beanstalk resource detector failed: #{e.message}" }
47
+ {}
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright The OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ require 'net/http'
8
+ require 'json'
9
+ require 'opentelemetry/common'
10
+ require 'opentelemetry/semantic_conventions/resource'
11
+
12
+ module OpenTelemetry
13
+ module Resource
14
+ module Detector
15
+ module AWS
16
+ # EC2 contains detect class method for determining EC2 resource attributes
17
+ module EC2
18
+ extend self
19
+
20
+ # EC2 metadata service endpoints and constants
21
+ EC2_METADATA_HOST = '169.254.169.254'
22
+ TOKEN_ENDPOINT = '/latest/api/token'
23
+ IDENTITY_DOCUMENT_ENDPOINT = '/latest/dynamic/instance-identity/document'
24
+ HOSTNAME_ENDPOINT = '/latest/meta-data/hostname'
25
+
26
+ TOKEN_HEADER = 'X-aws-ec2-metadata-token'
27
+ TOKEN_TTL_HEADER = 'X-aws-ec2-metadata-token-ttl-seconds'
28
+ TOKEN_TTL_VALUE = '60'
29
+
30
+ # Timeout in seconds for HTTP requests
31
+ HTTP_TIMEOUT = 1
32
+
33
+ # Create a constant for resource semantic conventions
34
+ RESOURCE = ::OpenTelemetry::SemanticConventions::Resource
35
+
36
+ def detect
37
+ # Implementation for EC2 detection supporting both IMDSv1 and IMDSv2
38
+ resource_attributes = {}
39
+
40
+ begin
41
+ # Attempt to get IMDSv2 token - this will fail if IMDSv2 is not supported
42
+ # but we'll still try IMDSv1 in that case
43
+ token = fetch_token
44
+
45
+ # Get instance identity document which contains most metadata
46
+ # Will try with token (IMDSv2) or without token (IMDSv1)
47
+ identity = fetch_identity_document(token) || {}
48
+ return ::OpenTelemetry::SDK::Resources::Resource.create({}) if identity.empty?
49
+
50
+ hostname = fetch_hostname(token)
51
+
52
+ # Set resource attributes from the identity document
53
+ resource_attributes[RESOURCE::CLOUD_PROVIDER] = 'aws'
54
+ resource_attributes[RESOURCE::CLOUD_PLATFORM] = 'aws_ec2'
55
+ resource_attributes[RESOURCE::CLOUD_ACCOUNT_ID] = identity['accountId']
56
+ resource_attributes[RESOURCE::CLOUD_REGION] = identity['region']
57
+ resource_attributes[RESOURCE::CLOUD_AVAILABILITY_ZONE] = identity['availabilityZone']
58
+
59
+ resource_attributes[RESOURCE::HOST_ID] = identity['instanceId']
60
+ resource_attributes[RESOURCE::HOST_TYPE] = identity['instanceType']
61
+ resource_attributes[RESOURCE::HOST_NAME] = hostname
62
+ rescue StandardError => e
63
+ ::OpenTelemetry.handle_error(exception: e, message: 'EC2 resource detection failed')
64
+ return ::OpenTelemetry::SDK::Resources::Resource.create({})
65
+ end
66
+
67
+ # Filter out nil or empty values
68
+ resource_attributes.delete_if { |_key, value| value.nil? || value.empty? }
69
+ ::OpenTelemetry::SDK::Resources::Resource.create(resource_attributes)
70
+ end
71
+
72
+ private
73
+
74
+ # Fetches an IMDSv2 token from the EC2 metadata service
75
+ #
76
+ # @return [String, nil] The token or nil if the request failed
77
+ def fetch_token
78
+ uri = URI.parse("http://#{EC2_METADATA_HOST}#{TOKEN_ENDPOINT}")
79
+ request = Net::HTTP::Put.new(uri)
80
+ request[TOKEN_TTL_HEADER] = TOKEN_TTL_VALUE
81
+
82
+ response = make_request(uri, request)
83
+ return nil unless response.is_a?(Net::HTTPSuccess)
84
+
85
+ response.body
86
+ end
87
+
88
+ # Fetches the instance identity document which contains EC2 instance metadata
89
+ #
90
+ # @param token [String, nil] IMDSv2 token (optional for IMDSv1)
91
+ # @return [Hash, nil] Parsed identity document or nil if the request failed
92
+ def fetch_identity_document(token)
93
+ uri = URI.parse("http://#{EC2_METADATA_HOST}#{IDENTITY_DOCUMENT_ENDPOINT}")
94
+ request = Net::HTTP::Get.new(uri)
95
+ request[TOKEN_HEADER] = token if token
96
+
97
+ response = make_request(uri, request)
98
+ return nil unless response.is_a?(Net::HTTPSuccess)
99
+
100
+ begin
101
+ JSON.parse(response.body)
102
+ rescue JSON::ParserError
103
+ nil
104
+ end
105
+ end
106
+
107
+ # Fetches the EC2 instance hostname
108
+ #
109
+ # @param token [String, nil] IMDSv2 token (optional for IMDSv1)
110
+ # @return [String, nil] The hostname or nil if the request failed
111
+ def fetch_hostname(token)
112
+ uri = URI.parse("http://#{EC2_METADATA_HOST}#{HOSTNAME_ENDPOINT}")
113
+ request = Net::HTTP::Get.new(uri)
114
+ request[TOKEN_HEADER] = token if token
115
+
116
+ response = make_request(uri, request)
117
+ return nil unless response.is_a?(Net::HTTPSuccess)
118
+
119
+ response.body
120
+ end
121
+
122
+ # Makes an HTTP request with timeout handling
123
+ #
124
+ # @param uri [URI] The request URI
125
+ # @param request [Net::HTTP::Request] The request to perform
126
+ # @return [Net::HTTPResponse, nil] The response or nil if the request failed
127
+ def make_request(uri, request)
128
+ http = Net::HTTP.new(uri.host, uri.port)
129
+ http.open_timeout = HTTP_TIMEOUT
130
+ http.read_timeout = HTTP_TIMEOUT
131
+
132
+ begin
133
+ ::OpenTelemetry::Common::Utilities.untraced do
134
+ http.request(request)
135
+ end
136
+ rescue StandardError
137
+ ::OpenTelemetry.logger.debug { 'EC2 metadata service request failed' }
138
+ nil
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright The OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ require 'net/http'
8
+ require 'json'
9
+ require 'socket'
10
+ require 'opentelemetry/common'
11
+ require 'opentelemetry/semantic_conventions/resource'
12
+
13
+ module OpenTelemetry
14
+ module Resource
15
+ module Detector
16
+ module AWS
17
+ # ECS contains detect class method for determining the ECS resource attributes
18
+ module ECS
19
+ extend self
20
+
21
+ # Container ID length from cgroup file
22
+ CONTAINER_ID_LENGTH = 64
23
+
24
+ # HTTP request timeout in seconds
25
+ HTTP_TIMEOUT = 5
26
+
27
+ # Create a constant for resource semantic conventions
28
+ RESOURCE = ::OpenTelemetry::SemanticConventions::Resource
29
+
30
+ def detect
31
+ # Return empty resource if not running on ECS
32
+ metadata_uri = ENV.fetch('ECS_CONTAINER_METADATA_URI', nil)
33
+ metadata_uri_v4 = ENV.fetch('ECS_CONTAINER_METADATA_URI_V4', nil)
34
+
35
+ return ::OpenTelemetry::SDK::Resources::Resource.create({}) if metadata_uri.nil? && metadata_uri_v4.nil?
36
+
37
+ resource_attributes = {}
38
+ container_id = fetch_container_id
39
+
40
+ # Base ECS resource attributes
41
+ resource_attributes[RESOURCE::CLOUD_PROVIDER] = 'aws'
42
+ resource_attributes[RESOURCE::CLOUD_PLATFORM] = 'aws_ecs'
43
+ resource_attributes[RESOURCE::CONTAINER_NAME] = Socket.gethostname
44
+ resource_attributes[RESOURCE::CONTAINER_ID] = container_id unless container_id.empty?
45
+
46
+ # If v4 endpoint is not available, return basic resource
47
+ return ::OpenTelemetry::SDK::Resources::Resource.create(resource_attributes) if metadata_uri_v4.nil?
48
+
49
+ begin
50
+ # Fetch container and task metadata
51
+ container_metadata = JSON.parse(http_get(metadata_uri_v4.to_s))
52
+ task_metadata = JSON.parse(http_get("#{metadata_uri_v4}/task"))
53
+
54
+ task_arn = task_metadata['TaskARN']
55
+ base_arn = task_arn[0..task_arn.rindex(':') - 1]
56
+
57
+ cluster = task_metadata['Cluster']
58
+ cluster_arn = cluster.start_with?('arn:') ? cluster : "#{base_arn}:cluster/#{cluster}"
59
+
60
+ # Set ECS-specific attributes
61
+ resource_attributes[RESOURCE::AWS_ECS_CONTAINER_ARN] = container_metadata['ContainerARN']
62
+ resource_attributes[RESOURCE::AWS_ECS_CLUSTER_ARN] = cluster_arn
63
+ resource_attributes[RESOURCE::AWS_ECS_LAUNCHTYPE] = task_metadata['LaunchType'].downcase
64
+ resource_attributes[RESOURCE::AWS_ECS_TASK_ARN] = task_arn
65
+ resource_attributes[RESOURCE::AWS_ECS_TASK_FAMILY] = task_metadata['Family']
66
+ resource_attributes[RESOURCE::AWS_ECS_TASK_REVISION] = task_metadata['Revision']
67
+
68
+ # Add logging attributes if awslogs is used
69
+ logs_attributes = get_logs_resource(container_metadata)
70
+ resource_attributes.merge!(logs_attributes)
71
+ rescue StandardError => e
72
+ ::OpenTelemetry.handle_error(exception: e, message: 'ECS resource detection failed')
73
+ return ::OpenTelemetry::SDK::Resources::Resource.create({})
74
+ end
75
+
76
+ # Filter out nil or empty values
77
+ resource_attributes.delete_if { |_key, value| value.nil? || value.empty? }
78
+ ::OpenTelemetry::SDK::Resources::Resource.create(resource_attributes)
79
+ end
80
+
81
+ private
82
+
83
+ # Fetches container ID from /proc/self/cgroup file
84
+ #
85
+ # @return [String] The container ID or empty string if not found
86
+ def fetch_container_id
87
+ begin
88
+ File.open('/proc/self/cgroup', 'r') do |file|
89
+ file.each_line do |line|
90
+ line = line.strip
91
+ # Look for container ID (64 chars) at the end of the line
92
+ return line[-CONTAINER_ID_LENGTH..] if line.length > CONTAINER_ID_LENGTH
93
+ end
94
+ end
95
+ rescue Errno::ENOENT => e
96
+ ::OpenTelemetry.handle_error(exception: e, message: 'Failed to get container ID on ECS')
97
+ end
98
+
99
+ ''
100
+ end
101
+
102
+ # Extracting logging-related resource attributes
103
+ #
104
+ # @param container_metadata [Hash] Container metadata from ECS metadata endpoint
105
+ # @returhn [Hash] Resource attributes for logging configuration
106
+ def get_logs_resource(container_metadata)
107
+ log_attributes = {}
108
+
109
+ if container_metadata['LogDriver'] == 'awslogs'
110
+ log_options = container_metadata['LogOptions']
111
+
112
+ if log_options
113
+ logs_region = log_options['awslogs-region']
114
+ logs_group_name = log_options['awslogs-group']
115
+ logs_stream_name = log_options['awslogs-stream']
116
+
117
+ container_arn = container_metadata['ContainerARN']
118
+
119
+ # Parse region from ARN if not specified in log options
120
+ if logs_region.nil? || logs_region.empty?
121
+ region_match = container_arn.match(/arn:aws:ecs:([^:]+):.*/)
122
+ logs_region = region_match[1] if region_match
123
+ end
124
+
125
+ # Parse account ID from ARN
126
+ account_match = container_arn.match(/arn:aws:ecs:[^:]+:([^:]+):.*/)
127
+ aws_account = account_match[1] if account_match
128
+
129
+ logs_group_arn = nil
130
+ logs_stream_arn = nil
131
+
132
+ if logs_region && aws_account
133
+ logs_group_arn = "arn:aws:logs:#{logs_region}:#{aws_account}:log-group:#{logs_group_name}" if logs_group_name
134
+
135
+ 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
136
+ end
137
+
138
+ log_attributes[RESOURCE::AWS_LOG_GROUP_NAMES] = [logs_group_name].compact
139
+ log_attributes[RESOURCE::AWS_LOG_GROUP_ARNS] = [logs_group_arn].compact
140
+ log_attributes[RESOURCE::AWS_LOG_STREAM_NAMES] = [logs_stream_name].compact
141
+ log_attributes[RESOURCE::AWS_LOG_STREAM_ARNS] = [logs_stream_arn].compact
142
+ else
143
+ ::OpenTelemetry.handle_error(message: 'The metadata endpoint v4 has returned \'awslogs\' as \'LogDriver\', but there is no \'LogOptions\' data')
144
+ end
145
+ end
146
+
147
+ log_attributes
148
+ end
149
+
150
+ # Makes an HTTP GET request to the specified URL
151
+ #
152
+ # @param url [String] The URL to request
153
+ # @return [String] The response body
154
+ def http_get(url)
155
+ uri = URI.parse(url)
156
+ request = Net::HTTP::Get.new(uri)
157
+
158
+ http = Net::HTTP.new(uri.host, uri.port)
159
+ http.open_timeout = HTTP_TIMEOUT
160
+ http.read_timeout = HTTP_TIMEOUT
161
+
162
+ ::OpenTelemetry::Common::Utilities.untraced do
163
+ response = http.request(request)
164
+ raise "HTTP request failed with status #{response.code}" unless response.is_a?(Net::HTTPSuccess)
165
+
166
+ response.body
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end