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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75c9fe7c6b744ebafc10428dbbfdcfe046fdcfc80b31227b29b4a18ade7ec5dd
4
- data.tar.gz: 94b10d48f251f49157203534306399fed404adf619318cdebb0ff1864a74fc41
3
+ metadata.gz: 80154e97601cca5ffd4537ecc7f664c97f27de2f62c0d72f0951d75749ddb5db
4
+ data.tar.gz: 61db3e2ab8805be8ed97da7f0b9d0fd24927f8f68a98d7c82075d69859db217a
5
5
  SHA512:
6
- metadata.gz: 56ff7ee602ef57a060bc1e467e11dc5466469433c54abc6457bb1959c4f5abf82d491b5db272b4cf45bea72335c28a0b9213357c44bf90543c123d424743f9f7
7
- data.tar.gz: 811b2bd02833ec5127fdfeee050cab1ce3c1410e120ecd4452c41207b4c7a4fbc11f390ba8a62181f7a56fd3cd073392ca5115a68bfeb3a200f84ad5812a8ba7
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
- response = @client.send_request('POST', path, spans)
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 response.ok?
96
- @logger.warn("Failed to send `#{spans.count}` spans to `#{path}`. Response: #{response.code} - #{response.body}")
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
- payload[:gc] = GCSnapshot.instance.report
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
@@ -5,16 +5,20 @@ require 'yaml'
5
5
 
6
6
  module Instana
7
7
  class Config
8
- def initialize(logger: ::Instana.logger, agent_host: ENV['INSTANA_AGENT_HOST'], agent_port: ENV['INSTANA_AGENT_PORT'])
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 (#{ENV['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 (#{ENV['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['INSTANA_ALLOW_EXIT_AS_ROOT'] == '1'
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['INSTANA_DISABLE_W3C_TRACE_CORRELATION'].nil?
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['INSTANA_STACK_TRACE'] || ENV['INSTANA_STACK_TRACE_LENGTH']
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['INSTANA_CONFIG_PATH']
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['com.instana.tracing']
154
- ::Instana.logger.warn('Please use "tracing" instead of "com.instana.tracing"')
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['tracing'] || yaml_content['com.instana.tracing']
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['INSTANA_STACK_TRACE'] || 'error',
182
- stack_trace_length: ENV['INSTANA_STACK_TRACE_LENGTH']&.to_i || 30,
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