instana 2.7.2 → 2.9.0
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.
- checksums.yaml +4 -4
- data/lib/instana/backend/gc_snapshot.rb +4 -4
- data/lib/instana/backend/host_agent_reporting_observer.rb +60 -5
- data/lib/instana/config.rb +227 -12
- data/lib/instana/exporter/otlp/aws_converter.rb +148 -0
- data/lib/instana/exporter/otlp/background_job_converter.rb +78 -0
- data/lib/instana/exporter/otlp/base_converter.rb +418 -0
- data/lib/instana/exporter/otlp/converter_factory.rb +135 -0
- data/lib/instana/exporter/otlp/custom_converter.rb +42 -0
- data/lib/instana/exporter/otlp/database_converter.rb +153 -0
- data/lib/instana/exporter/otlp/graphql_converter.rb +66 -0
- data/lib/instana/exporter/otlp/http_converter.rb +129 -0
- data/lib/instana/exporter/otlp/messaging_converter.rb +81 -0
- data/lib/instana/exporter/otlp/rails_converter.rb +114 -0
- data/lib/instana/exporter/otlp/resource.rb +403 -0
- data/lib/instana/exporter/otlp/rpc_converter.rb +102 -0
- data/lib/instana/instrumentation/excon.rb +8 -5
- data/lib/instana/instrumentation/net-http.rb +3 -3
- data/lib/instana/instrumentation/rest-client.rb +13 -1
- data/lib/instana/util.rb +35 -0
- data/lib/instana/version.rb +1 -1
- metadata +42 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52139723659935eb43ab59f60e5c0f7305d860d5457c9545d395f9e4576fc604
|
|
4
|
+
data.tar.gz: 223dcdbe22af35ceed817411b850114f9cf3e7eb5fce3b3fc1b4f5ffa29c00e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0743a4a453182c90abb623189d4a6e5105cd03e3823b3b6cf9ee5269c80f14652d21bfc5481c914457a79a8a52633c718cfd1581d219122327996cb67c2b222f
|
|
7
|
+
data.tar.gz: 767f9eb2f714e83e045f27086061ef11ab3511574b8636c87740595f871a0051ccc977341e0e523f174d4f4d5a4575fbcd3d8b87ed1ab26b347198fbdb3d4d94
|
|
@@ -17,18 +17,18 @@ module Instana
|
|
|
17
17
|
@last_minor_count = 0
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def report
|
|
20
|
+
def report(poll_rate = 1)
|
|
21
21
|
stats = ::GC.stat
|
|
22
22
|
total_time = ::GC::Profiler.total_time * 1000
|
|
23
23
|
|
|
24
24
|
::GC::Profiler.clear
|
|
25
25
|
|
|
26
26
|
payload = {
|
|
27
|
-
totalTime: total_time,
|
|
27
|
+
totalTime: total_time / poll_rate,
|
|
28
28
|
heap_live: stats[:heap_live_slots] || stats[:heap_live_num],
|
|
29
29
|
heap_free: stats[:heap_free_slots] || stats[:heap_free_num],
|
|
30
|
-
minorGcs: stats[:minor_gc_count] - @last_minor_count,
|
|
31
|
-
majorGcs: stats[:major_gc_count] - @last_major_count
|
|
30
|
+
minorGcs: (stats[:minor_gc_count] - @last_minor_count) / poll_rate.to_f,
|
|
31
|
+
majorGcs: (stats[:major_gc_count] - @last_major_count) / poll_rate.to_f
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
@last_major_count = stats[:major_gc_count]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# (c) Copyright IBM Corp. 2021
|
|
2
2
|
# (c) Copyright Instana Inc. 2021
|
|
3
|
+
require 'opentelemetry/exporter/otlp'
|
|
4
|
+
require_relative '../exporter/otlp/converter_factory'
|
|
3
5
|
|
|
4
6
|
module Instana
|
|
5
7
|
module Backend
|
|
@@ -22,7 +24,7 @@ module Instana
|
|
|
22
24
|
@timer_class = timer_class
|
|
23
25
|
@nonce = Time.now
|
|
24
26
|
@processor = processor
|
|
25
|
-
|
|
27
|
+
initialize_otlp_exporter
|
|
26
28
|
# Initialize timers with default 1 second interval
|
|
27
29
|
@metrics_timer = @timer_class.new(execution_interval: 1, run_now: true) { report_metrics_to_backend }
|
|
28
30
|
@traces_timer = @timer_class.new(execution_interval: 1, run_now: true) { report_traces_to_backend }
|
|
@@ -36,6 +38,8 @@ module Instana
|
|
|
36
38
|
if new_version.nil?
|
|
37
39
|
@metrics_timer&.shutdown
|
|
38
40
|
@traces_timer&.shutdown
|
|
41
|
+
@otlp_exporter&.shutdown
|
|
42
|
+
@otlp_exporter = nil
|
|
39
43
|
else
|
|
40
44
|
# Read poll_rate from discovery payload - it's nested under plugin.ruby.poll_rate
|
|
41
45
|
discovery = @discovery.value
|
|
@@ -90,10 +94,22 @@ module Instana
|
|
|
90
94
|
path = format(TRACES_DATA_URL, discovery['pid'])
|
|
91
95
|
|
|
92
96
|
@processor.send do |spans|
|
|
93
|
-
|
|
97
|
+
success = false
|
|
98
|
+
if @otlp_exporter
|
|
99
|
+
converted_spans = spans.map do |span|
|
|
100
|
+
::Instana::Exporter::Otlp::ConverterFactory.create(span).convert
|
|
101
|
+
end
|
|
102
|
+
result_code = @otlp_exporter.export(converted_spans)
|
|
103
|
+
Instana.logger.debug("Using OTLP Exporter to export result code: #{result_code}")
|
|
104
|
+
success = result_code == OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
105
|
+
else
|
|
106
|
+
response = @client.send_request('POST', path, spans)
|
|
107
|
+
Instana.logger.debug("Using Instana Native Exporter to export result code: #{response}")
|
|
108
|
+
success = response&.ok?
|
|
109
|
+
end
|
|
94
110
|
|
|
95
|
-
unless
|
|
96
|
-
@logger.warn("Failed to send `#{spans.count}` spans to `#{path}`.
|
|
111
|
+
unless success
|
|
112
|
+
@logger.warn("Failed to send `#{spans.count}` spans to `#{path}`.")
|
|
97
113
|
trigger_rediscovery
|
|
98
114
|
break
|
|
99
115
|
end
|
|
@@ -145,7 +161,8 @@ module Instana
|
|
|
145
161
|
end
|
|
146
162
|
|
|
147
163
|
if ::Instana.config[:metrics][:gc][:enabled]
|
|
148
|
-
|
|
164
|
+
poll_rate = discovery&.dig('plugin', 'ruby', 'poll_rate') || 1
|
|
165
|
+
payload[:gc] = GCSnapshot.instance.report(poll_rate)
|
|
149
166
|
end
|
|
150
167
|
|
|
151
168
|
if ::Instana.config[:metrics][:thread][:enabled]
|
|
@@ -159,6 +176,44 @@ module Instana
|
|
|
159
176
|
@discovery.swap { nil }
|
|
160
177
|
::Instana.agent.announce
|
|
161
178
|
end
|
|
179
|
+
|
|
180
|
+
def initialize_otlp_exporter
|
|
181
|
+
config = ::Instana.config[:otlp]
|
|
182
|
+
unless config[:enabled]
|
|
183
|
+
@otlp_exporter = nil
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
endpoint = resolve_otlp_endpoint(config[:endpoint], config[:config_source])
|
|
188
|
+
opts = { endpoint: endpoint, timeout: config[:timeout] / 1000.0 }
|
|
189
|
+
opts[:compression] = config[:compression] if config[:compression]
|
|
190
|
+
opts[:headers] = config[:headers] if config[:headers]&.any?
|
|
191
|
+
opts[:certificate_file] = config[:certificate] if config[:certificate]
|
|
192
|
+
opts[:client_certificate_file] = config[:client_certificate] if config[:client_certificate]
|
|
193
|
+
opts[:client_key_file] = config[:client_key] if config[:client_key]
|
|
194
|
+
|
|
195
|
+
@otlp_exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(**opts)
|
|
196
|
+
rescue StandardError => e
|
|
197
|
+
@logger.error("Failed to initialize OTLP exporter: #{e.message}")
|
|
198
|
+
@otlp_exporter = nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Derive the OTLP endpoint from the discovered agent host when no explicit
|
|
202
|
+
# endpoint has been configured (config_source == 'default').
|
|
203
|
+
OTLP_DEFAULT_PORT = 4318
|
|
204
|
+
OTLP_TRACES_PATH = '/v1/traces'.freeze
|
|
205
|
+
|
|
206
|
+
def resolve_otlp_endpoint(endpoint, config_source)
|
|
207
|
+
return endpoint unless config_source == 'default'
|
|
208
|
+
|
|
209
|
+
# Use the host that was discovered by HostAgentLookup (same host the
|
|
210
|
+
# metrics/traces client is already talking to) and append the standard
|
|
211
|
+
# OTLP HTTP port and traces path.
|
|
212
|
+
agent_host = @client&.host
|
|
213
|
+
return endpoint unless agent_host
|
|
214
|
+
|
|
215
|
+
"http://#{agent_host}:#{OTLP_DEFAULT_PORT}#{OTLP_TRACES_PATH}"
|
|
216
|
+
end
|
|
162
217
|
end
|
|
163
218
|
end
|
|
164
219
|
end
|
data/lib/instana/config.rb
CHANGED
|
@@ -5,16 +5,20 @@ require 'yaml'
|
|
|
5
5
|
|
|
6
6
|
module Instana
|
|
7
7
|
class Config
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
LEGACY_TRACING_KEY = 'com.instana.tracing'.freeze
|
|
10
|
+
TRACING_KEY = 'tracing'.freeze
|
|
11
|
+
|
|
12
|
+
def initialize(logger: ::Instana.logger, agent_host: ENV.fetch('INSTANA_AGENT_HOST', nil), agent_port: ENV.fetch('INSTANA_AGENT_PORT', nil)) # rubocop:disable Metrics/MethodLength
|
|
9
13
|
@config = {}
|
|
10
14
|
if agent_host
|
|
11
|
-
logger.debug "Using custom agent host location specified in INSTANA_AGENT_HOST (#{
|
|
15
|
+
logger.debug "Using custom agent host location specified in INSTANA_AGENT_HOST (#{agent_host})"
|
|
12
16
|
@config[:agent_host] = agent_host
|
|
13
17
|
else
|
|
14
18
|
@config[:agent_host] = '127.0.0.1'
|
|
15
19
|
end
|
|
16
20
|
if agent_port
|
|
17
|
-
logger.debug "Using custom agent port specified in INSTANA_AGENT_PORT (#{
|
|
21
|
+
logger.debug "Using custom agent port specified in INSTANA_AGENT_PORT (#{agent_port})"
|
|
18
22
|
@config[:agent_port] = agent_port
|
|
19
23
|
else
|
|
20
24
|
@config[:agent_port] = 42699
|
|
@@ -30,7 +34,7 @@ module Instana
|
|
|
30
34
|
@config[:tracing] = { :enabled => true }
|
|
31
35
|
|
|
32
36
|
# Enable/disable tracing exit spans as root spans
|
|
33
|
-
@config[:allow_exit_as_root] = ENV
|
|
37
|
+
@config[:allow_exit_as_root] = ENV.fetch('INSTANA_ALLOW_EXIT_AS_ROOT', nil) == '1'
|
|
34
38
|
|
|
35
39
|
# Enable/Disable logging
|
|
36
40
|
@config[:logging] = { :enabled => true }
|
|
@@ -49,6 +53,20 @@ module Instana
|
|
|
49
53
|
# @config[:back_trace] = { stack_trace_level: nil }
|
|
50
54
|
read_span_stack_config
|
|
51
55
|
|
|
56
|
+
# OTLP exporter configuration (default: disabled)
|
|
57
|
+
@config[:otlp] = {
|
|
58
|
+
enabled: false,
|
|
59
|
+
endpoint: 'http://localhost:4318/v1/traces',
|
|
60
|
+
timeout: 10_000,
|
|
61
|
+
compression: nil,
|
|
62
|
+
headers: {},
|
|
63
|
+
certificate: nil,
|
|
64
|
+
client_key: nil,
|
|
65
|
+
client_certificate: nil,
|
|
66
|
+
config_source: 'default'
|
|
67
|
+
}
|
|
68
|
+
read_otlp_config
|
|
69
|
+
|
|
52
70
|
# By default, collected SQL will be sanitized to remove potentially sensitive bind params such as:
|
|
53
71
|
# > SELECT "blocks".* FROM "blocks" WHERE "blocks"."name" = "Mr. Smith"
|
|
54
72
|
#
|
|
@@ -60,7 +78,11 @@ module Instana
|
|
|
60
78
|
@config[:sanitize_sql] = true
|
|
61
79
|
|
|
62
80
|
# W3C Trace Context Support
|
|
63
|
-
@config[:w3c_trace_correlation] = ENV
|
|
81
|
+
@config[:w3c_trace_correlation] = ENV.fetch('INSTANA_DISABLE_W3C_TRACE_CORRELATION', nil).nil?
|
|
82
|
+
|
|
83
|
+
# HTTP exit span 4xx error classification (opt-in, default: off)
|
|
84
|
+
# Priority: env vars > agent config > default (false / [])
|
|
85
|
+
read_http_exit_config
|
|
64
86
|
|
|
65
87
|
@config[:post_fork_proc] = proc { ::Instana.agent.spawn_background_thread }
|
|
66
88
|
|
|
@@ -94,7 +116,7 @@ module Instana
|
|
|
94
116
|
# Priority: Environment variables > YAML file > Agent discovery > Defaults
|
|
95
117
|
def read_span_stack_config
|
|
96
118
|
# Try environment variables first
|
|
97
|
-
if ENV
|
|
119
|
+
if ENV.fetch('INSTANA_STACK_TRACE', nil) || ENV.fetch('INSTANA_STACK_TRACE_LENGTH', nil)
|
|
98
120
|
read_span_stack_config_from_env
|
|
99
121
|
@config[:back_trace_technologies] = {}
|
|
100
122
|
return
|
|
@@ -122,6 +144,10 @@ module Instana
|
|
|
122
144
|
|
|
123
145
|
# Read stack trace configuration from agent if not already set from YAML or env
|
|
124
146
|
read_span_stack_config_from_agent(tracing_config) if should_read_from_agent?(:back_trace)
|
|
147
|
+
# Read OTLP configuration from agent if not already set from YAML or env
|
|
148
|
+
read_otlp_config_from_agent(tracing_config) if should_read_from_agent?(:otlp)
|
|
149
|
+
# Read HTTP exit 4xx classification from agent if env vars did not already set it
|
|
150
|
+
read_http_exit_classification_from_agent(tracing_config) unless env_http_exit_classification_set?
|
|
125
151
|
# Read span filtering configuration from agent
|
|
126
152
|
::Instana.span_filtering_config&.read_config_from_agent(discovery)
|
|
127
153
|
rescue => e
|
|
@@ -143,17 +169,17 @@ module Instana
|
|
|
143
169
|
# Read stack trace configuration from YAML file
|
|
144
170
|
# Returns hash with :global and :technologies keys or nil if not found
|
|
145
171
|
def read_span_stack_config_from_yaml
|
|
146
|
-
config_path = ENV
|
|
172
|
+
config_path = ENV.fetch('INSTANA_CONFIG_PATH', nil)
|
|
147
173
|
return nil unless config_path && File.exist?(config_path)
|
|
148
174
|
|
|
149
175
|
begin
|
|
150
176
|
yaml_content = YAML.safe_load(File.read(config_path))
|
|
151
177
|
|
|
152
178
|
# Support both "tracing" and "com.instana.tracing" as top-level keys
|
|
153
|
-
if yaml_content[
|
|
154
|
-
::Instana.logger.warn(
|
|
179
|
+
if yaml_content[LEGACY_TRACING_KEY]
|
|
180
|
+
::Instana.logger.warn("Please use \"#{TRACING_KEY}\" instead of \"#{LEGACY_TRACING_KEY}\"")
|
|
155
181
|
end
|
|
156
|
-
tracing_config = yaml_content[
|
|
182
|
+
tracing_config = yaml_content[TRACING_KEY] || yaml_content[LEGACY_TRACING_KEY]
|
|
157
183
|
return nil unless tracing_config
|
|
158
184
|
|
|
159
185
|
result = {}
|
|
@@ -178,8 +204,8 @@ module Instana
|
|
|
178
204
|
# Read stack trace configuration from environment variables
|
|
179
205
|
def read_span_stack_config_from_env
|
|
180
206
|
@config[:back_trace] = {
|
|
181
|
-
stack_trace_level: ENV
|
|
182
|
-
stack_trace_length: ENV
|
|
207
|
+
stack_trace_level: ENV.fetch('INSTANA_STACK_TRACE', 'error'),
|
|
208
|
+
stack_trace_length: ENV.fetch('INSTANA_STACK_TRACE_LENGTH', 30).to_i,
|
|
183
209
|
config_source: 'env'
|
|
184
210
|
}
|
|
185
211
|
end
|
|
@@ -217,8 +243,197 @@ module Instana
|
|
|
217
243
|
}
|
|
218
244
|
end
|
|
219
245
|
|
|
246
|
+
# Read OTLP configuration from agent discovery
|
|
247
|
+
# @param tracing_config [Hash] The tracing configuration from discovery
|
|
248
|
+
def read_otlp_config_from_agent(tracing_config)
|
|
249
|
+
otlp_config = tracing_config['otlp']
|
|
250
|
+
return unless otlp_config.is_a?(Hash)
|
|
251
|
+
|
|
252
|
+
@config[:otlp][:enabled] = truthy?(otlp_config['enabled']) unless otlp_config['enabled'].nil?
|
|
253
|
+
@config[:otlp][:endpoint] = otlp_config['endpoint'] if otlp_config['endpoint']
|
|
254
|
+
@config[:otlp][:timeout] = otlp_config['timeout'].to_i if otlp_config['timeout']
|
|
255
|
+
@config[:otlp][:compression] = otlp_config['compression'] if otlp_config['compression']
|
|
256
|
+
@config[:otlp][:headers] = otlp_config['headers'] if otlp_config['headers'].is_a?(Hash)
|
|
257
|
+
@config[:otlp][:certificate] = otlp_config['certificate'] if otlp_config['certificate']
|
|
258
|
+
@config[:otlp][:client_key] = otlp_config['client_key'] if otlp_config['client_key']
|
|
259
|
+
@config[:otlp][:client_certificate] = otlp_config['client_certificate'] if otlp_config['client_certificate']
|
|
260
|
+
@config[:otlp][:config_source] = 'agent'
|
|
261
|
+
end
|
|
262
|
+
|
|
220
263
|
private
|
|
221
264
|
|
|
265
|
+
# Read HTTP exit 4xx error classification config (wrapper — same pattern as read_span_stack_config / read_otlp_config)
|
|
266
|
+
def read_http_exit_config
|
|
267
|
+
@config[:http_exit_classify_all_4xx_as_errors] = false
|
|
268
|
+
@config[:http_exit_classify_as_errors] = []
|
|
269
|
+
read_http_exit_classification_from_env
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Read HTTP exit 4xx classification from environment variables (highest priority).
|
|
273
|
+
def read_http_exit_classification_from_env
|
|
274
|
+
classify_codes_raw = ENV.fetch('INSTANA_TRACING_HTTP_EXIT_CLASSIFY_AS_ERRORS', nil)
|
|
275
|
+
if classify_codes_raw
|
|
276
|
+
codes = classify_codes_raw.split(',').filter_map do |part|
|
|
277
|
+
part = part.strip
|
|
278
|
+
if part.match?(/\A\d+\z/)
|
|
279
|
+
code = part.to_i
|
|
280
|
+
if (400..499).cover?(code)
|
|
281
|
+
code
|
|
282
|
+
else
|
|
283
|
+
msg = "Ignoring out-of-range value in INSTANA_TRACING_HTTP_EXIT_CLASSIFY_AS_ERRORS: #{code}, must be 400-499"
|
|
284
|
+
::Instana.logger.warn(msg)
|
|
285
|
+
nil
|
|
286
|
+
end
|
|
287
|
+
elsif part != ''
|
|
288
|
+
::Instana.logger.warn("Ignoring non-integer value in INSTANA_TRACING_HTTP_EXIT_CLASSIFY_AS_ERRORS: #{part}")
|
|
289
|
+
nil
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
@config[:http_exit_classify_as_errors] = codes unless codes.empty?
|
|
293
|
+
else
|
|
294
|
+
classify_all_raw = ENV.fetch('INSTANA_TRACING_HTTP_EXIT_CLASSIFY_ALL_4XX_AS_ERRORS', nil)
|
|
295
|
+
@config[:http_exit_classify_all_4xx_as_errors] = truthy?(classify_all_raw) if classify_all_raw
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Return true if env vars have already configured HTTP exit 4xx classification.
|
|
300
|
+
def env_http_exit_classification_set?
|
|
301
|
+
ENV.key?('INSTANA_TRACING_HTTP_EXIT_CLASSIFY_AS_ERRORS') ||
|
|
302
|
+
ENV.key?('INSTANA_TRACING_HTTP_EXIT_CLASSIFY_ALL_4XX_AS_ERRORS')
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Read HTTP exit 4xx classification from agent discovery response (lowest priority).
|
|
306
|
+
# @param tracing_config [Hash] the tracing sub-hash from the discovery payload
|
|
307
|
+
def read_http_exit_classification_from_agent(tracing_config)
|
|
308
|
+
http_cfg = tracing_config['http']
|
|
309
|
+
return unless http_cfg.is_a?(Hash)
|
|
310
|
+
|
|
311
|
+
exit_cfg = http_cfg['exit']
|
|
312
|
+
return unless exit_cfg.is_a?(Hash)
|
|
313
|
+
|
|
314
|
+
if exit_cfg.key?('classify-as-errors')
|
|
315
|
+
codes = Array(exit_cfg['classify-as-errors']).filter_map do |code|
|
|
316
|
+
if code.is_a?(Integer) && (400..499).cover?(code)
|
|
317
|
+
code
|
|
318
|
+
else
|
|
319
|
+
::Instana.logger.warn("Ignoring invalid value in agent config tracing.http.exit.classify-as-errors: #{code}")
|
|
320
|
+
nil
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
@config[:http_exit_classify_as_errors] = codes unless codes.empty?
|
|
324
|
+
elsif exit_cfg.key?('classify-all-4xx-as-errors')
|
|
325
|
+
val = exit_cfg['classify-all-4xx-as-errors']
|
|
326
|
+
if [true, false].include?(val)
|
|
327
|
+
@config[:http_exit_classify_all_4xx_as_errors] = val
|
|
328
|
+
else
|
|
329
|
+
::Instana.logger.warn("Ignoring non-boolean value in agent config tracing.http.exit.classify-all-4xx-as-errors: #{val}")
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Read OTLP configuration — precedence: YAML > env vars > defaults (agent handled separately)
|
|
335
|
+
def read_otlp_config
|
|
336
|
+
# Try YAML first
|
|
337
|
+
yaml_otlp = parse_otlp_config_from_yaml
|
|
338
|
+
if yaml_otlp
|
|
339
|
+
@config[:otlp].merge!(yaml_otlp)
|
|
340
|
+
@config[:otlp][:config_source] = 'yaml'
|
|
341
|
+
return
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Try environment variables
|
|
345
|
+
env_otlp = parse_otlp_config_from_env
|
|
346
|
+
if env_otlp
|
|
347
|
+
@config[:otlp].merge!(env_otlp)
|
|
348
|
+
@config[:otlp][:config_source] = 'env'
|
|
349
|
+
end
|
|
350
|
+
# Otherwise leave defaults ('default' config_source), agent can update later
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Parse OTLP config from YAML file at INSTANA_CONFIG_PATH under tracing.otlp
|
|
354
|
+
# @return [Hash, nil] merged OTLP settings or nil if not found
|
|
355
|
+
def parse_otlp_config_from_yaml
|
|
356
|
+
config_path = ENV.fetch('INSTANA_CONFIG_PATH', nil)
|
|
357
|
+
return nil unless config_path && File.exist?(config_path)
|
|
358
|
+
|
|
359
|
+
begin
|
|
360
|
+
yaml_content = YAML.safe_load(File.read(config_path))
|
|
361
|
+
tracing_config = yaml_content[TRACING_KEY] || yaml_content[LEGACY_TRACING_KEY]
|
|
362
|
+
return nil unless tracing_config
|
|
363
|
+
|
|
364
|
+
otlp_yaml = tracing_config['otlp']
|
|
365
|
+
return nil unless otlp_yaml.is_a?(Hash)
|
|
366
|
+
|
|
367
|
+
build_otlp_yaml_result(otlp_yaml)
|
|
368
|
+
rescue => e
|
|
369
|
+
::Instana.logger.warn("Failed to load OTLP configuration from YAML: #{e.message}")
|
|
370
|
+
nil
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def build_otlp_yaml_result(otlp_yaml)
|
|
375
|
+
result = {}
|
|
376
|
+
result[:enabled] = truthy?(otlp_yaml['enabled']) unless otlp_yaml['enabled'].nil?
|
|
377
|
+
result[:endpoint] = otlp_yaml['endpoint'] if otlp_yaml['endpoint']
|
|
378
|
+
result[:timeout] = otlp_yaml['timeout'].to_i if otlp_yaml['timeout']
|
|
379
|
+
result[:compression] = otlp_yaml['compression'] if otlp_yaml['compression']
|
|
380
|
+
result[:headers] = otlp_yaml['headers'] if otlp_yaml['headers'].is_a?(Hash)
|
|
381
|
+
result[:certificate] = otlp_yaml['certificate'] if otlp_yaml['certificate']
|
|
382
|
+
result[:client_key] = otlp_yaml['client_key'] if otlp_yaml['client_key']
|
|
383
|
+
result[:client_certificate] = otlp_yaml['client_certificate'] if otlp_yaml['client_certificate']
|
|
384
|
+
result.empty? ? nil : result
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Parse OTLP config from environment variables
|
|
388
|
+
# @return [Hash, nil] merged OTLP settings or nil if no relevant env vars are set
|
|
389
|
+
def parse_otlp_config_from_env
|
|
390
|
+
raw = otlp_env_vars
|
|
391
|
+
return nil if raw.values.all?(&:nil?)
|
|
392
|
+
|
|
393
|
+
result = {}
|
|
394
|
+
result[:enabled] = truthy?(raw[:enabled_raw]) unless raw[:enabled_raw].nil?
|
|
395
|
+
result[:endpoint] = raw[:endpoint] if raw[:endpoint]
|
|
396
|
+
result[:timeout] = raw[:timeout_raw].to_i if raw[:timeout_raw]
|
|
397
|
+
result[:compression] = raw[:compression] if raw[:compression]
|
|
398
|
+
result[:headers] = parse_otlp_headers(raw[:headers_raw]) if raw[:headers_raw]
|
|
399
|
+
result[:certificate] = raw[:certificate] if raw[:certificate]
|
|
400
|
+
result[:client_key] = raw[:client_key] if raw[:client_key]
|
|
401
|
+
result[:client_certificate] = raw[:client_cert] if raw[:client_cert]
|
|
402
|
+
result
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Collect raw OTLP-related environment variable values into a single hash
|
|
406
|
+
# @return [Hash]
|
|
407
|
+
def otlp_env_vars
|
|
408
|
+
{
|
|
409
|
+
enabled_raw: ENV.fetch('INSTANA_TRACING_OTLP_ENABLED', nil),
|
|
410
|
+
endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', nil) || ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil),
|
|
411
|
+
timeout_raw: ENV.fetch('OTEL_EXPORTER_OTLP_TIMEOUT', nil),
|
|
412
|
+
compression: ENV.fetch('OTEL_EXPORTER_OTLP_COMPRESSION', nil),
|
|
413
|
+
headers_raw: ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_HEADERS', nil) || ENV.fetch('OTEL_EXPORTER_OTLP_HEADERS', nil),
|
|
414
|
+
certificate: ENV.fetch('OTEL_EXPORTER_OTLP_CERTIFICATE', nil),
|
|
415
|
+
client_key: ENV.fetch('OTEL_EXPORTER_OTLP_CLIENT_KEY', nil),
|
|
416
|
+
client_cert: ENV.fetch('OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE', nil)
|
|
417
|
+
}
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Parse OTEL_EXPORTER_OTLP_HEADERS value (comma-separated key=value pairs) into a Hash
|
|
421
|
+
# @param headers_str [String] e.g. "api-key=secret,x-tenant=tenant1"
|
|
422
|
+
# @return [Hash]
|
|
423
|
+
def parse_otlp_headers(headers_str)
|
|
424
|
+
return {} unless headers_str
|
|
425
|
+
|
|
426
|
+
headers_str.split(',').each_with_object({}) do |pair, hash|
|
|
427
|
+
key, value = pair.split('=', 2)
|
|
428
|
+
hash[key.strip] = value&.strip if key
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Normalise a truthy string value to a boolean
|
|
433
|
+
def truthy?(value)
|
|
434
|
+
%w[true 1 yes].include?(value.to_s.downcase)
|
|
435
|
+
end
|
|
436
|
+
|
|
222
437
|
# Parse global stack trace configuration from a config hash
|
|
223
438
|
# @param global_config [Hash] The global configuration hash
|
|
224
439
|
# @param config_source [String] The source of the configuration ('yaml', 'agent', etc.)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# (c) Copyright IBM Corp. 2026
|
|
4
|
+
|
|
5
|
+
require_relative 'base_converter'
|
|
6
|
+
require 'opentelemetry/semconv/incubating/messaging'
|
|
7
|
+
require 'opentelemetry/semconv/db'
|
|
8
|
+
require 'opentelemetry/semconv/incubating/db'
|
|
9
|
+
|
|
10
|
+
module Instana
|
|
11
|
+
module Exporter
|
|
12
|
+
module Otlp
|
|
13
|
+
# Converter for AWS SDK spans (SQS, SNS, DynamoDB) to OTLP format
|
|
14
|
+
class AwsConverter < BaseConverter
|
|
15
|
+
# Build OTel-compliant span name for AWS SDK spans
|
|
16
|
+
#
|
|
17
|
+
# Formulas per SPAN_NAME_PATTERNS.txt Section 6:
|
|
18
|
+
# SQS send/publish → "{queue} publish"
|
|
19
|
+
# SQS receive/delete → "{queue} receive"
|
|
20
|
+
# SNS → "{topic} publish"
|
|
21
|
+
# DynamoDB → "DynamoDB.{op}" e.g. "DynamoDB.PutItem"
|
|
22
|
+
# S3 → "S3.{op}" e.g. "S3.PutObject"
|
|
23
|
+
# Lambda invoke → "Lambda.{function}"
|
|
24
|
+
#
|
|
25
|
+
# @return [String] The span name
|
|
26
|
+
def span_name
|
|
27
|
+
data = span[:data] || {}
|
|
28
|
+
sqs_span_name(data[:sqs]) ||
|
|
29
|
+
sns_span_name(data[:sns]) ||
|
|
30
|
+
dynamodb_span_name(data[:dynamodb]) ||
|
|
31
|
+
s3_span_name(data[:s3]) ||
|
|
32
|
+
lambda_span_name(data.dig(:aws, :lambda, :invoke)) ||
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def convert_attributes
|
|
37
|
+
attributes = {}
|
|
38
|
+
data = span[:data]
|
|
39
|
+
return attributes unless data
|
|
40
|
+
|
|
41
|
+
convert_sqs_attributes(attributes, data[:sqs])
|
|
42
|
+
convert_sns_attributes(attributes, data[:sns])
|
|
43
|
+
convert_dynamodb_attributes(attributes, data[:dynamodb])
|
|
44
|
+
convert_s3_attributes(attributes, data[:s3])
|
|
45
|
+
convert_lambda_attributes(attributes, data.dig(:aws, :lambda, :invoke))
|
|
46
|
+
|
|
47
|
+
attributes
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def sqs_span_name(sqs)
|
|
53
|
+
return unless sqs
|
|
54
|
+
|
|
55
|
+
queue = sqs[:queue].to_s.strip
|
|
56
|
+
operation = sqs[:type].to_s =~ /^(delete|receive)/ ? 'receive' : 'publish'
|
|
57
|
+
queue.empty? ? "SQS #{operation}" : "#{queue} #{operation}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def sns_span_name(sns)
|
|
61
|
+
return unless sns
|
|
62
|
+
|
|
63
|
+
topic = sns[:topic].to_s.strip
|
|
64
|
+
topic = sns[:target].to_s.strip if topic.empty?
|
|
65
|
+
topic.empty? ? 'SNS publish' : "#{topic} publish"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def dynamodb_span_name(ddb)
|
|
69
|
+
return unless ddb
|
|
70
|
+
|
|
71
|
+
op = ddb[:op].to_s.strip
|
|
72
|
+
op.empty? ? 'DynamoDB' : "DynamoDB.#{op}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def s3_span_name(s3_data)
|
|
76
|
+
return unless s3_data
|
|
77
|
+
|
|
78
|
+
op = s3_data[:op].to_s.strip
|
|
79
|
+
op.empty? ? 'S3' : "S3.#{op}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def lambda_span_name(lambda_data)
|
|
83
|
+
return unless lambda_data
|
|
84
|
+
|
|
85
|
+
fn = lambda_data[:function].to_s.strip
|
|
86
|
+
fn.empty? ? 'Lambda.invoke' : "Lambda.#{fn}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def convert_sqs_attributes(attributes, sqs_data)
|
|
90
|
+
return unless sqs_data
|
|
91
|
+
|
|
92
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, 'aws_sqs')
|
|
93
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_DESTINATION_NAME, sqs_data[:queue])
|
|
94
|
+
add_attribute(attributes, 'messaging.aws.sqs.message_group_id', sqs_data[:group])
|
|
95
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_BATCH_MESSAGE_COUNT, sqs_data[:size])
|
|
96
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_TYPE, sqs_operation_type(sqs_data[:type]))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def sqs_operation_type(type)
|
|
100
|
+
case type.to_s
|
|
101
|
+
when /^send/, /^single\.sync/ then 'send'
|
|
102
|
+
when /^delete/ then 'process'
|
|
103
|
+
when /^create/, /^get/ then 'create'
|
|
104
|
+
else 'send'
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def convert_sns_attributes(attributes, sns_data)
|
|
109
|
+
return unless sns_data
|
|
110
|
+
|
|
111
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, 'aws_sns')
|
|
112
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_DESTINATION_NAME, sns_data[:topic])
|
|
113
|
+
add_attribute(attributes, 'messaging.aws.sns.target_arn', sns_data[:target])
|
|
114
|
+
add_attribute(attributes, 'messaging.aws.sns.phone_number', sns_data[:phone])
|
|
115
|
+
add_attribute(attributes, 'messaging.aws.sns.subject', sns_data[:subject])
|
|
116
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_TYPE, 'send')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def convert_dynamodb_attributes(attributes, dynamodb_data)
|
|
120
|
+
return unless dynamodb_data
|
|
121
|
+
|
|
122
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, 'dynamodb')
|
|
123
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_OPERATION_NAME, dynamodb_data[:op])
|
|
124
|
+
add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, dynamodb_data[:table])
|
|
125
|
+
add_attribute(attributes, 'aws.dynamodb.table_name', dynamodb_data[:table])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def convert_s3_attributes(attributes, s3_data)
|
|
129
|
+
return unless s3_data
|
|
130
|
+
|
|
131
|
+
add_attribute(attributes, 'aws.service', 's3')
|
|
132
|
+
add_attribute(attributes, 'aws.s3.bucket', s3_data[:bucket])
|
|
133
|
+
add_attribute(attributes, 'aws.s3.key', s3_data[:key])
|
|
134
|
+
add_attribute(attributes, 'aws.s3.operation', s3_data[:op])
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def convert_lambda_attributes(attributes, lambda_data)
|
|
138
|
+
return unless lambda_data
|
|
139
|
+
|
|
140
|
+
add_attribute(attributes, 'aws.service', 'lambda')
|
|
141
|
+
add_attribute(attributes, 'aws.lambda.function_name', lambda_data[:function])
|
|
142
|
+
add_attribute(attributes, 'aws.lambda.invocation_type', lambda_data[:type])
|
|
143
|
+
add_attribute(attributes, 'faas.invoked_name', lambda_data[:function])
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|