instana 2.9.0 → 2.10.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: 52139723659935eb43ab59f60e5c0f7305d860d5457c9545d395f9e4576fc604
4
- data.tar.gz: 223dcdbe22af35ceed817411b850114f9cf3e7eb5fce3b3fc1b4f5ffa29c00e1
3
+ metadata.gz: 85a74f9236a4a35af2f74850bb0c5230a921f37b23bdf80af6b578375fe620d3
4
+ data.tar.gz: eaaa01ae5c3a67c7d374fff6ccdf58be1bff45e8ba36186e2617b3084abb2e90
5
5
  SHA512:
6
- metadata.gz: 0743a4a453182c90abb623189d4a6e5105cd03e3823b3b6cf9ee5269c80f14652d21bfc5481c914457a79a8a52633c718cfd1581d219122327996cb67c2b222f
7
- data.tar.gz: 767f9eb2f714e83e045f27086061ef11ab3511574b8636c87740595f871a0051ccc977341e0e523f174d4f4d5a4575fbcd3d8b87ed1ab26b347198fbdb3d4d94
6
+ metadata.gz: 1338789ea5e37aa238bc08815cdbabf801ead7b17f28a9fc49e1efefecfa103ed2d0eb2958be6a50efa821169c2b4551e2b521b6f518585fa75e30a84cc92b0e
7
+ data.tar.gz: f1290dc8888e9e570bc2ee6a1dab5f11ac2f241726a0bf809aaa4c8fa5066d2bc142a268be3e85183052e9f26eff5a9a3377cf48b06a91acf9861f1c490f4da5
data/README.md CHANGED
@@ -51,6 +51,179 @@ Although the gem has no configuration required for out of the box metrics and tr
51
51
 
52
52
  This Ruby gem provides a simple API for tracing and also supports [OpenTracing](http://opentracing.io/). See the [Ruby Tracing SDK](https://www.ibm.com/docs/en/instana-observability/current?topic=ruby-tracing-sdk) and [OpenTracing](https://www.ibm.com/docs/en/instana-observability/current?topic=ruby-opentracing) pages for details.
53
53
 
54
+ ## OTLP Export
55
+
56
+ The `instana` gem supports exporting traces via the **OpenTelemetry Protocol (OTLP)**
57
+ directly to the Instana agent's OTLP receiver, bypassing the native agent trace format.
58
+ OTLP export is **opt-in** — the gem behaves identically to previous versions when
59
+ `INSTANA_TRACING_OTLP_ENABLED` is not set.
60
+
61
+ ### Overview
62
+
63
+ When OTLP export is enabled:
64
+ - Spans are converted from Instana's internal format to OTLP protobuf
65
+ - The converted spans are sent via HTTP/protobuf to the agent's OTLP port (`4318`)
66
+ - The endpoint is auto-derived from the same host where the Instana agent was discovered
67
+ - The native agent reporting path (`/com.instana.plugin.ruby/traces`) is replaced by OTLP
68
+
69
+ ### Quick Start
70
+
71
+ The only required change is setting one environment variable before starting your application:
72
+
73
+ ```bash
74
+ INSTANA_TRACING_OTLP_ENABLED=true bundle exec rails server
75
+ ```
76
+
77
+ Expected log output (with `INSTANA_LOG_LEVEL=debug`):
78
+ ```
79
+ [Instana] Using OTLP Exporter to export result code: 0
80
+ ```
81
+
82
+ Result code `0` = `SUCCESS`. No other configuration is needed — the endpoint is
83
+ automatically set to `http://<agent-host>:4318/v1/traces`.
84
+
85
+ ### Gem Dependencies
86
+
87
+ `opentelemetry-exporter-otlp` is a **runtime dependency** of the `instana` gem and
88
+ is installed automatically. No additional gems are required.
89
+
90
+ > **Note:** gRPC (`OTEL_EXPORTER_OTLP_PROTOCOL=grpc`) is not supported.
91
+ > Only `http/protobuf` is supported and tested.
92
+
93
+ ### Environment Variables
94
+
95
+ | Variable | Type | Default | Description |
96
+ |---|---|---|---|
97
+ | `INSTANA_TRACING_OTLP_ENABLED` | Boolean | `false` | Enables OTLP export. Valid values: `true`, `1`, `yes` |
98
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | String (URL) | auto-derived from agent host | **Base** endpoint URL. `/v1/traces` is appended automatically. |
99
+ | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | String (URL) | `<OTEL_EXPORTER_OTLP_ENDPOINT>/v1/traces` | **Fully-qualified** trace endpoint. Used as-is; overrides `OTEL_EXPORTER_OTLP_ENDPOINT`. |
100
+ | `OTEL_EXPORTER_OTLP_PROTOCOL` | String | `http/protobuf` | Transport protocol. Only `http/protobuf` is supported. |
101
+ | `OTEL_EXPORTER_OTLP_HEADERS` | String | — | Comma-separated `key=value` auth headers. e.g. `api-key=secret,x-tenant-id=myorg` |
102
+ | `OTEL_EXPORTER_OTLP_TIMEOUT` | Integer (ms) | `10000` | Maximum time to wait for export completion. |
103
+ | `OTEL_EXPORTER_OTLP_COMPRESSION` | String | — | Compression algorithm. Valid value: `gzip`. |
104
+ | `OTEL_EXPORTER_OTLP_CERTIFICATE` | String (path) | — | Path to CA certificate file for TLS verification. |
105
+ | `OTEL_EXPORTER_OTLP_CLIENT_KEY` | String (path) | — | Path to client private key file for mutual TLS. |
106
+ | `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` | String (path) | — | Path to client certificate file for mutual TLS. |
107
+ | `OTEL_EXPORTER_OTLP_INSECURE` | Boolean | `false` | Disable TLS certificate verification. |
108
+ | `OTEL_SEMCONV_STABILITY_OPT_IN` | String | `stable` | Semantic convention stability level. Valid values: `stable`, `development`. |
109
+
110
+ > **Endpoint precedence:** `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` > `OTEL_EXPORTER_OTLP_ENDPOINT` > auto-derived from agent host
111
+
112
+ ### YAML Configuration
113
+
114
+ When `INSTANA_CONFIG_PATH` points to a YAML file, OTLP settings are placed under
115
+ the `tracing.otlp` key. YAML configuration takes precedence over environment variables.
116
+
117
+ **Minimal example** (`config/instana.yml`):
118
+ ```yaml
119
+ tracing:
120
+ otlp:
121
+ enabled: true
122
+ ```
123
+
124
+ **Full example:**
125
+ ```yaml
126
+ tracing:
127
+ otlp:
128
+ enabled: true
129
+ endpoint: "http://localhost:4318/v1/traces"
130
+ protocol: "http/protobuf"
131
+ timeout: 10000
132
+ compression: "gzip"
133
+ headers:
134
+ x-instana-key: "YOUR_API_KEY"
135
+ x-tenant-id: "YOUR_TENANT"
136
+ certificate: "/path/to/ca-cert.pem"
137
+ client_key: "/path/to/client-key.pem"
138
+ client_certificate: "/path/to/client-cert.pem"
139
+ insecure: false
140
+ semconv_stability: "stable"
141
+ ```
142
+
143
+ Start the application:
144
+ ```bash
145
+ INSTANA_CONFIG_PATH=config/instana.yml bundle exec rails server
146
+ ```
147
+
148
+ ### Configuration Precedence
149
+
150
+ Settings are resolved in this order (highest wins):
151
+
152
+ ```
153
+ INSTANA_CONFIG_PATH YAML → Environment variables → Agent config → Defaults
154
+ ```
155
+
156
+ This means a YAML file always wins over env vars. Agent-provided config (pushed
157
+ via the Instana agent's discovery payload) is applied after env vars but can be
158
+ overridden by YAML or env vars set at startup.
159
+
160
+ ### TLS / mTLS
161
+
162
+ **Server-side TLS** (verify the collector's certificate):
163
+ ```bash
164
+ INSTANA_TRACING_OTLP_ENABLED=true \
165
+ OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4318/v1/traces \
166
+ OTEL_EXPORTER_OTLP_CERTIFICATE=/path/to/ca-cert.pem \
167
+ bundle exec rails server
168
+ ```
169
+
170
+ **Mutual TLS** (client certificate authentication):
171
+ ```bash
172
+ INSTANA_TRACING_OTLP_ENABLED=true \
173
+ OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4318/v1/traces \
174
+ OTEL_EXPORTER_OTLP_CERTIFICATE=/path/to/ca-cert.pem \
175
+ OTEL_EXPORTER_OTLP_CLIENT_KEY=/path/to/client-key.pem \
176
+ OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE=/path/to/client-cert.pem \
177
+ bundle exec rails server
178
+ ```
179
+
180
+ ### Troubleshooting
181
+
182
+ **OTLP export is not happening — spans go via the native path**
183
+
184
+ OTLP is disabled by default. Check that `INSTANA_TRACING_OTLP_ENABLED=true` is set
185
+ in the process environment before the gem initialises (i.e. before `require 'instana'`
186
+ or Rails boot). Verify with:
187
+ ```bash
188
+ INSTANA_LOG_LEVEL=debug bundle exec rails server
189
+ # Look for: "Using OTLP Exporter" vs "Using Instana Native Exporter"
190
+ ```
191
+
192
+ **`Failed to initialize OTLP exporter` in the log**
193
+
194
+ The exporter could not be constructed. Common causes:
195
+ - `OTEL_EXPORTER_OTLP_CERTIFICATE` path does not exist or is not readable
196
+ - `OTEL_EXPORTER_OTLP_CLIENT_KEY` / `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` path invalid
197
+ - An unsupported protocol (e.g. `grpc`) was set — only `http/protobuf` is supported
198
+
199
+ The gem falls back to `@otlp_exporter = nil` and logs an `ERROR` line with the
200
+ exception message. Check that message for the root cause.
201
+
202
+ **`result code: 1` (FAILURE) — spans not arriving**
203
+
204
+ The exporter initialised but the export request failed. Common causes:
205
+ - The Instana agent is not running or OTLP is not enabled on the agent side
206
+ - `OTEL_EXPORTER_OTLP_ENDPOINT` points to a wrong host or port
207
+ - `OTEL_EXPORTER_OTLP_ENDPOINT` was set to a base URL like `http://host:4318`
208
+ without `/v1/traces` — this was a known bug fixed in v2.9.0 (the sensor now
209
+ appends the path automatically)
210
+ - A firewall is blocking port `4318`
211
+
212
+ Verify the agent OTLP port is reachable:
213
+ ```bash
214
+ curl -s -o /dev/null -w "%{http_code}" \
215
+ -X POST http://localhost:4318/v1/traces \
216
+ -H "Content-Type: application/x-protobuf"
217
+ # 200 or 415 = port open; "connection refused" = agent OTLP not enabled
218
+ ```
219
+
220
+ **`HTTP request path is empty` exception (pre-v2.9.0)**
221
+
222
+ If you are on a version earlier than 2.9.0 and pass a base URL to
223
+ `OTEL_EXPORTER_OTLP_ENDPOINT` (without `/v1/traces`), the `opentelemetry-exporter-otlp`
224
+ gem crashes with `HTTP request path is empty`. Upgrade to v2.9.0 or later, or use
225
+ `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` with the full path instead.
226
+
54
227
  ## Documentation
55
228
 
56
229
  You can find more documentation covering supported components and minimum versions in the Instana [documentation portal](https://www.ibm.com/docs/en/instana-observability/current?topic=technologies-monitoring-ruby).
@@ -186,11 +186,12 @@ module Instana
186
186
 
187
187
  endpoint = resolve_otlp_endpoint(config[:endpoint], config[:config_source])
188
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]
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
+ opts[:ssl_verify_mode] = OpenSSL::SSL::VERIFY_NONE if config[:insecure]
194
195
 
195
196
  @otlp_exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(**opts)
196
197
  rescue StandardError => e
@@ -198,21 +199,32 @@ module Instana
198
199
  @otlp_exporter = nil
199
200
  end
200
201
 
201
- # Derive the OTLP endpoint from the discovered agent host when no explicit
202
- # endpoint has been configured (config_source == 'default').
203
202
  OTLP_DEFAULT_PORT = 4318
204
203
  OTLP_TRACES_PATH = '/v1/traces'.freeze
205
204
 
205
+ # Returns a fully-qualified OTLP traces endpoint.
206
+ #
207
+ # When config_source is 'default', builds the endpoint from the discovered
208
+ # agent host so OTLP traffic goes to the same host as metrics/traces.
209
+ #
210
+ # For user-supplied endpoints, appends '/v1/traces' when the URL has no
211
+ # path, because OTEL_EXPORTER_OTLP_ENDPOINT is a base URL and the
212
+ # opentelemetry-exporter-otlp gem only auto-appends the path when no
213
+ # endpoint is given at all.
206
214
  def resolve_otlp_endpoint(endpoint, config_source)
207
- return endpoint unless config_source == 'default'
215
+ if config_source == 'default'
216
+ agent_host = @client&.host
217
+ return endpoint unless agent_host
208
218
 
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
219
+ return "http://#{agent_host}:#{OTLP_DEFAULT_PORT}#{OTLP_TRACES_PATH}"
220
+ end
214
221
 
215
- "http://#{agent_host}:#{OTLP_DEFAULT_PORT}#{OTLP_TRACES_PATH}"
222
+ # User-supplied: append the traces path if no path was given.
223
+ uri = URI.parse(endpoint)
224
+ uri.path = OTLP_TRACES_PATH if uri.path.empty? || uri.path == '/'
225
+ uri.to_s
226
+ rescue URI::InvalidURIError
227
+ endpoint
216
228
  end
217
229
  end
218
230
  end
@@ -4,7 +4,7 @@
4
4
  require 'yaml'
5
5
 
6
6
  module Instana
7
- class Config
7
+ class Config # rubocop:disable Metrics/ClassLength
8
8
 
9
9
  LEGACY_TRACING_KEY = 'com.instana.tracing'.freeze
10
10
  TRACING_KEY = 'tracing'.freeze
@@ -63,6 +63,9 @@ module Instana
63
63
  certificate: nil,
64
64
  client_key: nil,
65
65
  client_certificate: nil,
66
+ protocol: 'http/protobuf',
67
+ semconv_stability: 'stable',
68
+ insecure: false,
66
69
  config_source: 'default'
67
70
  }
68
71
  read_otlp_config
@@ -257,6 +260,9 @@ module Instana
257
260
  @config[:otlp][:certificate] = otlp_config['certificate'] if otlp_config['certificate']
258
261
  @config[:otlp][:client_key] = otlp_config['client_key'] if otlp_config['client_key']
259
262
  @config[:otlp][:client_certificate] = otlp_config['client_certificate'] if otlp_config['client_certificate']
263
+ @config[:otlp][:protocol] = otlp_config['protocol'] if otlp_config['protocol']
264
+ @config[:otlp][:semconv_stability] = otlp_config['semconv_stability'] if otlp_config['semconv_stability']
265
+ @config[:otlp][:insecure] = truthy?(otlp_config['insecure']) unless otlp_config['insecure'].nil?
260
266
  @config[:otlp][:config_source] = 'agent'
261
267
  end
262
268
 
@@ -381,6 +387,9 @@ module Instana
381
387
  result[:certificate] = otlp_yaml['certificate'] if otlp_yaml['certificate']
382
388
  result[:client_key] = otlp_yaml['client_key'] if otlp_yaml['client_key']
383
389
  result[:client_certificate] = otlp_yaml['client_certificate'] if otlp_yaml['client_certificate']
390
+ result[:protocol] = otlp_yaml['protocol'] if otlp_yaml['protocol']
391
+ result[:semconv_stability] = otlp_yaml['semconv_stability'] if otlp_yaml['semconv_stability']
392
+ result[:insecure] = truthy?(otlp_yaml['insecure']) unless otlp_yaml['insecure'].nil?
384
393
  result.empty? ? nil : result
385
394
  end
386
395
 
@@ -399,6 +408,9 @@ module Instana
399
408
  result[:certificate] = raw[:certificate] if raw[:certificate]
400
409
  result[:client_key] = raw[:client_key] if raw[:client_key]
401
410
  result[:client_certificate] = raw[:client_cert] if raw[:client_cert]
411
+ result[:protocol] = raw[:protocol] if raw[:protocol]
412
+ result[:semconv_stability] = raw[:semconv_stability] if raw[:semconv_stability]
413
+ result[:insecure] = truthy?(raw[:insecure_raw]) unless raw[:insecure_raw].nil?
402
414
  result
403
415
  end
404
416
 
@@ -413,7 +425,10 @@ module Instana
413
425
  headers_raw: ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_HEADERS', nil) || ENV.fetch('OTEL_EXPORTER_OTLP_HEADERS', nil),
414
426
  certificate: ENV.fetch('OTEL_EXPORTER_OTLP_CERTIFICATE', nil),
415
427
  client_key: ENV.fetch('OTEL_EXPORTER_OTLP_CLIENT_KEY', nil),
416
- client_cert: ENV.fetch('OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE', nil)
428
+ client_cert: ENV.fetch('OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE', nil),
429
+ protocol: ENV.fetch('OTEL_EXPORTER_OTLP_PROTOCOL', nil),
430
+ semconv_stability: ENV.fetch('OTEL_SEMCONV_STABILITY_OPT_IN', nil),
431
+ insecure_raw: ENV.fetch('OTEL_EXPORTER_OTLP_INSECURE', nil)
417
432
  }
418
433
  end
419
434
 
@@ -6,6 +6,9 @@ require_relative 'base_converter'
6
6
  require 'opentelemetry/semconv/incubating/messaging'
7
7
  require 'opentelemetry/semconv/db'
8
8
  require 'opentelemetry/semconv/incubating/db'
9
+ require 'opentelemetry/semconv/incubating/rpc'
10
+ require 'opentelemetry/semconv/incubating/cloud'
11
+ require 'opentelemetry/semconv/incubating/faas'
9
12
 
10
13
  module Instana
11
14
  module Exporter
@@ -128,10 +131,18 @@ module Instana
128
131
  def convert_s3_attributes(attributes, s3_data)
129
132
  return unless s3_data
130
133
 
131
- add_attribute(attributes, 'aws.service', 's3')
134
+ add_attribute(attributes, Instana::Exporter::Otlp::RPC_SYSTEM_ATTR, 'aws-api')
135
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_METHOD, s3_rpc_method(s3_data[:op]))
136
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::CLOUD::CLOUD_PROVIDER, 'aws')
137
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::CLOUD::CLOUD_REGION, s3_data[:region])
132
138
  add_attribute(attributes, 'aws.s3.bucket', s3_data[:bucket])
133
139
  add_attribute(attributes, 'aws.s3.key', s3_data[:key])
134
- add_attribute(attributes, 'aws.s3.operation', s3_data[:op])
140
+ end
141
+
142
+ def s3_rpc_method(operation)
143
+ return unless operation
144
+
145
+ "S3.#{operation}"
135
146
  end
136
147
 
137
148
  def convert_lambda_attributes(attributes, lambda_data)
@@ -140,7 +151,7 @@ module Instana
140
151
  add_attribute(attributes, 'aws.service', 'lambda')
141
152
  add_attribute(attributes, 'aws.lambda.function_name', lambda_data[:function])
142
153
  add_attribute(attributes, 'aws.lambda.invocation_type', lambda_data[:type])
143
- add_attribute(attributes, 'faas.invoked_name', lambda_data[:function])
154
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::FAAS::FAAS_INVOKED_NAME, lambda_data[:function])
144
155
  end
145
156
  end
146
157
  end
@@ -41,6 +41,8 @@ module Instana
41
41
  convert_job_attributes(attributes, span[:'resque-client'] || span[:data]&.[](:'resque-client'), 'resque', 'publish')
42
42
  elsif span_type == 'resque-worker'
43
43
  convert_job_attributes(attributes, span[:'resque-worker'] || span[:data]&.[](:'resque-worker'), 'resque', 'process')
44
+ elsif span_type == 'activejob'
45
+ convert_activejob_attributes(attributes, span[:data]&.[](:activejob))
44
46
  end
45
47
 
46
48
  attributes
@@ -53,13 +55,22 @@ module Instana
53
55
 
54
56
  add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, system)
55
57
  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)
58
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_NAME, operation)
57
59
  add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_MESSAGE_ID, data[:job_id] || data['job_id'])
58
60
  add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_CONSUMER_GROUP_NAME, data[:job] || data['job'])
59
61
  add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, extract_host(data[:'redis-url'] || data['redis-url']))
60
62
  add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, extract_port(data[:'redis-url'] || data['redis-url']))
61
63
  end
62
64
 
65
+ def convert_activejob_attributes(attributes, data)
66
+ return unless data
67
+
68
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, 'activejob')
69
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_DESTINATION_NAME, data[:queue] || data['queue'])
70
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_NAME, data[:action] || data['action'])
71
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_MESSAGE_ID, data[:job_id] || data['job_id'])
72
+ end
73
+
63
74
  def extract_host(connection)
64
75
  return nil unless connection
65
76
 
@@ -5,10 +5,22 @@
5
5
  require_relative 'resource'
6
6
  require 'opentelemetry/trace'
7
7
  require 'forwardable'
8
+ require 'opentelemetry/semconv/incubating/rpc'
8
9
 
9
10
  module Instana
10
11
  module Exporter
11
12
  module Otlp
13
+ # Compatibility shim: RPC_SYSTEM_NAME was added in a newer release of
14
+ # opentelemetry-semantic_conventions. Fall back to the deprecated
15
+ # RPC_SYSTEM constant ('rpc.system') on older installs (e.g. Ruby 3.2
16
+ # lockfiles that resolve an earlier gem version).
17
+ RPC_SYSTEM_ATTR =
18
+ if OpenTelemetry::SemConv::Incubating::RPC.const_defined?(:RPC_SYSTEM_NAME)
19
+ OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM_NAME
20
+ else
21
+ OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM
22
+ end
23
+
12
24
  # Base class for all OTLP span converters
13
25
  #
14
26
  # Provides common interface and shared functionality for converting Instana spans
@@ -312,12 +324,12 @@ module Instana
312
324
  timestamp = calculate_end_timestamp
313
325
 
314
326
  if stacktrace
315
- attrs = { 'exception.type' => span_name }
327
+ attrs = {}
316
328
  attrs['exception.message'] = error_msg if error_msg
317
329
  attrs['exception.stacktrace'] = stacktrace
318
330
  [Event.new('exception', attrs, timestamp)]
319
331
  else
320
- [Event.new('error', { 'error.type' => span_name }, timestamp)]
332
+ [Event.new('error', {}, timestamp)]
321
333
  end
322
334
  end
323
335
 
@@ -82,32 +82,51 @@ module Instana
82
82
  parts.empty? ? 'mongodb' : parts.join('.')
83
83
  end
84
84
 
85
+ # Normalize an ActiveRecord/Sequel adapter name to OTel db.system.name.
86
+ # Only adapters that are actively instrumented and tested are listed here.
87
+ DB_SYSTEM_NORMALIZATION = {
88
+ 'mysql2' => 'mysql',
89
+ 'sqlite3' => 'sqlite',
90
+ 'postgresql' => 'postgresql',
91
+ 'mysql' => 'mysql',
92
+ 'sqlite' => 'sqlite'
93
+ }.freeze
94
+
95
+ def normalize_db_system(adapter)
96
+ return unless adapter
97
+
98
+ DB_SYSTEM_NORMALIZATION.fetch(adapter.to_s.downcase, adapter.to_s.downcase)
99
+ end
100
+
85
101
  def convert_activerecord_attributes(attributes, ar_data)
86
102
  return unless ar_data
87
103
 
88
- add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, ar_data[:adapter])
104
+ add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, normalize_db_system(ar_data[:adapter]))
89
105
  add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, ar_data[:db])
90
106
  add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_QUERY_TEXT, ar_data[:sql])
91
107
  add_attribute(attributes, OpenTelemetry::SemConv::Incubating::DB::DB_USER, ar_data[:username])
92
108
  add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, ar_data[:host])
109
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::DB::DB_COLLECTION_NAME, ar_data[:table])
93
110
  end
94
111
 
95
112
  def convert_sequel_attributes(attributes, seq_data)
96
113
  return unless seq_data
97
114
 
98
- add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, seq_data[:adapter])
115
+ add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, normalize_db_system(seq_data[:adapter]))
99
116
  add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, seq_data[:db])
100
117
  add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_QUERY_TEXT, seq_data[:sql])
101
118
  add_attribute(attributes, OpenTelemetry::SemConv::Incubating::DB::DB_USER, seq_data[:username])
102
119
  add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, seq_data[:host])
120
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::DB::DB_COLLECTION_NAME, seq_data[:table])
103
121
  end
104
122
 
105
123
  def convert_redis_attributes(attributes, redis_data)
106
124
  return unless redis_data
107
125
 
108
126
  add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, 'redis')
127
+ add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_OPERATION_NAME, redis_data[:command])
109
128
  add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_QUERY_TEXT, redis_data[:command])
110
- add_attribute(attributes, 'db.redis.database_index', redis_data[:db])
129
+ add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, redis_data[:db]&.to_s)
111
130
  add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, extract_host(redis_data[:connection]))
112
131
  add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, extract_port(redis_data[:connection]))
113
132
  end
@@ -119,7 +138,7 @@ module Instana
119
138
  add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_OPERATION_NAME, mc_data[:command])
120
139
  add_attribute(attributes, 'db.memcached.key', mc_data[:key])
121
140
  add_attribute(attributes, 'db.memcached.keys', mc_data[:keys])
122
- add_attribute(attributes, 'db.memcached.namespace', mc_data[:namespace])
141
+ add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, mc_data[:namespace])
123
142
  add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, extract_host(mc_data[:server]))
124
143
  add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, extract_port(mc_data[:server]))
125
144
  end
@@ -131,6 +150,7 @@ module Instana
131
150
  add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, mongo_data[:namespace])
132
151
  add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_OPERATION_NAME, mongo_data[:command])
133
152
  add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_QUERY_TEXT, mongo_data[:json])
153
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::DB::DB_COLLECTION_NAME, mongo_data[:collection])
134
154
  add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, mongo_data.dig(:peer, :hostname))
135
155
  add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, mongo_data.dig(:peer, :port))
136
156
  end
@@ -7,6 +7,7 @@ require 'opentelemetry/semconv/http'
7
7
  require 'opentelemetry/semconv/url'
8
8
  require 'opentelemetry/semconv/server'
9
9
  require 'opentelemetry/semconv/user_agent'
10
+ require 'opentelemetry/semconv/network'
10
11
 
11
12
  module Instana
12
13
  module Exporter
@@ -120,8 +121,8 @@ module Instana
120
121
  name = parts[0].downcase
121
122
  version = parts[1]
122
123
 
123
- add_attribute(attributes, 'network.protocol.name', name)
124
- add_attribute(attributes, 'network.protocol.version', version)
124
+ add_attribute(attributes, OpenTelemetry::SemConv::NETWORK::NETWORK_PROTOCOL_NAME, name)
125
+ add_attribute(attributes, OpenTelemetry::SemConv::NETWORK::NETWORK_PROTOCOL_VERSION, version)
125
126
  end
126
127
  end
127
128
  end
@@ -11,15 +11,36 @@ module Instana
11
11
  module Otlp
12
12
  # Converter for messaging spans to OTLP format
13
13
  class MessagingConverter < BaseConverter
14
- # Build OTel-compliant span name for messaging (RabbitMQ) spans
14
+ # Build OTel-compliant span name for messaging spans
15
15
  #
16
- # Formula per SPAN_NAME_PATTERNS.txt Section 3 (bunny/AMQP):
17
- # publish → "{exchange} publish" (or "{queue} publish" when no exchange)
18
- # receive → "{queue} receive"
16
+ # Formula per SPAN_NAME_PATTERNS.txt Section 3 (bunny/AMQP / Kafka):
17
+ # RabbitMQ publish → "{exchange} publish" (or "{queue} publish" when no exchange)
18
+ # RabbitMQ receive → "{queue} receive"
19
+ # Kafka → "{service} {access}" (e.g., "orders send")
19
20
  #
20
21
  # @return [String] The span name
21
22
  def span_name
22
- rabbitmq_data = span[:data]&.[](:rabbitmq) || {}
23
+ if (rabbitmq_data = span[:data]&.[](:rabbitmq))
24
+ rabbitmq_span_name(rabbitmq_data)
25
+ elsif (kafka_data = span[:data]&.[](:kafka))
26
+ kafka_span_name(kafka_data)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ def convert_attributes
33
+ attributes = {}
34
+
35
+ convert_rabbitmq_attributes(attributes, span[:data]&.[](:rabbitmq))
36
+ convert_kafka_attributes(attributes, span[:data]&.[](:kafka))
37
+
38
+ attributes
39
+ end
40
+
41
+ private
42
+
43
+ def rabbitmq_span_name(rabbitmq_data)
23
44
  sort = rabbitmq_data[:sort].to_s
24
45
 
25
46
  if sort == 'publish'
@@ -32,26 +53,51 @@ module Instana
32
53
  end
33
54
  end
34
55
 
35
- def convert_attributes
36
- attributes = {}
56
+ def kafka_span_name(kafka_data)
57
+ service = kafka_data[:service].to_s.strip
58
+ access = kafka_data[:access].to_s.strip
59
+ parts = [service, access].reject(&:empty?)
60
+ parts.empty? ? 'kafka' : parts.join(' ')
61
+ end
37
62
 
38
- rabbitmq_data = span[:data]&.[](:rabbitmq)
39
- if rabbitmq_data
40
- add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, 'rabbitmq')
41
- add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_DESTINATION_NAME,
42
- rabbitmq_destination_name(rabbitmq_data))
43
- add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY, rabbitmq_data[:key])
44
- add_attribute(attributes, 'messaging.rabbitmq.queue', rabbitmq_data[:queue])
45
- add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, rabbitmq_data[:address])
46
-
47
- operation = rabbitmq_data[:sort] == 'publish' ? 'send' : 'receive'
48
- add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_TYPE, operation)
49
- end
63
+ def convert_rabbitmq_attributes(attributes, rabbitmq_data)
64
+ return unless rabbitmq_data
50
65
 
51
- attributes
66
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, 'rabbitmq')
67
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_DESTINATION_NAME,
68
+ rabbitmq_destination_name(rabbitmq_data))
69
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY, rabbitmq_data[:key])
70
+ add_attribute(attributes, 'messaging.rabbitmq.queue', rabbitmq_data[:queue])
71
+ add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, extract_host(rabbitmq_data[:address]))
72
+ add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, extract_port(rabbitmq_data[:address]))
73
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_MESSAGE_BODY_SIZE, rabbitmq_data[:size])
74
+
75
+ operation = rabbitmq_data[:sort] == 'publish' ? 'send' : 'receive'
76
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_TYPE, operation)
77
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_NAME, rabbitmq_data[:sort])
52
78
  end
53
79
 
54
- private
80
+ def convert_kafka_attributes(attributes, kafka_data)
81
+ return unless kafka_data
82
+
83
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, 'kafka')
84
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_DESTINATION_NAME, kafka_data[:service])
85
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_NAME, kafka_data[:access])
86
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_TYPE, kafka_data[:access])
87
+ end
88
+
89
+ def extract_host(address)
90
+ return nil unless address
91
+
92
+ address.to_s.split(':').first
93
+ end
94
+
95
+ def extract_port(address)
96
+ return nil unless address
97
+
98
+ port = address.to_s.split(':').last
99
+ port.to_i if port =~ /^\d+$/
100
+ end
55
101
 
56
102
  # Build the composite destination name per spec:
57
103
  # Producer (publish): "{exchange}:{key}" — omit absent parts
@@ -6,6 +6,7 @@ require_relative 'base_converter'
6
6
  require 'opentelemetry/semconv/incubating/rpc'
7
7
  require 'opentelemetry/semconv/incubating/code'
8
8
  require 'opentelemetry/semconv/server'
9
+ require 'opentelemetry/semconv/network'
9
10
 
10
11
  module Instana
11
12
  module Exporter
@@ -48,10 +49,13 @@ module Instana
48
49
 
49
50
  private
50
51
 
52
+ # gRPC status code mapping: 0 = OK for success, otherwise derive from error
53
+ GRPC_STATUS_CODE_OK = 0
54
+
51
55
  # Convert gRPC span attributes
52
56
  def convert_grpc_attributes(attributes, rpc_data)
53
57
  # RPC system
54
- add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM, 'grpc')
58
+ add_attribute(attributes, Instana::Exporter::Otlp::RPC_SYSTEM_ATTR, 'grpc')
55
59
 
56
60
  # RPC service and method
57
61
  if rpc_data[:call]
@@ -60,9 +64,20 @@ module Instana
60
64
  add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_METHOD, method)
61
65
  end
62
66
 
63
- # Network peer
67
+ # Server host and port
64
68
  add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, rpc_data[:host])
65
- add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, rpc_data.dig(:peer, :address))
69
+ add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, parse_port(rpc_data[:port]))
70
+
71
+ # Network peer address (peer.address → network.peer.address)
72
+ peer = rpc_data[:peer]
73
+ if peer
74
+ add_attribute(attributes, OpenTelemetry::SemConv::NETWORK::NETWORK_PEER_ADDRESS, peer[:address])
75
+ add_attribute(attributes, OpenTelemetry::SemConv::NETWORK::NETWORK_PEER_PORT, parse_port(peer[:port]))
76
+ end
77
+
78
+ # gRPC status code (integer): 0 = OK for success spans
79
+ grpc_status = grpc_status_code(rpc_data)
80
+ add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_GRPC_STATUS_CODE, grpc_status)
66
81
 
67
82
  # gRPC-specific attributes
68
83
  add_attribute(attributes, 'rpc.grpc.call_type', rpc_data[:call_type])
@@ -71,7 +86,7 @@ module Instana
71
86
  # Convert ActionCable span attributes
72
87
  def convert_action_cable_attributes(attributes, rpc_data)
73
88
  # RPC system
74
- add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM, 'actioncable')
89
+ add_attribute(attributes, Instana::Exporter::Otlp::RPC_SYSTEM_ATTR, 'actioncable')
75
90
 
76
91
  # ActionCable-specific attributes
77
92
  add_attribute(attributes, 'rails.actioncable.channel', rpc_data[:call])
@@ -96,6 +111,51 @@ module Instana
96
111
 
97
112
  [parts[1], parts[2]]
98
113
  end
114
+
115
+ # Parse port value to integer, returns nil if invalid or out of range
116
+ def parse_port(port)
117
+ return nil unless port
118
+
119
+ int_port = port.to_i
120
+ int_port if int_port >= 0 && int_port <= 65_535
121
+ end
122
+
123
+ # Map to gRPC status code integer.
124
+ # For successful spans (ec = 0) → 0 (OK).
125
+ # For error spans with a recognized gRPC status string in the error → map it.
126
+ # Otherwise → nil (omit the attribute).
127
+ GRPC_STATUS_CODES = {
128
+ 'OK' => 0,
129
+ 'CANCELLED' => 1,
130
+ 'UNKNOWN' => 2,
131
+ 'INVALID_ARGUMENT' => 3,
132
+ 'DEADLINE_EXCEEDED' => 4,
133
+ 'NOT_FOUND' => 5,
134
+ 'ALREADY_EXISTS' => 6,
135
+ 'PERMISSION_DENIED' => 7,
136
+ 'RESOURCE_EXHAUSTED' => 8,
137
+ 'FAILED_PRECONDITION' => 9,
138
+ 'ABORTED' => 10,
139
+ 'OUT_OF_RANGE' => 11,
140
+ 'UNIMPLEMENTED' => 12,
141
+ 'INTERNAL' => 13,
142
+ 'UNAVAILABLE' => 14,
143
+ 'DATA_LOSS' => 15,
144
+ 'UNAUTHENTICATED' => 16
145
+ }.freeze
146
+
147
+ def grpc_status_code(rpc_data)
148
+ error_count = span[:ec].to_i
149
+ return GRPC_STATUS_CODE_OK if error_count.zero?
150
+
151
+ # Attempt to extract gRPC status string from error message
152
+ error_msg = rpc_data[:error].to_s
153
+ GRPC_STATUS_CODES.each do |name, code|
154
+ return code if error_msg.include?(name)
155
+ end
156
+
157
+ GRPC_STATUS_CODES['UNKNOWN']
158
+ end
99
159
  end
100
160
  end
101
161
  end
@@ -2,6 +2,6 @@
2
2
  # (c) Copyright Instana Inc. 2016
3
3
 
4
4
  module Instana
5
- VERSION = "2.9.0"
5
+ VERSION = "2.10.0"
6
6
  VERSION_FULL = "instana-#{VERSION}"
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: instana
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.0
4
+ version: 2.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Giacomo Lombardo
@@ -416,7 +416,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
416
416
  - !ruby/object:Gem::Version
417
417
  version: '0'
418
418
  requirements: []
419
- rubygems_version: 4.0.21
419
+ rubygems_version: 4.0.22
420
420
  specification_version: 4
421
421
  summary: Ruby Distributed Tracing & Metrics Sensor for Instana
422
422
  test_files: []