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
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# (c) Copyright IBM Corp. 2026
|
|
4
|
+
|
|
5
|
+
require_relative 'resource'
|
|
6
|
+
require 'opentelemetry/trace'
|
|
7
|
+
require 'forwardable'
|
|
8
|
+
|
|
9
|
+
module Instana
|
|
10
|
+
module Exporter
|
|
11
|
+
module Otlp
|
|
12
|
+
# Base class for all OTLP span converters
|
|
13
|
+
#
|
|
14
|
+
# Provides common interface and shared functionality for converting Instana spans
|
|
15
|
+
# to OpenTelemetry Protocol (OTLP) compatible span data objects.
|
|
16
|
+
#
|
|
17
|
+
# @abstract Subclasses should override {#convert_attributes} to provide
|
|
18
|
+
# type-specific attribute conversion logic.
|
|
19
|
+
#
|
|
20
|
+
# @example Creating a custom converter
|
|
21
|
+
# class MyConverter < BaseConverter
|
|
22
|
+
# def convert_attributes
|
|
23
|
+
# attributes = {}
|
|
24
|
+
# add_attribute(attributes, 'custom.field', span[:data][:custom][:field])
|
|
25
|
+
# attributes
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
class BaseConverter
|
|
29
|
+
# Represents the instrumentation scope (library) that created the span
|
|
30
|
+
InstrumentationScope = Struct.new(:name, :version)
|
|
31
|
+
|
|
32
|
+
# Represents the status of a span (OK, ERROR, or UNSET)
|
|
33
|
+
Status = Struct.new(:code, :description)
|
|
34
|
+
|
|
35
|
+
# Represents a span event (mirrors OpenTelemetry::SDK::Trace::Event)
|
|
36
|
+
#
|
|
37
|
+
# Fields:
|
|
38
|
+
# name [String] - event name
|
|
39
|
+
# attributes [Hash] - key/value attributes attached to the event
|
|
40
|
+
# timestamp [Integer] - Unix nanoseconds
|
|
41
|
+
Event = Struct.new(:name, :attributes, :timestamp)
|
|
42
|
+
|
|
43
|
+
# Adapter to make resource objects compatible with OTLP exporter expectations
|
|
44
|
+
ResourceAdapter = Struct.new(:attributes) do
|
|
45
|
+
# @return [Enumerator] Iterator over resource attributes
|
|
46
|
+
def attribute_enumerator
|
|
47
|
+
attributes.each
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Plain object that directly implements the interface expected by
|
|
52
|
+
# OpenTelemetry::Exporter::OTLP::Exporter#export
|
|
53
|
+
#
|
|
54
|
+
# This is a simple data structure with the required methods, avoiding
|
|
55
|
+
# unnecessary delegation overhead.
|
|
56
|
+
SpanData = Struct.new(
|
|
57
|
+
:name,
|
|
58
|
+
:trace_id,
|
|
59
|
+
:span_id,
|
|
60
|
+
:parent_span_id,
|
|
61
|
+
:resource,
|
|
62
|
+
:instrumentation_scope,
|
|
63
|
+
:kind,
|
|
64
|
+
:start_timestamp,
|
|
65
|
+
:end_timestamp,
|
|
66
|
+
:attributes,
|
|
67
|
+
:status,
|
|
68
|
+
keyword_init: true
|
|
69
|
+
) do
|
|
70
|
+
# @return [OpenTelemetry::Trace::Tracestate] Default empty tracestate
|
|
71
|
+
def tracestate
|
|
72
|
+
OpenTelemetry::Trace::Tracestate::DEFAULT
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Integer] Number of attributes recorded
|
|
76
|
+
def total_recorded_attributes
|
|
77
|
+
attributes.size
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [Array] Empty array — error-free spans carry no events
|
|
81
|
+
def events
|
|
82
|
+
EMPTY_ARRAY
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Integer] Zero — no events on error-free spans
|
|
86
|
+
def total_recorded_events
|
|
87
|
+
0
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [Array] Empty array (links not currently supported)
|
|
91
|
+
def links
|
|
92
|
+
EMPTY_ARRAY
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [Integer] Zero (links not currently supported)
|
|
96
|
+
def total_recorded_links
|
|
97
|
+
0
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [Boolean] False (remote parent detection not implemented)
|
|
101
|
+
def parent_span_is_remote
|
|
102
|
+
false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @return [OpenTelemetry::Trace::TraceFlags] Default trace flags
|
|
106
|
+
def trace_flags
|
|
107
|
+
OpenTelemetry::Trace::TraceFlags::DEFAULT
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
EMPTY_ARRAY = [].freeze # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# SpanData extended with an optional list of span events
|
|
114
|
+
#
|
|
115
|
+
# We wrap the base struct to carry events without changing the positional
|
|
116
|
+
# keyword_init constructor used by all converters.
|
|
117
|
+
SpanDataWithEvents = Struct.new(:span_data, :span_events) do
|
|
118
|
+
extend Forwardable
|
|
119
|
+
def_delegators :span_data,
|
|
120
|
+
:name, :trace_id, :span_id, :parent_span_id,
|
|
121
|
+
:resource, :instrumentation_scope, :kind,
|
|
122
|
+
:start_timestamp, :end_timestamp, :attributes, :status,
|
|
123
|
+
:tracestate, :total_recorded_attributes,
|
|
124
|
+
:links, :total_recorded_links,
|
|
125
|
+
:parent_span_is_remote, :trace_flags
|
|
126
|
+
|
|
127
|
+
def events
|
|
128
|
+
span_events
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def total_recorded_events
|
|
132
|
+
span_events.size
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Milliseconds to nanoseconds conversion factor
|
|
137
|
+
MS_TO_NS = 1_000_000
|
|
138
|
+
private_constant :MS_TO_NS
|
|
139
|
+
|
|
140
|
+
# @param span [Instana::Trace::Span] The span to convert
|
|
141
|
+
# @param resource [Object, nil] Optional resource information (defaults to global resource)
|
|
142
|
+
def initialize(span, resource = nil)
|
|
143
|
+
@span = span
|
|
144
|
+
@resource = resource || Resource.instance
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Convert the Instana span to OTLP-compatible span data
|
|
148
|
+
#
|
|
149
|
+
# @return [SpanData, SpanDataWithEvents] Converted span data object ready for export
|
|
150
|
+
def convert
|
|
151
|
+
# Resolve error info once — reused by both status and events
|
|
152
|
+
error_count = span[:ec].to_i
|
|
153
|
+
error_msg = error_count.positive? ? extract_error_message : nil
|
|
154
|
+
stacktrace = error_count.positive? ? convert_stack_trace : nil
|
|
155
|
+
|
|
156
|
+
span_data = SpanData.new(
|
|
157
|
+
name: span_name,
|
|
158
|
+
trace_id: format_trace_id(span[:t]),
|
|
159
|
+
span_id: format_span_id(span[:s]),
|
|
160
|
+
parent_span_id: format_parent_span_id,
|
|
161
|
+
resource: resource_adapter,
|
|
162
|
+
instrumentation_scope: instrumentation_scope,
|
|
163
|
+
kind: convert_span_kind,
|
|
164
|
+
start_timestamp: convert_to_unix_nano(span[:ts]),
|
|
165
|
+
end_timestamp: calculate_end_timestamp,
|
|
166
|
+
attributes: convert_attributes,
|
|
167
|
+
status: build_status(error_count, error_msg)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
events = build_error_events(error_count, error_msg, stacktrace)
|
|
171
|
+
return SpanDataWithEvents.new(span_data, events) unless events.empty?
|
|
172
|
+
|
|
173
|
+
span_data
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
protected
|
|
177
|
+
|
|
178
|
+
attr_reader :span, :resource
|
|
179
|
+
|
|
180
|
+
# Format trace ID to the expected 16-byte binary format
|
|
181
|
+
#
|
|
182
|
+
# @param trace_id [String, nil] The trace ID as hex string
|
|
183
|
+
# @return [String] Formatted trace ID as 16-byte binary string
|
|
184
|
+
def format_trace_id(trace_id)
|
|
185
|
+
return OpenTelemetry::Trace::INVALID_TRACE_ID unless trace_id
|
|
186
|
+
|
|
187
|
+
# Pad to 32 hex characters (16 bytes) and convert to binary
|
|
188
|
+
hex_string = trace_id.to_s.rjust(32, '0')
|
|
189
|
+
[hex_string].pack('H*')
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Format span ID to the expected 8-byte binary format
|
|
193
|
+
#
|
|
194
|
+
# @param span_id [String, nil] The span ID as hex string
|
|
195
|
+
# @return [String, nil] Formatted span ID as 8-byte binary string, or nil if input is nil
|
|
196
|
+
def format_span_id(span_id)
|
|
197
|
+
return nil unless span_id
|
|
198
|
+
|
|
199
|
+
# Pad to 16 hex characters (8 bytes) and convert to binary
|
|
200
|
+
hex_string = span_id.to_s.rjust(16, '0')
|
|
201
|
+
[hex_string].pack('H*')
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Convert Instana span kind to OpenTelemetry span kind
|
|
205
|
+
#
|
|
206
|
+
# Instana span kinds:
|
|
207
|
+
# 1 = entry/server
|
|
208
|
+
# 2 = exit/client
|
|
209
|
+
# 3 = intermediate/internal
|
|
210
|
+
#
|
|
211
|
+
# @return [Symbol] One of :server, :client, :internal, :producer, or :consumer
|
|
212
|
+
def convert_span_kind
|
|
213
|
+
# Explicit kind takes precedence
|
|
214
|
+
case span[:k]
|
|
215
|
+
when 1 then :server
|
|
216
|
+
when 2 then :client
|
|
217
|
+
when 3 then :internal
|
|
218
|
+
else
|
|
219
|
+
# Infer from span name if no explicit kind
|
|
220
|
+
infer_span_kind_from_name
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Convert Instana millisecond timestamps to Unix nanoseconds
|
|
225
|
+
#
|
|
226
|
+
# @param time [Time, Integer, nil] The timestamp (Time object or milliseconds since epoch)
|
|
227
|
+
# @return [Integer] Unix timestamp in nanoseconds
|
|
228
|
+
def convert_to_unix_nano(time)
|
|
229
|
+
case time
|
|
230
|
+
when nil
|
|
231
|
+
0
|
|
232
|
+
when Integer
|
|
233
|
+
time * MS_TO_NS
|
|
234
|
+
else
|
|
235
|
+
(time.to_f * 1_000_000_000).to_i
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Build span status from pre-resolved error info
|
|
240
|
+
#
|
|
241
|
+
# @param error_count [Integer] Span error count (span[:ec].to_i)
|
|
242
|
+
# @param error_msg [String, nil] Pre-extracted error message
|
|
243
|
+
# @return [Status]
|
|
244
|
+
def build_status(error_count, error_msg)
|
|
245
|
+
if error_count.positive?
|
|
246
|
+
Status.new(OpenTelemetry::Trace::Status::ERROR, error_msg.to_s)
|
|
247
|
+
else
|
|
248
|
+
Status.new(OpenTelemetry::Trace::Status::UNSET, '')
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Extract error message from span data
|
|
253
|
+
#
|
|
254
|
+
# Searches `span[:data][<type>][:error]` for any span type that
|
|
255
|
+
# carries an error field (e.g. http.error, activerecord.error).
|
|
256
|
+
# Returns the first non-nil value found, truncated to 1024 chars
|
|
257
|
+
# per the OTel status.message recommendation.
|
|
258
|
+
#
|
|
259
|
+
# @return [String, nil] Error message or nil when not present
|
|
260
|
+
def extract_error_message
|
|
261
|
+
data = span[:data]
|
|
262
|
+
return nil unless data.is_a?(Hash)
|
|
263
|
+
|
|
264
|
+
data.each_value do |type_data|
|
|
265
|
+
next unless type_data.is_a?(Hash)
|
|
266
|
+
|
|
267
|
+
msg = type_data[:error]
|
|
268
|
+
return msg.to_s[0, 1024] if msg
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
nil
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Convert Instana stack trace to OTel exception.stacktrace string
|
|
275
|
+
#
|
|
276
|
+
# Instana stores stack frames as an Array of Hashes with keys:
|
|
277
|
+
# c: file path, n: line number, m: method name
|
|
278
|
+
#
|
|
279
|
+
# OTel requires a newline-separated string of stack frames.
|
|
280
|
+
#
|
|
281
|
+
# @return [String, nil] Formatted stacktrace or nil if not present
|
|
282
|
+
def convert_stack_trace
|
|
283
|
+
stack = span[:stack]
|
|
284
|
+
return nil unless stack.is_a?(Array) && !stack.empty?
|
|
285
|
+
|
|
286
|
+
stack.map { |frame| "#{frame[:c]}:#{frame[:n]} in #{frame[:m]}" }.join("\n")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Convert span attributes to OTLP-compatible attributes
|
|
290
|
+
#
|
|
291
|
+
# Subclasses should override this method to provide type-specific
|
|
292
|
+
# attribute conversion logic.
|
|
293
|
+
#
|
|
294
|
+
# @return [Hash] Hash of attribute key-value pairs
|
|
295
|
+
def convert_attributes
|
|
296
|
+
{}
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Build OTel span events from pre-resolved error info
|
|
300
|
+
#
|
|
301
|
+
# - error_count > 0 AND stacktrace present → "exception" event
|
|
302
|
+
# - error_count > 0, no stack → "error" event
|
|
303
|
+
# - error_count == 0 → []
|
|
304
|
+
#
|
|
305
|
+
# @param error_count [Integer]
|
|
306
|
+
# @param error_msg [String, nil]
|
|
307
|
+
# @param stacktrace [String, nil]
|
|
308
|
+
# @return [Array<Event>]
|
|
309
|
+
def build_error_events(error_count, error_msg, stacktrace)
|
|
310
|
+
return [] unless error_count.positive?
|
|
311
|
+
|
|
312
|
+
timestamp = calculate_end_timestamp
|
|
313
|
+
|
|
314
|
+
if stacktrace
|
|
315
|
+
attrs = { 'exception.type' => span_name }
|
|
316
|
+
attrs['exception.message'] = error_msg if error_msg
|
|
317
|
+
attrs['exception.stacktrace'] = stacktrace
|
|
318
|
+
[Event.new('exception', attrs, timestamp)]
|
|
319
|
+
else
|
|
320
|
+
[Event.new('error', { 'error.type' => span_name }, timestamp)]
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Add an attribute to the attributes hash if value is not nil
|
|
325
|
+
#
|
|
326
|
+
# @param attributes [Hash] The attributes hash to add to
|
|
327
|
+
# @param key [String, Symbol] The attribute key
|
|
328
|
+
# @param value [Object] The attribute value
|
|
329
|
+
# @return [void]
|
|
330
|
+
def add_attribute(attributes, key, value)
|
|
331
|
+
return if value.nil?
|
|
332
|
+
|
|
333
|
+
attributes[key] = normalize_attribute_value(value)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Normalize attribute value to OTLP-compatible types
|
|
337
|
+
#
|
|
338
|
+
# OTLP supports: String, Integer, Float, Boolean, and Arrays of these types
|
|
339
|
+
#
|
|
340
|
+
# @param value [Object] The value to normalize
|
|
341
|
+
# @return [String, Integer, Float, Boolean, Array] Normalized value
|
|
342
|
+
def normalize_attribute_value(value)
|
|
343
|
+
case value
|
|
344
|
+
when String, Integer, Float, TrueClass, FalseClass
|
|
345
|
+
value
|
|
346
|
+
when Symbol
|
|
347
|
+
value.to_s
|
|
348
|
+
when Array
|
|
349
|
+
value.map { |item| normalize_attribute_value(item) }
|
|
350
|
+
else
|
|
351
|
+
value.to_s
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
private
|
|
356
|
+
|
|
357
|
+
# Get the span name as a string
|
|
358
|
+
#
|
|
359
|
+
# For custom (SDK) spans the user-supplied name is stored in
|
|
360
|
+
# span[:data][:sdk][:name], not in span[:n] (which is always :sdk).
|
|
361
|
+
# We read that path directly to avoid crashing when sdk data has been
|
|
362
|
+
# overwritten by tests or other code. Non-custom spans use span[:n].
|
|
363
|
+
#
|
|
364
|
+
# @return [String] The span name
|
|
365
|
+
def span_name
|
|
366
|
+
if span.respond_to?(:custom?) ? span.custom? : span[:n]&.to_sym == :sdk
|
|
367
|
+
span[:data]&.dig(:sdk, :name).to_s
|
|
368
|
+
else
|
|
369
|
+
span[:n].to_s
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Format parent span ID, returning INVALID_SPAN_ID if no parent
|
|
374
|
+
#
|
|
375
|
+
# @return [String] Formatted parent span ID or INVALID_SPAN_ID
|
|
376
|
+
def format_parent_span_id
|
|
377
|
+
format_span_id(span[:p]) || OpenTelemetry::Trace::INVALID_SPAN_ID
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Calculate end timestamp from start time and duration
|
|
381
|
+
#
|
|
382
|
+
# @return [Integer] End timestamp in nanoseconds
|
|
383
|
+
def calculate_end_timestamp
|
|
384
|
+
start_time = span[:ts] || 0
|
|
385
|
+
duration = span[:d] || 0
|
|
386
|
+
convert_to_unix_nano(start_time + duration)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Infer span kind from span name using Instana's span kind registry
|
|
390
|
+
#
|
|
391
|
+
# @return [Symbol] Inferred span kind
|
|
392
|
+
def infer_span_kind_from_name
|
|
393
|
+
name = span[:n]&.to_sym
|
|
394
|
+
return :server if ::Instana::SpanKind::ENTRY_SPANS.include?(name)
|
|
395
|
+
return :client if ::Instana::SpanKind::EXIT_SPANS.include?(name)
|
|
396
|
+
|
|
397
|
+
:internal
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Get or create resource adapter for OTLP export
|
|
401
|
+
#
|
|
402
|
+
# @return [Object] Resource adapter with attribute_enumerator method
|
|
403
|
+
def resource_adapter
|
|
404
|
+
return resource if resource.respond_to?(:attribute_enumerator)
|
|
405
|
+
|
|
406
|
+
ResourceAdapter.new(resource)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Get or create instrumentation scope
|
|
410
|
+
#
|
|
411
|
+
# @return [InstrumentationScope] Scope identifying the Instana Ruby sensor
|
|
412
|
+
def instrumentation_scope
|
|
413
|
+
@instrumentation_scope ||= InstrumentationScope.new('instana-ruby', ::Instana::VERSION)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# (c) Copyright IBM Corp. 2026
|
|
4
|
+
|
|
5
|
+
require_relative 'base_converter'
|
|
6
|
+
require_relative 'http_converter'
|
|
7
|
+
require_relative 'database_converter'
|
|
8
|
+
require_relative 'messaging_converter'
|
|
9
|
+
require_relative 'background_job_converter'
|
|
10
|
+
require_relative 'aws_converter'
|
|
11
|
+
require_relative 'rpc_converter'
|
|
12
|
+
require_relative 'rails_converter'
|
|
13
|
+
require_relative 'graphql_converter'
|
|
14
|
+
require_relative 'custom_converter'
|
|
15
|
+
require_relative '../../trace/span_kind'
|
|
16
|
+
|
|
17
|
+
module Instana
|
|
18
|
+
module Exporter
|
|
19
|
+
module Otlp
|
|
20
|
+
# Factory class for creating appropriate OTLP span converters
|
|
21
|
+
# based on span type
|
|
22
|
+
class ConverterFactory
|
|
23
|
+
# Span type constants
|
|
24
|
+
SPAN_TYPES = {
|
|
25
|
+
http: 'http',
|
|
26
|
+
database: 'database',
|
|
27
|
+
messaging: 'messaging',
|
|
28
|
+
background_job: 'background_job',
|
|
29
|
+
aws: 'aws',
|
|
30
|
+
rpc: 'rpc',
|
|
31
|
+
rails: 'rails',
|
|
32
|
+
graphql: 'graphql',
|
|
33
|
+
custom: 'custom'
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# Create a converter for the given span
|
|
38
|
+
# @param span [Instana::Trace::Span] The span to convert
|
|
39
|
+
# @return [BaseConverter] An instance of the appropriate converter
|
|
40
|
+
def create(span)
|
|
41
|
+
span_type = determine_span_type(span)
|
|
42
|
+
converter_class = get_converter_class(span_type)
|
|
43
|
+
|
|
44
|
+
converter_class.new(span)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Determine the type of span based on its attributes
|
|
50
|
+
# @param span [Instana::Trace::Span] The span to analyze
|
|
51
|
+
# @return [String] The span type
|
|
52
|
+
def determine_span_type(span)
|
|
53
|
+
return SPAN_TYPES[:http] if http_span?(span)
|
|
54
|
+
return SPAN_TYPES[:database] if database_span?(span)
|
|
55
|
+
return SPAN_TYPES[:aws] if aws_span?(span)
|
|
56
|
+
return SPAN_TYPES[:background_job] if background_job_span?(span)
|
|
57
|
+
return SPAN_TYPES[:messaging] if messaging_span?(span)
|
|
58
|
+
return SPAN_TYPES[:rails] if rails_span?(span)
|
|
59
|
+
return SPAN_TYPES[:graphql] if graphql_span?(span)
|
|
60
|
+
return SPAN_TYPES[:rpc] if rpc_span?(span)
|
|
61
|
+
return SPAN_TYPES[:custom] if custom_span?(span)
|
|
62
|
+
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get the appropriate converter class for the span type
|
|
67
|
+
# @param span_type [String] The type of span
|
|
68
|
+
# @return [Class] The converter class
|
|
69
|
+
def get_converter_class(span_type)
|
|
70
|
+
return BaseConverter unless span_type
|
|
71
|
+
|
|
72
|
+
# Convert snake_case to CamelCase (e.g., 'background_job' -> 'BackgroundJob')
|
|
73
|
+
class_name = "#{span_type.split('_').map(&:capitalize).join}Converter"
|
|
74
|
+
|
|
75
|
+
begin
|
|
76
|
+
const_get("Instana::Exporter::Otlp::#{class_name}")
|
|
77
|
+
rescue NameError
|
|
78
|
+
BaseConverter
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check if span is an HTTP span
|
|
83
|
+
# Uses the HTTP_SPANS constant to identify HTTP spans
|
|
84
|
+
def http_span?(span)
|
|
85
|
+
Instana::SpanKind::HTTP_SPANS.include?(span[:n]&.to_sym)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if span is a database span
|
|
89
|
+
# Instana native spans always have a name, so we only check the name
|
|
90
|
+
def database_span?(span)
|
|
91
|
+
span[:n]&.match?(/sql|database|query|activerecord|sequel|mongo|redis|dalli/i)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Check if span is an AWS span
|
|
95
|
+
# Note: SQS and SNS are handled by messaging_span? since they're messaging services
|
|
96
|
+
def aws_span?(span)
|
|
97
|
+
span[:n]&.match?(/dynamodb|s3|aws\.lambda/i)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if span is a messaging span
|
|
101
|
+
def messaging_span?(span)
|
|
102
|
+
span[:n]&.match?(/sqs|sns|kafka|rabbitmq|message|bunny|shoryuken/i)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check if span is a background job span
|
|
106
|
+
def background_job_span?(span)
|
|
107
|
+
span[:n]&.match?(/sidekiq-(client|worker)|resque-(client|worker)/i)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Check if span is a Rails span
|
|
111
|
+
def rails_span?(span)
|
|
112
|
+
span[:n]&.match?(/actioncontroller|actionview|actionmailer|render|mail\.actionmailer/i)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Check if span is a GraphQL span
|
|
116
|
+
def graphql_span?(span)
|
|
117
|
+
span[:n]&.match?(/graphql/i)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Check if span is an RPC span
|
|
121
|
+
# Instana native spans always have a name, so we only check the name
|
|
122
|
+
def rpc_span?(span)
|
|
123
|
+
span[:n]&.match?(/grpc|rpc/i)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Check if span is an Instana SDK custom span
|
|
127
|
+
def custom_span?(span)
|
|
128
|
+
span[:n]&.match?(/custom|sdk/i) ||
|
|
129
|
+
span[:data]&.dig(:sdk, :type)&.to_s == 'custom'
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# (c) Copyright IBM Corp. 2026
|
|
4
|
+
|
|
5
|
+
module Instana
|
|
6
|
+
module Exporter
|
|
7
|
+
module Otlp
|
|
8
|
+
# Converter for Instana SDK custom spans to OTLP format
|
|
9
|
+
class CustomConverter < BaseConverter
|
|
10
|
+
# Build OTel-compliant span name for custom (SDK) spans
|
|
11
|
+
#
|
|
12
|
+
# Formula per SPAN_NAME_PATTERNS.txt Section 7:
|
|
13
|
+
# Use the user-supplied sdk[:name] when available, otherwise fall back
|
|
14
|
+
# to the internal span type key (span[:n]).
|
|
15
|
+
#
|
|
16
|
+
# @return [String] The span name
|
|
17
|
+
def span_name
|
|
18
|
+
sdk_name = span[:data]&.[](:sdk)&.[](:name).to_s.strip
|
|
19
|
+
sdk_name.empty? ? super : sdk_name
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def convert_attributes
|
|
23
|
+
attributes = {}
|
|
24
|
+
sdk_data = span[:data]&.[](:sdk) || {}
|
|
25
|
+
|
|
26
|
+
# Add standard Instana attributes
|
|
27
|
+
add_attribute(attributes, 'instana.span.type', 'custom')
|
|
28
|
+
add_attribute(attributes, 'instana.sdk.name', sdk_data[:name] || span[:n])
|
|
29
|
+
add_attribute(attributes, 'instana.sdk.type', sdk_data[:type])
|
|
30
|
+
|
|
31
|
+
# Add tags directly
|
|
32
|
+
tags = sdk_data.dig(:custom, :tags) || {}
|
|
33
|
+
tags.each do |key, value|
|
|
34
|
+
attributes[key] = value
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attributes
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|