rails-otel-context 0.9.8 → 0.9.9

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: 0aa66c7dfd0480bd0a9100e00fbea58eaab5ec0153dd5cb4a82b8df840092787
4
- data.tar.gz: 9743746552b3a934798f2812a61f9d4f72f5c296ba75dae943b9dcb5120e8f5e
3
+ metadata.gz: 4eb35396c5cddc8f420624441b7f2bdacf2d1ef005510db0855cb9803cfb4635
4
+ data.tar.gz: e8a3ffbb994abad920bf7ccbc66903777d380952711237ca4ce0cd7ef153c7c0
5
5
  SHA512:
6
- metadata.gz: 40c59f1db646b5d159e23b36d9f651b4809ee19dc8f797ef9b6a5d261810ccfabc7d50c4460e01ac1b132058e1a28d9312b235d0c66e2f99fa6503f98a5ce4b9
7
- data.tar.gz: 828f792d02e5ff2c7588d946f8535f5d41f25ad84e55f22854f73e007cb6bf6c39eaf1f05ab40eedae6a37c0d8d5c9e952d127428255221d47bee6d72e51170f
6
+ metadata.gz: b1c75e3474d1d922f96427658daafbe73b7ca41e9f8f59a6654f7cc2ef4576a7d055287d1fb95b75161a1c178848350672f7b05f798ef080aef50236d6d36824
7
+ data.tar.gz: e2708c7b6dfa266cd62f66d7b302f54aa181108b47fa7fc589fe5e5a0645a42bab0a4d73753f3fd3c326df24c99280052b025808995cda86815662d7848bb573
data/README.md CHANGED
@@ -76,7 +76,7 @@ DB spans additionally get:
76
76
 
77
77
  ## Configuration
78
78
 
79
- Zero configuration gets you everything above. The optional initializer adds span naming and slow-query detection:
79
+ Zero configuration gets you everything above. The optional initializer adds span naming, slow-query detection, and opts into the heavier adapters:
80
80
 
81
81
  ```ruby
82
82
  # config/initializers/rails_otel_context.rb
@@ -115,6 +115,11 @@ RailsOtelContext.configure do |c|
115
115
 
116
116
  # Attach any per-request context to every span
117
117
  c.custom_span_attributes = -> { { 'tenant' => Current.tenant } if Current.tenant }
118
+
119
+ # Opt-in adapters — disabled by default because they patch third-party classes
120
+ # and add a span per call. Turn on what you actually use.
121
+ c.clickhouse_enabled = true # instrument click_house gem queries
122
+ c.connection_pool_tracing_enabled = true # span per AR connection checkout
118
123
  end
119
124
  ```
120
125
 
@@ -222,7 +227,13 @@ end
222
227
 
223
228
  ## Redis and ClickHouse
224
229
 
225
- Redis and ClickHouse spans get the same `code.*` attributes pointing to the app-code frame that issued the call:
230
+ Redis is enabled by default. ClickHouse requires opt-in:
231
+
232
+ ```ruby
233
+ c.clickhouse_enabled = true
234
+ ```
235
+
236
+ Both get the same `code.*` attributes pointing to the app-code frame that issued the call:
226
237
 
227
238
  ```json
228
239
  {
@@ -237,6 +248,70 @@ Redis and ClickHouse spans get the same `code.*` attributes pointing to the app-
237
248
  }
238
249
  ```
239
250
 
251
+ ### ClickHouse span naming
252
+
253
+ ClickHouse spans follow the OTel DB convention: `"{VERB} {table}"`. No configuration required — this happens automatically when the ClickHouse adapter is active.
254
+
255
+ | Query | Span name |
256
+ |---|---|
257
+ | `SELECT * FROM events` | `SELECT events` |
258
+ | `INSERT INTO analytics.page_views ...` | `INSERT page_views` |
259
+ | `OPTIMIZE TABLE logs` | `OPTIMIZE clickhouse` (no FROM — falls back gracefully) |
260
+
261
+ Schema-qualified tables (`db.table`) are supported: `db.name` gets the schema, `db.sql.table` gets the bare table name.
262
+
263
+ If you've configured a `span_name_formatter`, it runs on ClickHouse spans too — same formatter, no extra wiring. The original OTel name is preserved in `l9.orig.name`.
264
+
265
+ ## Capturing request and response bodies
266
+
267
+ `BodyCapture` is a Rack middleware that attaches request and response bodies to the active OTel span. It's opt-in — add it to your middleware stack:
268
+
269
+ ```ruby
270
+ # config/application.rb (or an initializer, after OTel is configured)
271
+ config.middleware.use RailsOtelContext::BodyCapture
272
+ ```
273
+
274
+ That's the zero-config path. Bodies land on `http.request.body` and `http.response.body`, capped at 8 KB, for `application/json`, `application/xml`, and `text/plain` content types. `/health`, `/ready`, and `/metrics` are excluded by default.
275
+
276
+ The option that matters most in production:
277
+
278
+ ```ruby
279
+ config.middleware.use RailsOtelContext::BodyCapture, on_error_only: true
280
+ ```
281
+
282
+ With `on_error_only: true`, the response body is never buffered on the success path — zero overhead for 2xx. Bodies are only captured when status >= 400, which is exactly when you need them.
283
+
284
+ Full options:
285
+
286
+ | Option | Default | Description |
287
+ |---|---|---|
288
+ | `on_error_only:` | `false` | Capture only on 4xx/5xx |
289
+ | `max_bytes:` | `8192` | Truncate bodies above this size |
290
+ | `content_types:` | `%w[application/json application/xml text/plain]` | Allowlist |
291
+ | `include_paths:` | `nil` | Restrict capture to these path prefixes |
292
+ | `exclude_paths:` | `%w[/health /ready /metrics]` | Skip these path prefixes |
293
+
294
+ ## Connection pool tracing
295
+
296
+ When you're chasing timeouts or thread starvation, knowing that `checkout` spent 200ms waiting for a connection is the difference between guessing and knowing. Both `clickhouse_enabled` and `connection_pool_tracing_enabled` are off by default — they patch third-party classes and add a span per call, which is overhead you should choose consciously. Enable it:
297
+
298
+ ```ruby
299
+ RailsOtelContext.configure do |c|
300
+ c.connection_pool_tracing_enabled = true
301
+ end
302
+ ```
303
+
304
+ Each `checkout` call gets its own span with pool state at the moment of acquisition:
305
+
306
+ | Attribute | Description |
307
+ |---|---|
308
+ | `db.pool.size` | Total pool capacity |
309
+ | `db.pool.busy` | Connections currently checked out |
310
+ | `db.pool.idle` | Connections available |
311
+ | `db.pool.waiting` | Threads queued waiting for a connection |
312
+
313
+ Spans inside transactions and `with_connection` blocks are skipped — a pinned connection is already held, so there's nothing to measure.
314
+
240
315
  ## Performance
241
316
 
242
317
  `CallContextProcessor#on_start` fires for every span. For a typical 10–20 span request the overhead is in the low-microsecond range and does not require configuration.
@@ -213,9 +213,9 @@ module RailsOtelContext
213
213
  attrs = span.instance_variable_get(:@attributes)
214
214
  return unless attrs.respond_to?(:store)
215
215
 
216
- attrs.store(AR_MODEL_ATTR, ctx[:model_name]) if ctx[:model_name]
217
- attrs.store(AR_METHOD_ATTR, ctx[:method_name]) if ctx[:method_name]
218
- attrs.store(AR_SCOPE_ATTR, ctx[:scope_name]) if ctx[:scope_name]
216
+ attrs.store('code.activerecord.model', ctx[:model_name]) if ctx[:model_name]
217
+ attrs.store('code.activerecord.method', ctx[:method_name]) if ctx[:method_name]
218
+ attrs.store('code.activerecord.scope', ctx[:scope_name]) if ctx[:scope_name]
219
219
 
220
220
  formatter = RailsOtelContext.configuration.span_name_formatter
221
221
  return unless formatter && span.respond_to?(:name) && attrs.key?('db.system')
@@ -228,7 +228,7 @@ module RailsOtelContext
228
228
  new_name = formatter.call(original_name, ar_ctx)
229
229
  return unless new_name && new_name != original_name
230
230
 
231
- attrs.store(ORIG_NAME_ATTR, original_name)
231
+ attrs.store('l9.orig.name', original_name)
232
232
  span.instance_variable_set(:@name, new_name)
233
233
  rescue StandardError
234
234
  nil
@@ -7,9 +7,12 @@ module RailsOtelContext
7
7
 
8
8
  # click_house gem v1.x used :query/:select; v2.x uses :select_all/:select_one/:select_value.
9
9
  # We list all known variants so install! picks whichever the loaded gem version defines.
10
+ #
11
+ # insert/insert_rows/insert_compact are intentionally absent: in v2.x they all delegate
12
+ # to execute, so patching execute alone is sufficient and avoids wrapping methods whose
13
+ # keyword-argument signatures may differ across gem versions.
10
14
  CANDIDATE_METHODS = %i[
11
15
  select_all select_one select_value
12
- insert insert_compact insert_rows
13
16
  execute command
14
17
  query select
15
18
  ].freeze
@@ -51,13 +54,11 @@ module RailsOtelContext
51
54
  end
52
55
 
53
56
  # Maps compound gem method names to their SQL verb for span naming.
54
- # select_all/select_one/select_value → SELECT; insert_* → INSERT.
57
+ # select_all/select_one/select_value → SELECT.
55
58
  METHOD_OP_ALIAS = {
56
59
  'SELECT_ALL' => 'SELECT',
57
60
  'SELECT_ONE' => 'SELECT',
58
- 'SELECT_VALUE' => 'SELECT',
59
- 'INSERT_COMPACT' => 'INSERT',
60
- 'INSERT_ROWS' => 'INSERT'
61
+ 'SELECT_VALUE' => 'SELECT'
61
62
  }.freeze
62
63
 
63
64
  # Derives a human-readable span name from the SQL statement.
@@ -115,55 +116,56 @@ module RailsOtelContext
115
116
  .fetch(method_name.to_s.upcase, method_name.to_s.upcase)
116
117
  .freeze
117
118
 
118
- define_method(method_name) do |*args, &block|
119
- return super(*args, &block) if Thread.current[reentrancy_key]
120
-
121
- site = mod.call_site_for_app
122
- statement = args.first.is_a?(String) ? args.first : nil
123
-
124
- # Parse table once — span_name_for accepts the pre-parsed value to skip
125
- # the internal regex scan, and we reuse db_name for db.name attribute.
126
- db_name, table_name = RailsOtelContext::Adapters::Clickhouse.parse_table(statement)
127
- sql_verb = statement ? statement.lstrip.split(/\s/, 2).first&.upcase || method_op : method_op
128
- span_name = RailsOtelContext::Adapters::Clickhouse.span_name_for(
129
- statement, method_op, table_name: table_name
130
- )
119
+ define_method(method_name) do |*args, **kwargs, &block|
120
+ return super(*args, **kwargs, &block) if Thread.current[reentrancy_key]
131
121
 
132
- tracer = OpenTelemetry.tracer_provider.tracer('rails-otel-context-clickhouse')
133
122
  Thread.current[reentrancy_key] = true
134
-
135
- tracer.in_span(span_name, kind: :client) do |span|
136
- span.set_attribute('db.system', 'clickhouse')
137
- span.set_attribute('db.operation', sql_verb)
138
- span.set_attribute('db.statement', statement) if statement
139
- span.set_attribute('db.name', db_name) if db_name
140
- span.set_attribute('db.sql.table', table_name) if table_name
141
-
142
- result = super(*args, &block)
143
- mod.apply_call_site_to_span(span, site)
144
-
145
- # ClickHouse spans don't fire sql.active_record notifications, so
146
- # CallContextProcessor#apply_db_context never runs for them.
147
- # Apply the span_name_formatter here with a synthetic AR-shaped context
148
- # built from code.namespace/code.function set by apply_call_site_to_span.
149
- formatter = RailsOtelContext.configuration.span_name_formatter
150
- code_ns = formatter && span.respond_to?(:attributes) &&
151
- span.attributes['code.namespace']
152
- if code_ns
153
- fn = span.attributes['code.function']
154
- ar_ctx = { model_name: code_ns, method_name: fn, scope_name: nil,
155
- code_namespace: code_ns, code_function: fn }
156
- new_name = formatter.call(span_name, ar_ctx)
157
- if new_name && new_name != span_name
158
- span.set_attribute('l9.orig.name', span_name)
159
- span.name = new_name
123
+ begin
124
+ site = mod.call_site_for_app
125
+ statement = args.first.is_a?(String) ? args.first : nil
126
+
127
+ # Avoid a second regex scan: pass pre-parsed table_name to span_name_for,
128
+ # and reuse db_name for the db.name attribute.
129
+ db_name, table_name = RailsOtelContext::Adapters::Clickhouse.parse_table(statement)
130
+ sql_verb = statement ? statement.lstrip.split(/\s/, 2).first&.upcase || method_op : method_op
131
+ span_name = RailsOtelContext::Adapters::Clickhouse.span_name_for(
132
+ statement, method_op, table_name: table_name
133
+ )
134
+
135
+ tracer = OpenTelemetry.tracer_provider.tracer('rails-otel-context-clickhouse')
136
+ tracer.in_span(span_name, kind: :client) do |span|
137
+ span.set_attribute('db.system', 'clickhouse')
138
+ span.set_attribute('db.operation', sql_verb)
139
+ span.set_attribute('db.statement', statement) if statement
140
+ span.set_attribute('db.name', db_name) if db_name
141
+ span.set_attribute('db.sql.table', table_name) if table_name
142
+
143
+ result = super(*args, **kwargs, &block)
144
+ mod.apply_call_site_to_span(span, site)
145
+
146
+ # ClickHouse spans don't fire sql.active_record notifications, so
147
+ # CallContextProcessor#apply_db_context never runs for them.
148
+ # Apply the span_name_formatter here with a synthetic AR-shaped context
149
+ # built from code.namespace/code.function set by apply_call_site_to_span.
150
+ formatter = RailsOtelContext.configuration.span_name_formatter
151
+ code_ns = formatter && span.respond_to?(:attributes) &&
152
+ span.attributes['code.namespace']
153
+ if code_ns
154
+ fn = span.attributes['code.function']
155
+ ar_ctx = { model_name: code_ns, method_name: fn, scope_name: nil,
156
+ code_namespace: code_ns, code_function: fn }
157
+ new_name = formatter.call(span_name, ar_ctx)
158
+ if new_name && new_name != span_name
159
+ span.set_attribute('l9.orig.name', span_name)
160
+ span.name = new_name
161
+ end
160
162
  end
161
- end
162
163
 
163
- result
164
+ result
165
+ end
166
+ ensure
167
+ Thread.current[reentrancy_key] = false
164
168
  end
165
- ensure
166
- Thread.current[reentrancy_key] = false
167
169
  end
168
170
  end
169
171
  end
@@ -92,7 +92,8 @@ module RailsOtelContext
92
92
  end
93
93
 
94
94
  def drain_response(body, headers)
95
- chunks = body.map { |chunk| chunk }
95
+ chunks = []
96
+ body.each { |chunk| chunks << chunk } # rubocop:disable Style/MapIntoArray -- RackBody (Rails 8) has no #map
96
97
  body.close if body.respond_to?(:close)
97
98
 
98
99
  content_type = headers['Content-Type'] || headers['content-type']
@@ -16,7 +16,7 @@ module RailsOtelContext
16
16
 
17
17
  def initialize
18
18
  @redis_source_enabled = false
19
- @clickhouse_enabled = true
19
+ @clickhouse_enabled = false
20
20
  @connection_pool_tracing_enabled = false
21
21
  @span_name_formatter = nil
22
22
  @custom_span_attributes = nil
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsOtelContext
4
- VERSION = '0.9.8'
4
+ VERSION = '0.9.9'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-otel-context
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.8
4
+ version: 0.9.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Last9