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 +4 -4
- data/README.md +77 -2
- data/lib/rails_otel_context/activerecord_context.rb +4 -4
- data/lib/rails_otel_context/adapters/clickhouse.rb +51 -49
- data/lib/rails_otel_context/body_capture.rb +2 -1
- data/lib/rails_otel_context/configuration.rb +1 -1
- data/lib/rails_otel_context/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4eb35396c5cddc8f420624441b7f2bdacf2d1ef005510db0855cb9803cfb4635
|
|
4
|
+
data.tar.gz: e8a3ffbb994abad920bf7ccbc66903777d380952711237ca4ce0cd7ef153c7c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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(
|
|
217
|
-
attrs.store(
|
|
218
|
-
attrs.store(
|
|
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(
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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 =
|
|
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']
|