instana 2.7.2 → 2.8.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 +152 -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/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: 80154e97601cca5ffd4537ecc7f664c97f27de2f62c0d72f0951d75749ddb5db
|
|
4
|
+
data.tar.gz: 61db3e2ab8805be8ed97da7f0b9d0fd24927f8f68a98d7c82075d69859db217a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 29c643217182cc683c2c770c72fe74d5d3acb84d39766f38363dbd250d6469d49bbf0d00fb9987af600af66b0afec944063d79bff17da91b8dd9f68d9a2eeb1a
|
|
7
|
+
data.tar.gz: 262ab506d671a71f4eab35078717ff27fedaa46eebaa1cb8cc01eeb6e01a11ce30ea3fec7709b68db246b918bc822ad5d54e971271903a3c99c4c25ef9272433
|
|
@@ -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,7 @@ 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?
|
|
64
82
|
|
|
65
83
|
@config[:post_fork_proc] = proc { ::Instana.agent.spawn_background_thread }
|
|
66
84
|
|
|
@@ -94,7 +112,7 @@ module Instana
|
|
|
94
112
|
# Priority: Environment variables > YAML file > Agent discovery > Defaults
|
|
95
113
|
def read_span_stack_config
|
|
96
114
|
# Try environment variables first
|
|
97
|
-
if ENV
|
|
115
|
+
if ENV.fetch('INSTANA_STACK_TRACE', nil) || ENV.fetch('INSTANA_STACK_TRACE_LENGTH', nil)
|
|
98
116
|
read_span_stack_config_from_env
|
|
99
117
|
@config[:back_trace_technologies] = {}
|
|
100
118
|
return
|
|
@@ -122,6 +140,8 @@ module Instana
|
|
|
122
140
|
|
|
123
141
|
# Read stack trace configuration from agent if not already set from YAML or env
|
|
124
142
|
read_span_stack_config_from_agent(tracing_config) if should_read_from_agent?(:back_trace)
|
|
143
|
+
# Read OTLP configuration from agent if not already set from YAML or env
|
|
144
|
+
read_otlp_config_from_agent(tracing_config) if should_read_from_agent?(:otlp)
|
|
125
145
|
# Read span filtering configuration from agent
|
|
126
146
|
::Instana.span_filtering_config&.read_config_from_agent(discovery)
|
|
127
147
|
rescue => e
|
|
@@ -143,17 +163,17 @@ module Instana
|
|
|
143
163
|
# Read stack trace configuration from YAML file
|
|
144
164
|
# Returns hash with :global and :technologies keys or nil if not found
|
|
145
165
|
def read_span_stack_config_from_yaml
|
|
146
|
-
config_path = ENV
|
|
166
|
+
config_path = ENV.fetch('INSTANA_CONFIG_PATH', nil)
|
|
147
167
|
return nil unless config_path && File.exist?(config_path)
|
|
148
168
|
|
|
149
169
|
begin
|
|
150
170
|
yaml_content = YAML.safe_load(File.read(config_path))
|
|
151
171
|
|
|
152
172
|
# Support both "tracing" and "com.instana.tracing" as top-level keys
|
|
153
|
-
if yaml_content[
|
|
154
|
-
::Instana.logger.warn(
|
|
173
|
+
if yaml_content[LEGACY_TRACING_KEY]
|
|
174
|
+
::Instana.logger.warn("Please use \"#{TRACING_KEY}\" instead of \"#{LEGACY_TRACING_KEY}\"")
|
|
155
175
|
end
|
|
156
|
-
tracing_config = yaml_content[
|
|
176
|
+
tracing_config = yaml_content[TRACING_KEY] || yaml_content[LEGACY_TRACING_KEY]
|
|
157
177
|
return nil unless tracing_config
|
|
158
178
|
|
|
159
179
|
result = {}
|
|
@@ -178,8 +198,8 @@ module Instana
|
|
|
178
198
|
# Read stack trace configuration from environment variables
|
|
179
199
|
def read_span_stack_config_from_env
|
|
180
200
|
@config[:back_trace] = {
|
|
181
|
-
stack_trace_level: ENV
|
|
182
|
-
stack_trace_length: ENV
|
|
201
|
+
stack_trace_level: ENV.fetch('INSTANA_STACK_TRACE', 'error'),
|
|
202
|
+
stack_trace_length: ENV.fetch('INSTANA_STACK_TRACE_LENGTH', 30).to_i,
|
|
183
203
|
config_source: 'env'
|
|
184
204
|
}
|
|
185
205
|
end
|
|
@@ -217,8 +237,128 @@ module Instana
|
|
|
217
237
|
}
|
|
218
238
|
end
|
|
219
239
|
|
|
240
|
+
# Read OTLP configuration from agent discovery
|
|
241
|
+
# @param tracing_config [Hash] The tracing configuration from discovery
|
|
242
|
+
def read_otlp_config_from_agent(tracing_config)
|
|
243
|
+
otlp_config = tracing_config['otlp']
|
|
244
|
+
return unless otlp_config.is_a?(Hash)
|
|
245
|
+
|
|
246
|
+
@config[:otlp][:enabled] = truthy?(otlp_config['enabled']) unless otlp_config['enabled'].nil?
|
|
247
|
+
@config[:otlp][:endpoint] = otlp_config['endpoint'] if otlp_config['endpoint']
|
|
248
|
+
@config[:otlp][:timeout] = otlp_config['timeout'].to_i if otlp_config['timeout']
|
|
249
|
+
@config[:otlp][:compression] = otlp_config['compression'] if otlp_config['compression']
|
|
250
|
+
@config[:otlp][:headers] = otlp_config['headers'] if otlp_config['headers'].is_a?(Hash)
|
|
251
|
+
@config[:otlp][:certificate] = otlp_config['certificate'] if otlp_config['certificate']
|
|
252
|
+
@config[:otlp][:client_key] = otlp_config['client_key'] if otlp_config['client_key']
|
|
253
|
+
@config[:otlp][:client_certificate] = otlp_config['client_certificate'] if otlp_config['client_certificate']
|
|
254
|
+
@config[:otlp][:config_source] = 'agent'
|
|
255
|
+
end
|
|
256
|
+
|
|
220
257
|
private
|
|
221
258
|
|
|
259
|
+
# Read OTLP configuration — precedence: YAML > env vars > defaults (agent handled separately)
|
|
260
|
+
def read_otlp_config
|
|
261
|
+
# Try YAML first
|
|
262
|
+
yaml_otlp = parse_otlp_config_from_yaml
|
|
263
|
+
if yaml_otlp
|
|
264
|
+
@config[:otlp].merge!(yaml_otlp)
|
|
265
|
+
@config[:otlp][:config_source] = 'yaml'
|
|
266
|
+
return
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Try environment variables
|
|
270
|
+
env_otlp = parse_otlp_config_from_env
|
|
271
|
+
if env_otlp
|
|
272
|
+
@config[:otlp].merge!(env_otlp)
|
|
273
|
+
@config[:otlp][:config_source] = 'env'
|
|
274
|
+
end
|
|
275
|
+
# Otherwise leave defaults ('default' config_source), agent can update later
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Parse OTLP config from YAML file at INSTANA_CONFIG_PATH under tracing.otlp
|
|
279
|
+
# @return [Hash, nil] merged OTLP settings or nil if not found
|
|
280
|
+
def parse_otlp_config_from_yaml
|
|
281
|
+
config_path = ENV.fetch('INSTANA_CONFIG_PATH', nil)
|
|
282
|
+
return nil unless config_path && File.exist?(config_path)
|
|
283
|
+
|
|
284
|
+
begin
|
|
285
|
+
yaml_content = YAML.safe_load(File.read(config_path))
|
|
286
|
+
tracing_config = yaml_content[TRACING_KEY] || yaml_content[LEGACY_TRACING_KEY]
|
|
287
|
+
return nil unless tracing_config
|
|
288
|
+
|
|
289
|
+
otlp_yaml = tracing_config['otlp']
|
|
290
|
+
return nil unless otlp_yaml.is_a?(Hash)
|
|
291
|
+
|
|
292
|
+
build_otlp_yaml_result(otlp_yaml)
|
|
293
|
+
rescue => e
|
|
294
|
+
::Instana.logger.warn("Failed to load OTLP configuration from YAML: #{e.message}")
|
|
295
|
+
nil
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def build_otlp_yaml_result(otlp_yaml)
|
|
300
|
+
result = {}
|
|
301
|
+
result[:enabled] = truthy?(otlp_yaml['enabled']) unless otlp_yaml['enabled'].nil?
|
|
302
|
+
result[:endpoint] = otlp_yaml['endpoint'] if otlp_yaml['endpoint']
|
|
303
|
+
result[:timeout] = otlp_yaml['timeout'].to_i if otlp_yaml['timeout']
|
|
304
|
+
result[:compression] = otlp_yaml['compression'] if otlp_yaml['compression']
|
|
305
|
+
result[:headers] = otlp_yaml['headers'] if otlp_yaml['headers'].is_a?(Hash)
|
|
306
|
+
result[:certificate] = otlp_yaml['certificate'] if otlp_yaml['certificate']
|
|
307
|
+
result[:client_key] = otlp_yaml['client_key'] if otlp_yaml['client_key']
|
|
308
|
+
result[:client_certificate] = otlp_yaml['client_certificate'] if otlp_yaml['client_certificate']
|
|
309
|
+
result.empty? ? nil : result
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Parse OTLP config from environment variables
|
|
313
|
+
# @return [Hash, nil] merged OTLP settings or nil if no relevant env vars are set
|
|
314
|
+
def parse_otlp_config_from_env
|
|
315
|
+
raw = otlp_env_vars
|
|
316
|
+
return nil if raw.values.all?(&:nil?)
|
|
317
|
+
|
|
318
|
+
result = {}
|
|
319
|
+
result[:enabled] = truthy?(raw[:enabled_raw]) unless raw[:enabled_raw].nil?
|
|
320
|
+
result[:endpoint] = raw[:endpoint] if raw[:endpoint]
|
|
321
|
+
result[:timeout] = raw[:timeout_raw].to_i if raw[:timeout_raw]
|
|
322
|
+
result[:compression] = raw[:compression] if raw[:compression]
|
|
323
|
+
result[:headers] = parse_otlp_headers(raw[:headers_raw]) if raw[:headers_raw]
|
|
324
|
+
result[:certificate] = raw[:certificate] if raw[:certificate]
|
|
325
|
+
result[:client_key] = raw[:client_key] if raw[:client_key]
|
|
326
|
+
result[:client_certificate] = raw[:client_cert] if raw[:client_cert]
|
|
327
|
+
result
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Collect raw OTLP-related environment variable values into a single hash
|
|
331
|
+
# @return [Hash]
|
|
332
|
+
def otlp_env_vars
|
|
333
|
+
{
|
|
334
|
+
enabled_raw: ENV.fetch('INSTANA_TRACING_OTLP_ENABLED', nil),
|
|
335
|
+
endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', nil) || ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil),
|
|
336
|
+
timeout_raw: ENV.fetch('OTEL_EXPORTER_OTLP_TIMEOUT', nil),
|
|
337
|
+
compression: ENV.fetch('OTEL_EXPORTER_OTLP_COMPRESSION', nil),
|
|
338
|
+
headers_raw: ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_HEADERS', nil) || ENV.fetch('OTEL_EXPORTER_OTLP_HEADERS', nil),
|
|
339
|
+
certificate: ENV.fetch('OTEL_EXPORTER_OTLP_CERTIFICATE', nil),
|
|
340
|
+
client_key: ENV.fetch('OTEL_EXPORTER_OTLP_CLIENT_KEY', nil),
|
|
341
|
+
client_cert: ENV.fetch('OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE', nil)
|
|
342
|
+
}
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Parse OTEL_EXPORTER_OTLP_HEADERS value (comma-separated key=value pairs) into a Hash
|
|
346
|
+
# @param headers_str [String] e.g. "api-key=secret,x-tenant=tenant1"
|
|
347
|
+
# @return [Hash]
|
|
348
|
+
def parse_otlp_headers(headers_str)
|
|
349
|
+
return {} unless headers_str
|
|
350
|
+
|
|
351
|
+
headers_str.split(',').each_with_object({}) do |pair, hash|
|
|
352
|
+
key, value = pair.split('=', 2)
|
|
353
|
+
hash[key.strip] = value&.strip if key
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Normalise a truthy string value to a boolean
|
|
358
|
+
def truthy?(value)
|
|
359
|
+
%w[true 1 yes].include?(value.to_s.downcase)
|
|
360
|
+
end
|
|
361
|
+
|
|
222
362
|
# Parse global stack trace configuration from a config hash
|
|
223
363
|
# @param global_config [Hash] The global configuration hash
|
|
224
364
|
# @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
|
|
@@ -0,0 +1,78 @@
|
|
|
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/server'
|
|
8
|
+
|
|
9
|
+
module Instana
|
|
10
|
+
module Exporter
|
|
11
|
+
module Otlp
|
|
12
|
+
class BackgroundJobConverter < BaseConverter
|
|
13
|
+
# Build OTel-compliant span name for background-job spans
|
|
14
|
+
#
|
|
15
|
+
# Formula per SPAN_NAME_PATTERNS.txt Section 3 (sidekiq / resque):
|
|
16
|
+
# client/producer → "{queue} publish"
|
|
17
|
+
# worker/consumer → "{queue} process"
|
|
18
|
+
#
|
|
19
|
+
# @return [String] The span name
|
|
20
|
+
def span_name
|
|
21
|
+
span_type = span[:n].to_s
|
|
22
|
+
data_key = span_type.to_sym
|
|
23
|
+
job_data = span[data_key] || span[:data]&.[](data_key) || {}
|
|
24
|
+
|
|
25
|
+
queue = job_data[:queue] || job_data['queue']
|
|
26
|
+
queue = queue.to_s.strip
|
|
27
|
+
|
|
28
|
+
operation = span_type.end_with?('-client') ? 'publish' : 'process'
|
|
29
|
+
queue.empty? ? operation : "#{queue} #{operation}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def convert_attributes
|
|
33
|
+
attributes = {}
|
|
34
|
+
span_type = span[:n].to_s
|
|
35
|
+
|
|
36
|
+
if span_type == 'sidekiq-client' # rubocop:disable Style/CaseLikeIf
|
|
37
|
+
convert_job_attributes(attributes, span[:'sidekiq-client'] || span[:data]&.[](:'sidekiq-client'), 'sidekiq', 'publish')
|
|
38
|
+
elsif span_type == 'sidekiq-worker'
|
|
39
|
+
convert_job_attributes(attributes, span[:'sidekiq-worker'] || span[:data]&.[](:'sidekiq-worker'), 'sidekiq', 'process')
|
|
40
|
+
elsif span_type == 'resque-client'
|
|
41
|
+
convert_job_attributes(attributes, span[:'resque-client'] || span[:data]&.[](:'resque-client'), 'resque', 'publish')
|
|
42
|
+
elsif span_type == 'resque-worker'
|
|
43
|
+
convert_job_attributes(attributes, span[:'resque-worker'] || span[:data]&.[](:'resque-worker'), 'resque', 'process')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
attributes
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def convert_job_attributes(attributes, data, system, operation)
|
|
52
|
+
return unless data
|
|
53
|
+
|
|
54
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, system)
|
|
55
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_DESTINATION_NAME, data[:queue] || data['queue'])
|
|
56
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION, operation)
|
|
57
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_MESSAGE_ID, data[:job_id] || data['job_id'])
|
|
58
|
+
add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_CONSUMER_GROUP_NAME, data[:job] || data['job'])
|
|
59
|
+
add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, extract_host(data[:'redis-url'] || data['redis-url']))
|
|
60
|
+
add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, extract_port(data[:'redis-url'] || data['redis-url']))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def extract_host(connection)
|
|
64
|
+
return nil unless connection
|
|
65
|
+
|
|
66
|
+
connection.to_s.split(':').first
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def extract_port(connection)
|
|
70
|
+
return nil unless connection
|
|
71
|
+
|
|
72
|
+
port = connection.to_s.split(':').last
|
|
73
|
+
port.to_i if port =~ /^\d+$/
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|