opentrace 0.12.0 → 0.13.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/README.md +353 -16
- data/lib/opentrace/client.rb +11 -4
- data/lib/opentrace/http_tracker.rb +2 -1
- data/lib/opentrace/local_vars.rb +18 -6
- data/lib/opentrace/payload_builder.rb +6 -1
- data/lib/opentrace/pii_scrubber.rb +2 -2
- data/lib/opentrace/source_context.rb +11 -1
- data/lib/opentrace/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: efabcbbf59438deda1b76fcaa0953218bd2ab1144388e0abd20cef1c77d42582
|
|
4
|
+
data.tar.gz: 6df6eef94307d9ee5ab79869cd785abea16464dd7488fe7c52288a3f65f326a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac55ce5f272fea2dfa221b9a08283f0d70a534f09fe4119fece6a2e953231f863f346c95997f685f65f47ec4ffa9e48af7b5dba6445ca9e21d8ce162d980034a
|
|
7
|
+
data.tar.gz: 927293c59438670c43b58a486355bb227b988bad34a3532b234d538fc871ec541d62279668c2c495eacfc54d1ef5a7eef1c778238d30906b19356020452e1f15
|
data/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A thin, safe Ruby client that forwards structured application logs to an [OpenTr
|
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
|
+
### Core
|
|
14
15
|
- **Zero-risk integration** -- all errors swallowed, never raises to host app
|
|
15
16
|
- **Async dispatch** -- logs are queued in-memory and sent via a background thread
|
|
16
17
|
- **Batch sending** -- groups logs into configurable batches for efficient network usage
|
|
@@ -19,28 +20,59 @@ A thin, safe Ruby client that forwards structured application logs to an [OpenTr
|
|
|
19
20
|
- **Works with any server** -- Puma (threads), Unicorn (forks), Passenger, and Falcon (fibers)
|
|
20
21
|
- **Fork safe** -- detects forked worker processes and re-initializes cleanly
|
|
21
22
|
- **Fiber safe** -- uses `Fiber[]` storage for correct request isolation in fiber-based servers
|
|
22
|
-
- **Rails integration** -- auto-instruments controllers, SQL queries, ActiveJob, views, cache, and more
|
|
23
|
-
- **Rack middleware** -- propagates `request_id` via fiber-local storage
|
|
24
|
-
- **Logger wrapper** -- drop-in replacement that forwards to OpenTrace while keeping your original logger
|
|
25
|
-
- **Rails 7.1+ BroadcastLogger** -- native support via `broadcast_to`
|
|
26
|
-
- **TaggedLogging** -- preserves `ActiveSupport::TaggedLogging` tags in metadata
|
|
27
|
-
- **Context support** -- attach global metadata to every log via Hash or Proc
|
|
28
|
-
- **Business events** -- `OpenTrace.event` sends typed events (e.g. `payment.completed`) that bypass level filtering
|
|
29
23
|
- **Level filtering** -- `min_level` threshold or `allowed_levels` list to control which severities are forwarded
|
|
30
24
|
- **Auto-enrichment** -- every log includes `hostname`, `pid`, and `git_sha` automatically
|
|
31
|
-
- **Exception helper** -- `OpenTrace.error` captures class, message, cleaned backtrace, and error fingerprint
|
|
32
25
|
- **Runtime controls** -- enable/disable logging at runtime without restarting
|
|
33
26
|
- **Graceful shutdown** -- pending logs are flushed automatically on process exit
|
|
34
|
-
- **
|
|
27
|
+
- **Adaptive sampling** -- graduated backpressure reduces overhead under load (configurable `sample_rate`)
|
|
28
|
+
- **Deferred payloads** -- request thread pushes frozen arrays; heavy work runs on background thread
|
|
29
|
+
|
|
30
|
+
### Instrumentation
|
|
31
|
+
- **Custom instrumentation** -- `OpenTrace.trace("stripe.charge") { ... }` with nested spans and timing
|
|
32
|
+
- **Exception helper** -- `OpenTrace.error` captures class, message, cleaned backtrace, cause chain, and error fingerprint
|
|
33
|
+
- **Exception cause chaining** -- walks `exception.cause` up to 5 levels deep
|
|
34
|
+
- **Breadcrumbs** -- `OpenTrace.add_breadcrumb` records a trail of events attached to errors
|
|
35
|
+
- **Source code context** -- captures surrounding source lines at the error origin (opt-in)
|
|
36
|
+
- **Local variables capture** -- `OpenTrace.capture_binding(e, binding)` snapshots variables at crash point (opt-in)
|
|
37
|
+
- **Transaction naming** -- `OpenTrace.set_transaction_name` for custom grouping
|
|
38
|
+
- **Business events** -- `OpenTrace.event` sends typed events (e.g. `payment.completed`) that bypass level filtering
|
|
39
|
+
- **Context support** -- attach global metadata to every log via Hash or Proc
|
|
40
|
+
|
|
41
|
+
### Rails Integration
|
|
42
|
+
- **Auto-instrumentation** -- controllers, SQL queries, ActiveJob, views, cache, deprecation warnings
|
|
43
|
+
- **Rack middleware** -- propagates `request_id` via fiber-local storage
|
|
35
44
|
- **Per-request summary** -- one rich log per request with SQL, view, cache breakdown and timeline
|
|
45
|
+
- **N+1 query detection** -- warns when a request exceeds 20 SQL queries
|
|
46
|
+
- **Duplicate query detection** -- fingerprints SQL queries to find repeated patterns
|
|
47
|
+
- **SQL normalization** -- replaces literals with `?` for grouping; generates stable fingerprints
|
|
48
|
+
- **EXPLAIN plan capture** -- runs EXPLAIN on slow queries asynchronously on background thread (opt-in)
|
|
49
|
+
- **Log trace injection** -- injects `[trace_id=xxx request_id=yyy]` into Rails logger output (opt-in)
|
|
50
|
+
- **Session tracking** -- extracts session ID from rack session or cookies (opt-in)
|
|
51
|
+
- **Logger wrapper** -- drop-in replacement that forwards to OpenTrace while keeping your original logger
|
|
52
|
+
- **Rails 7.1+ BroadcastLogger** -- native support via `broadcast_to`
|
|
53
|
+
- **TaggedLogging** -- preserves `ActiveSupport::TaggedLogging` tags in metadata
|
|
36
54
|
- **Error fingerprinting** -- stable fingerprint for grouping identical errors across requests
|
|
37
55
|
- **Deprecation tracking** -- captures Rails deprecation warnings with callsite
|
|
56
|
+
|
|
57
|
+
### Data Protection
|
|
58
|
+
- **PII scrubbing** -- automatic detection and redaction of emails, credit cards, SSNs, tokens, passwords (opt-in)
|
|
59
|
+
- **Lifecycle callbacks** -- `on_error`, `after_send`, `before_breadcrumb`, `before_send` hooks
|
|
60
|
+
- **Before-send filter** -- drop or modify payloads before delivery
|
|
61
|
+
|
|
62
|
+
### Monitoring
|
|
38
63
|
- **DB pool monitoring** -- background thread reports connection pool saturation (opt-in)
|
|
39
64
|
- **Job queue depth** -- monitors Sidekiq, GoodJob, or SolidQueue queue sizes (opt-in)
|
|
40
65
|
- **Memory delta tracking** -- snapshots process RSS before/after each request (opt-in)
|
|
41
66
|
- **External HTTP tracking** -- captures outbound Net::HTTP calls with timing (opt-in)
|
|
42
|
-
- **
|
|
67
|
+
- **GC/Runtime metrics** -- periodic collection of GC stats, thread count, and process RSS (opt-in)
|
|
68
|
+
|
|
69
|
+
### Delivery
|
|
43
70
|
- **Distributed tracing** -- W3C Trace Context (`traceparent`) propagation across services with span IDs
|
|
71
|
+
- **Unix socket transport** -- 2-5x faster delivery for co-located servers with automatic HTTP fallback (opt-in)
|
|
72
|
+
- **Gzip compression** -- automatic payload compression for bandwidth reduction
|
|
73
|
+
- **Version negotiation** -- startup compatibility check with capability-based feature detection
|
|
74
|
+
- **Circuit breaker** -- stops sending when server is unreachable, resumes after cooldown
|
|
75
|
+
- **Exponential backoff** -- retries with jitter on server errors
|
|
44
76
|
|
|
45
77
|
## Installation
|
|
46
78
|
|
|
@@ -89,7 +121,7 @@ OpenTrace.configure do |c|
|
|
|
89
121
|
c.environment = "production" # default: nil
|
|
90
122
|
c.timeout = 1.0 # HTTP timeout in seconds (default: 1.0)
|
|
91
123
|
c.enabled = true # default: true
|
|
92
|
-
c.min_level = :info # minimum level to forward (default: :
|
|
124
|
+
c.min_level = :info # minimum level to forward (default: :info)
|
|
93
125
|
c.allowed_levels = [:warn, :error] # explicit level list (overrides min_level, default: nil)
|
|
94
126
|
c.batch_size = 50 # logs per batch (default: 50)
|
|
95
127
|
c.flush_interval = 5.0 # seconds between flushes (default: 5.0)
|
|
@@ -105,15 +137,15 @@ OpenTrace.configure do |c|
|
|
|
105
137
|
c.git_sha = ENV["REVISION"] # checks REVISION, GIT_SHA, HEROKU_SLUG_COMMIT
|
|
106
138
|
|
|
107
139
|
# SQL logging (Rails only)
|
|
108
|
-
c.sql_logging =
|
|
140
|
+
c.sql_logging = false # forward individual SQL queries (default: false)
|
|
109
141
|
c.sql_duration_threshold_ms = 100.0 # only log queries slower than this (default: 0.0 = all)
|
|
110
142
|
|
|
111
|
-
# Path filtering
|
|
112
|
-
c.ignore_paths = ["/health", %r{\A/assets/}] #
|
|
143
|
+
# Path filtering (defaults include /up, /health, /healthz, /ping, /ready, /livez, /readyz)
|
|
144
|
+
c.ignore_paths = ["/health", %r{\A/assets/}] # customize paths to skip
|
|
113
145
|
|
|
114
146
|
# Per-request summary (Rails only)
|
|
115
147
|
c.request_summary = true # accumulate events into one rich log (default: true)
|
|
116
|
-
c.timeline =
|
|
148
|
+
c.timeline = false # include event timeline in summary (default: false)
|
|
117
149
|
c.timeline_max_events = 200 # cap timeline entries (default: 200)
|
|
118
150
|
|
|
119
151
|
# Background monitors (opt-in)
|
|
@@ -125,6 +157,42 @@ OpenTrace.configure do |c|
|
|
|
125
157
|
# Advanced opt-in features
|
|
126
158
|
c.memory_tracking = false # RSS delta per request (default: false)
|
|
127
159
|
c.http_tracking = false # external HTTP call tracking (default: false)
|
|
160
|
+
|
|
161
|
+
# Sampling & performance
|
|
162
|
+
c.sample_rate = 1.0 # 0.0-1.0, fraction of requests to trace (default: 1.0)
|
|
163
|
+
c.sampler = ->(env) { 0.1 } # dynamic per-endpoint sampler (default: nil)
|
|
164
|
+
c.before_send = ->(payload) { payload } # filter/drop payloads before delivery (default: nil)
|
|
165
|
+
|
|
166
|
+
# SQL normalization (default: true)
|
|
167
|
+
c.sql_normalization = true # replace SQL literals with ? for grouping
|
|
168
|
+
|
|
169
|
+
# Instrumentation
|
|
170
|
+
c.source_context = false # capture source code around errors (default: false)
|
|
171
|
+
c.local_vars_capture = false # enable OpenTrace.capture_binding (default: false)
|
|
172
|
+
c.log_trace_injection = false # inject trace_id into Rails logger (default: false)
|
|
173
|
+
c.session_tracking = false # extract session ID from cookies (default: false)
|
|
174
|
+
|
|
175
|
+
# EXPLAIN plan capture
|
|
176
|
+
c.explain_slow_queries = false # run EXPLAIN on slow queries (default: false)
|
|
177
|
+
c.explain_threshold_ms = 100.0 # threshold in ms (default: 100.0)
|
|
178
|
+
|
|
179
|
+
# PII protection
|
|
180
|
+
c.pii_scrubbing = false # scrub PII from metadata (default: false)
|
|
181
|
+
c.pii_patterns = [/CUST-\d{8}/] # additional patterns (default: nil)
|
|
182
|
+
c.pii_disabled_patterns = [:phone] # disable built-in patterns (default: nil)
|
|
183
|
+
|
|
184
|
+
# Lifecycle callbacks
|
|
185
|
+
c.on_error = ->(exc, meta) { } # called on error capture (default: nil)
|
|
186
|
+
c.after_send = ->(batch_size, bytes) { } # called after delivery (default: nil)
|
|
187
|
+
c.before_breadcrumb = ->(crumb) { crumb } # filter breadcrumbs (default: nil)
|
|
188
|
+
|
|
189
|
+
# GC/Runtime metrics
|
|
190
|
+
c.runtime_metrics = false # collect GC/thread/memory stats (default: false)
|
|
191
|
+
c.runtime_metrics_interval = 30 # seconds between collections (default: 30)
|
|
192
|
+
|
|
193
|
+
# Unix socket transport
|
|
194
|
+
c.transport = :http # :http or :unix_socket (default: :http)
|
|
195
|
+
c.socket_path = "/tmp/opentrace.sock" # path to Unix socket (default: "/tmp/opentrace.sock")
|
|
128
196
|
end
|
|
129
197
|
```
|
|
130
198
|
|
|
@@ -185,6 +253,96 @@ This captures:
|
|
|
185
253
|
- `exception_message` -- truncated to 500 characters
|
|
186
254
|
- `backtrace` -- cleaned (Rails backtrace cleaner or gem-filtered), limited to 15 frames
|
|
187
255
|
- `error_fingerprint` -- 12-char hash for grouping identical errors (stable across line number changes)
|
|
256
|
+
- `exception_causes` -- full cause chain (up to 5 levels via `exception.cause`)
|
|
257
|
+
- `breadcrumbs` -- trail of events leading up to the error (if any were added)
|
|
258
|
+
- `source_context` -- surrounding source code lines at the error origin (when enabled)
|
|
259
|
+
- `local_variables` -- variable state at crash point (when `capture_binding` was called)
|
|
260
|
+
|
|
261
|
+
### Custom Instrumentation
|
|
262
|
+
|
|
263
|
+
Trace any block of code with automatic timing:
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
OpenTrace.trace("stripe.charge", resource: "Invoice") do |span|
|
|
267
|
+
span.set_tag(:amount, 2000)
|
|
268
|
+
Stripe::Charge.create(amount: 2000, currency: "usd")
|
|
269
|
+
end
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Spans can be nested -- child spans automatically track their parent:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
OpenTrace.trace("checkout.process") do
|
|
276
|
+
OpenTrace.trace("checkout.validate") { validate_cart }
|
|
277
|
+
OpenTrace.trace("checkout.charge") { charge_card }
|
|
278
|
+
OpenTrace.trace("checkout.fulfill") { create_order }
|
|
279
|
+
end
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Each span emits a log entry with `span_operation`, `span_duration_ms`, and parent/child IDs. When a `RequestCollector` is active, spans also appear in the request summary.
|
|
283
|
+
|
|
284
|
+
### Breadcrumbs
|
|
285
|
+
|
|
286
|
+
Record a trail of events leading up to an error:
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
OpenTrace.add_breadcrumb("auth", "User logged in", { provider: "google" })
|
|
290
|
+
OpenTrace.add_breadcrumb("nav", "Visited /settings")
|
|
291
|
+
OpenTrace.add_breadcrumb("action", "Changed password")
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Breadcrumbs are stored per-request (Fiber-local, max 25) and automatically attached to error payloads. They are cleared after each request.
|
|
295
|
+
|
|
296
|
+
Filter breadcrumbs with a callback:
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
OpenTrace.configure do |c|
|
|
300
|
+
c.before_breadcrumb = ->(crumb) {
|
|
301
|
+
crumb.category == "noisy" ? nil : crumb # return nil to drop
|
|
302
|
+
}
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Local Variables Capture
|
|
307
|
+
|
|
308
|
+
Capture the state of local variables when an error occurs:
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
OpenTrace.configure do |c|
|
|
312
|
+
c.local_vars_capture = true
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def update_profile(user, params)
|
|
316
|
+
user.update!(params)
|
|
317
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
318
|
+
OpenTrace.capture_binding(e, binding) # explicit capture
|
|
319
|
+
OpenTrace.error(e, { action: "update_profile" })
|
|
320
|
+
raise
|
|
321
|
+
end
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
This produces:
|
|
325
|
+
|
|
326
|
+
```json
|
|
327
|
+
{
|
|
328
|
+
"local_variables": [
|
|
329
|
+
{ "name": "user", "value": "#<User id: 42, name: nil>", "type": "User" },
|
|
330
|
+
{ "name": "params", "value": "{\"name\"=>\"\"}", "type": "Hash" }
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Capped at 10 variables, 500 chars per value. No global VM hooks -- zero overhead unless you explicitly call `capture_binding`.
|
|
336
|
+
|
|
337
|
+
### Transaction Naming
|
|
338
|
+
|
|
339
|
+
Override the auto-detected transaction name for custom grouping:
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
OpenTrace.set_transaction_name("API::V2::Users#search")
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The transaction name appears in the request log message and metadata, enabling grouping by business operation instead of route.
|
|
188
346
|
|
|
189
347
|
### Business Events
|
|
190
348
|
|
|
@@ -383,7 +541,7 @@ Configure SQL logging:
|
|
|
383
541
|
```ruby
|
|
384
542
|
OpenTrace.configure do |c|
|
|
385
543
|
# ...
|
|
386
|
-
c.sql_logging = true # enable/disable (default:
|
|
544
|
+
c.sql_logging = true # enable/disable (default: false)
|
|
387
545
|
c.sql_duration_threshold_ms = 100.0 # only log slow queries (default: 0.0 = all)
|
|
388
546
|
end
|
|
389
547
|
```
|
|
@@ -543,6 +701,185 @@ A recursion guard prevents OpenTrace's own HTTP calls to the server from being t
|
|
|
543
701
|
|
|
544
702
|
**Note**: This works by prepending a module to `Net::HTTP`. Libraries that use `Net::HTTP` internally (Faraday, HTTParty, RestClient) are automatically captured.
|
|
545
703
|
|
|
704
|
+
### Source Code Context
|
|
705
|
+
|
|
706
|
+
When enabled, error logs include the surrounding source lines at the crash location:
|
|
707
|
+
|
|
708
|
+
```ruby
|
|
709
|
+
OpenTrace.configure do |c|
|
|
710
|
+
c.source_context = true
|
|
711
|
+
end
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
Produces:
|
|
715
|
+
|
|
716
|
+
```json
|
|
717
|
+
{
|
|
718
|
+
"source_context": {
|
|
719
|
+
"file": "app/models/order.rb",
|
|
720
|
+
"line": 42,
|
|
721
|
+
"context": {
|
|
722
|
+
"39": " def total",
|
|
723
|
+
"40": " items.sum(:price) +",
|
|
724
|
+
"41": " tax_amount +",
|
|
725
|
+
"42": " shipping_cost",
|
|
726
|
+
"43": " end"
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Files are cached (LRU, 50 files max). Only reads files under `/app/`, `/lib/`, or `/config/` and smaller than 100KB.
|
|
733
|
+
|
|
734
|
+
### SQL Normalization
|
|
735
|
+
|
|
736
|
+
Enabled by default. Replaces SQL literals with `?` placeholders for grouping:
|
|
737
|
+
|
|
738
|
+
```
|
|
739
|
+
SELECT * FROM users WHERE id = 42 AND email = 'alice@example.com'
|
|
740
|
+
=> SELECT * FROM users WHERE id = ? AND email = ?
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
Each normalized query gets a 12-char fingerprint (MD5) for fast grouping. The request summary includes `top_duplicates` with the most repeated query patterns.
|
|
744
|
+
|
|
745
|
+
### Duplicate Query Detection
|
|
746
|
+
|
|
747
|
+
When a `RequestCollector` is active, SQL queries are fingerprinted and counted. The request summary includes:
|
|
748
|
+
|
|
749
|
+
- `duplicate_queries` -- number of fingerprints seen more than once
|
|
750
|
+
- `worst_duplicate_count` -- highest repeat count
|
|
751
|
+
- `top_duplicates` -- top 3 repeated queries with count and fingerprint
|
|
752
|
+
- `n_plus_one_warning` -- `true` when worst duplicate exceeds 5
|
|
753
|
+
|
|
754
|
+
### Log Trace Injection
|
|
755
|
+
|
|
756
|
+
Injects trace context into your existing Rails logger output:
|
|
757
|
+
|
|
758
|
+
```ruby
|
|
759
|
+
OpenTrace.configure do |c|
|
|
760
|
+
c.log_trace_injection = true
|
|
761
|
+
end
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
Before: `Processing by UsersController#show`
|
|
765
|
+
After: `[trace_id=abc123 request_id=req-456] Processing by UsersController#show`
|
|
766
|
+
|
|
767
|
+
### Session Tracking
|
|
768
|
+
|
|
769
|
+
Extracts session ID from the Rack session or session cookie:
|
|
770
|
+
|
|
771
|
+
```ruby
|
|
772
|
+
OpenTrace.configure do |c|
|
|
773
|
+
c.session_tracking = true
|
|
774
|
+
end
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
The session ID appears in request metadata, enabling session-level analysis.
|
|
778
|
+
|
|
779
|
+
### PII Scrubbing
|
|
780
|
+
|
|
781
|
+
Automatically detects and redacts sensitive data before sending:
|
|
782
|
+
|
|
783
|
+
```ruby
|
|
784
|
+
OpenTrace.configure do |c|
|
|
785
|
+
c.pii_scrubbing = true
|
|
786
|
+
end
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
Built-in patterns detect: credit card numbers, email addresses, SSNs, phone numbers, bearer tokens, and API keys. Sensitive keys (`password`, `secret`, `token`, `api_key`, `authorization`) are always redacted.
|
|
790
|
+
|
|
791
|
+
Customize patterns:
|
|
792
|
+
|
|
793
|
+
```ruby
|
|
794
|
+
OpenTrace.configure do |c|
|
|
795
|
+
c.pii_scrubbing = true
|
|
796
|
+
c.pii_patterns = [/CUST-\d{8}/] # add custom patterns
|
|
797
|
+
c.pii_disabled_patterns = [:phone] # disable built-in patterns
|
|
798
|
+
end
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
PII scrubbing runs on the background thread -- zero request-thread overhead.
|
|
802
|
+
|
|
803
|
+
### EXPLAIN Plan Capture
|
|
804
|
+
|
|
805
|
+
Automatically captures EXPLAIN output for slow SQL queries:
|
|
806
|
+
|
|
807
|
+
```ruby
|
|
808
|
+
OpenTrace.configure do |c|
|
|
809
|
+
c.explain_slow_queries = true
|
|
810
|
+
c.explain_threshold_ms = 50.0 # queries slower than 50ms
|
|
811
|
+
end
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
The SQL text is captured on the request thread (zero DB overhead). EXPLAIN is executed asynchronously on the background thread using a separate DB connection. Max 3 EXPLAIN queries per request.
|
|
815
|
+
|
|
816
|
+
```json
|
|
817
|
+
{
|
|
818
|
+
"explain_plans": [{
|
|
819
|
+
"sql": "SELECT * FROM orders WHERE user_id = 42 ORDER BY created_at DESC",
|
|
820
|
+
"duration_ms": 87.3,
|
|
821
|
+
"explain_plan": "Seq Scan on orders (cost=0.00..45892.00 rows=12 width=380)\n Filter: (user_id = 42)"
|
|
822
|
+
}]
|
|
823
|
+
}
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### GC/Runtime Metrics
|
|
827
|
+
|
|
828
|
+
Background thread collects Ruby runtime stats at a configurable interval:
|
|
829
|
+
|
|
830
|
+
```ruby
|
|
831
|
+
OpenTrace.configure do |c|
|
|
832
|
+
c.runtime_metrics = true
|
|
833
|
+
c.runtime_metrics_interval = 30 # seconds (default: 30)
|
|
834
|
+
end
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
Metrics collected: `gc_count`, `gc_major_count`, `gc_minor_count`, `gc_heap_live_slots`, `gc_heap_free_slots`, `gc_malloc_increase_bytes`, `thread_count`, `process_rss_mb`, `process_pid`. Sent as `event_type: "runtime.metrics"`.
|
|
838
|
+
|
|
839
|
+
### Unix Socket Transport
|
|
840
|
+
|
|
841
|
+
For deployments where the OpenTrace server runs on the same host:
|
|
842
|
+
|
|
843
|
+
```ruby
|
|
844
|
+
OpenTrace.configure do |c|
|
|
845
|
+
c.transport = :unix_socket
|
|
846
|
+
c.socket_path = "/var/run/opentrace.sock"
|
|
847
|
+
end
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
2-5x faster than HTTP for local delivery (no TCP/TLS overhead). Falls back to HTTP automatically if the socket is unavailable. Same batching, compression, and retry guarantees.
|
|
851
|
+
|
|
852
|
+
### Lifecycle Callbacks
|
|
853
|
+
|
|
854
|
+
Hook into key lifecycle events:
|
|
855
|
+
|
|
856
|
+
```ruby
|
|
857
|
+
OpenTrace.configure do |c|
|
|
858
|
+
# Called when OpenTrace.error captures an exception
|
|
859
|
+
c.on_error = ->(exception, metadata) {
|
|
860
|
+
Sentry.capture_exception(exception) if metadata[:exception_class] == "CriticalError"
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
# Called after each successful batch delivery
|
|
864
|
+
c.after_send = ->(batch_size, bytes) {
|
|
865
|
+
StatsD.histogram("opentrace.batch_size", batch_size)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
# Filter or modify breadcrumbs
|
|
869
|
+
c.before_breadcrumb = ->(crumb) {
|
|
870
|
+
crumb.category == "secret" ? nil : crumb # return nil to drop
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
# Filter or modify entire payloads before delivery
|
|
874
|
+
c.before_send = ->(payload) {
|
|
875
|
+
payload[:metadata].delete(:internal_debug) if Rails.env.production?
|
|
876
|
+
payload # return nil to drop the entire payload
|
|
877
|
+
}
|
|
878
|
+
end
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
All callbacks are wrapped in rescue -- a broken callback will never affect the host app.
|
|
882
|
+
|
|
546
883
|
## Runtime Controls
|
|
547
884
|
|
|
548
885
|
```ruby
|
data/lib/opentrace/client.rb
CHANGED
|
@@ -212,7 +212,9 @@ module OpenTrace
|
|
|
212
212
|
nil
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
MAX_BATCH_SPLIT_DEPTH = 5
|
|
216
|
+
|
|
217
|
+
def send_batch(batch, depth: 0)
|
|
216
218
|
# Circuit breaker: skip if server is known-down
|
|
217
219
|
unless @circuit_breaker.allow_request?
|
|
218
220
|
@stats.increment(:dropped_circuit_open, batch.size)
|
|
@@ -246,12 +248,17 @@ module OpenTrace
|
|
|
246
248
|
|
|
247
249
|
json = JSON.generate(batch)
|
|
248
250
|
|
|
249
|
-
# If entire batch exceeds limit, split and retry
|
|
251
|
+
# If entire batch exceeds limit, split and retry (with depth guard)
|
|
250
252
|
if json.bytesize > @config.max_payload_bytes
|
|
253
|
+
if depth >= MAX_BATCH_SPLIT_DEPTH
|
|
254
|
+
@stats.increment(:dropped_oversized, batch.size)
|
|
255
|
+
fire_on_drop(batch.size, :oversized)
|
|
256
|
+
return
|
|
257
|
+
end
|
|
251
258
|
@stats.increment(:payload_splits)
|
|
252
259
|
mid = batch.size / 2
|
|
253
|
-
send_batch(batch[0...mid]) if mid > 0
|
|
254
|
-
send_batch(batch[mid..]) if mid < batch.size
|
|
260
|
+
send_batch(batch[0...mid], depth: depth + 1) if mid > 0
|
|
261
|
+
send_batch(batch[mid..], depth: depth + 1) if mid < batch.size
|
|
255
262
|
return
|
|
256
263
|
end
|
|
257
264
|
|
|
@@ -23,7 +23,8 @@ module OpenTrace
|
|
|
23
23
|
host = address
|
|
24
24
|
port_str = (port == 443 || port == 80) ? "" : ":#{port}"
|
|
25
25
|
scheme = use_ssl? ? "https" : "http"
|
|
26
|
-
|
|
26
|
+
safe_path = req.path.to_s.split("?").first
|
|
27
|
+
url = "#{scheme}://#{host}#{port_str}#{safe_path}"
|
|
27
28
|
|
|
28
29
|
if collector
|
|
29
30
|
collector.record_http(
|
data/lib/opentrace/local_vars.rb
CHANGED
|
@@ -5,6 +5,13 @@ module OpenTrace
|
|
|
5
5
|
MAX_VARS = 10
|
|
6
6
|
MAX_VALUE_LENGTH = 500
|
|
7
7
|
|
|
8
|
+
SENSITIVE_PATTERNS = %w[
|
|
9
|
+
password passwd secret token api_key apikey
|
|
10
|
+
authorization auth_token access_token refresh_token
|
|
11
|
+
credit_card card_number cvv ssn private_key
|
|
12
|
+
session_id cookie credential
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
8
15
|
module_function
|
|
9
16
|
|
|
10
17
|
# Capture local variables from an explicit binding.
|
|
@@ -24,17 +31,22 @@ module OpenTrace
|
|
|
24
31
|
# Skip internal variables (_, _1, etc.)
|
|
25
32
|
next if name.to_s.start_with?("_")
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
name: name.to_s,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
name_s = name.to_s.downcase
|
|
35
|
+
if sensitive_name?(name_s)
|
|
36
|
+
{ name: name.to_s, value: "[FILTERED]", type: "filtered" }
|
|
37
|
+
else
|
|
38
|
+
value = binding_obj.local_variable_get(name)
|
|
39
|
+
{ name: name.to_s, value: safe_inspect(value), type: value.class.name }
|
|
40
|
+
end
|
|
33
41
|
end
|
|
34
42
|
rescue StandardError
|
|
35
43
|
nil
|
|
36
44
|
end
|
|
37
45
|
|
|
46
|
+
def sensitive_name?(name)
|
|
47
|
+
SENSITIVE_PATTERNS.any? { |pattern| name.include?(pattern) }
|
|
48
|
+
end
|
|
49
|
+
|
|
38
50
|
def safe_inspect(value)
|
|
39
51
|
str = value.inspect
|
|
40
52
|
str.length > MAX_VALUE_LENGTH ? str[0, MAX_VALUE_LENGTH] + "..." : str
|
|
@@ -167,8 +167,13 @@ module OpenTrace
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
def run_explain(sql)
|
|
170
|
+
# Only EXPLAIN simple SELECTs — reject anything suspicious
|
|
171
|
+
normalized = sql.to_s.strip
|
|
172
|
+
return nil unless normalized.match?(/\ASELECT\b/i)
|
|
173
|
+
return nil if normalized.include?(";") # No multi-statement
|
|
174
|
+
|
|
170
175
|
ActiveRecord::Base.connection_pool.with_connection do |conn|
|
|
171
|
-
result = conn.execute("EXPLAIN #{
|
|
176
|
+
result = conn.execute("EXPLAIN #{normalized}")
|
|
172
177
|
rows = result.respond_to?(:rows) ? result.rows : result.map(&:values)
|
|
173
178
|
rows.flatten.join("\n").slice(0, 2000)
|
|
174
179
|
end
|
|
@@ -5,8 +5,8 @@ module OpenTrace
|
|
|
5
5
|
REDACTED = "[REDACTED]"
|
|
6
6
|
|
|
7
7
|
PATTERNS = {
|
|
8
|
-
credit_card: /\b(?:\d[ -]*?){13,
|
|
9
|
-
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-
|
|
8
|
+
credit_card: /\b(?:\d[ -]*?){13,16}\b/,
|
|
9
|
+
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/,
|
|
10
10
|
ssn: /\b\d{3}-\d{2}-\d{4}\b/,
|
|
11
11
|
phone: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/,
|
|
12
12
|
bearer_token: /Bearer\s+[A-Za-z0-9\-._~+\/]+=*/,
|
|
@@ -59,7 +59,17 @@ module OpenTrace
|
|
|
59
59
|
|
|
60
60
|
def safe_path?(path)
|
|
61
61
|
return false unless path.include?("/app/") || path.include?("/lib/") || path.include?("/config/")
|
|
62
|
-
|
|
62
|
+
|
|
63
|
+
# Resolve symlinks and '..' to prevent path traversal
|
|
64
|
+
real = File.realpath(path)
|
|
65
|
+
|
|
66
|
+
# Verify the resolved path is under the application root
|
|
67
|
+
if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
|
|
68
|
+
root = ::Rails.root.to_s
|
|
69
|
+
return false unless real.start_with?("#{root}/")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
File.size(real) <= MAX_FILE_SIZE
|
|
63
73
|
rescue StandardError
|
|
64
74
|
false
|
|
65
75
|
end
|
data/lib/opentrace/version.rb
CHANGED