dead_bro 0.2.30 → 0.2.32
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/CHANGELOG.md +13 -0
- data/README.md +12 -173
- data/lib/dead_bro/job_subscriber.rb +42 -113
- data/lib/dead_bro/sql_subscriber.rb +103 -1
- data/lib/dead_bro/sql_tracking_middleware.rb +1 -0
- data/lib/dead_bro/subscriber.rb +77 -33
- data/lib/dead_bro/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: 22b021f41cc62097a8fc1f1aedf489e8ee1dca10b2b2abc6edbce6809763b39a
|
|
4
|
+
data.tar.gz: 30c442462399195cec1edb2f9505aa51896230da92d18ad97f5eca35d2749221
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d0cbef4fcc8b05de9e8a4c1c357352a17cd133b372ab0239b2402b4d8e7322728a881f0679cbfe0df812a0c3e4143e78d35279a7780e525810efc438b6d5d90
|
|
7
|
+
data.tar.gz: 81bd8d1e49b663c5ae1d651e26abcaa2eb3dcc10f7bf71155d79da9a59d03aafaee9dc282670fd376b9b99991a3074763ed2abf5461f405022d26f86fe4ecc23
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.32] - 2026-09-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Errored requests now ship the SQL/cache/memory/etc data captured before the exception, not just the exception itself.** Previously, as soon as a request raised (e.g. an `ActiveRecord::LockWaitTimeout` mid-transaction), the subscriber discarded every already-captured `sql_queries`, `cache_events`, `ar_instantiation_count`, `memory_events`, `gc_stats`, etc. entry and sent only exception metadata — so error pages showed a backtrace with zero query context even when the gem had captured it. The error payload now carries the same detail fields a successful request would.
|
|
7
|
+
- **Background job failures were reported to the dashboard as successful completions, not dropped.** There is no `exception.active_job` event anywhere in Rails/ActiveJob — the handler that subscribed to it never fired. `perform.active_job` (the real event) fires whether or not the job raised, but its handler built `status: "completed"` unconditionally and never inspected `data[:exception_object]`, so a failing job wasn't invisible — it was recorded as if it had succeeded. This silently skewed job success-rate and duration numbers for every account on a version prior to this one; there's nothing to backfill, since no exception data was ever sent for those occurrences. The handler now branches on `data[:exception_object]`: a failing job reports `status: "failed"` with `error`, `fingerprint`, and `cause_chain`, and ships regardless of sampling.
|
|
8
|
+
- **Known limitation:** this only covers a job whose exception escapes `perform_now` uncaught. A job using `retry_on`, `discard_on`, or a custom `rescue_from` has the exception handled *inside* `perform_now`, before `perform.active_job`'s payload is ever built — so a retried or discarded job still reports `status: "completed"` for every handled attempt. Only the final attempt of a `retry_on` job that exhausts its retries with no block given re-raises far enough to be visible. Covering retried/discarded attempts would need separate instrumentation on `enqueue_retry.active_job` / `retry_stopped.active_job` / `discard.active_job` — not done here.
|
|
9
|
+
- **A request opening 5 or more transactions/savepoints could trip a false N+1 flag.** Every Rails adapter checked (MySQL/PostgreSQL/SQLite3, 7.1–8.1) logs transaction-control statements under the single name `"TRANSACTION"`, which the old skip list (`data[:name] == "BEGIN" || ...`) never matched — so `BEGIN`/`COMMIT`/etc. weren't ignored, they were tracked as ordinary queries: sanitized, counted toward N+1 detection, and folded into one aggregate per normalized statement. Five or more transactions in one request hit `N_PLUS_ONE_THRESHOLD` and flagged that aggregate `n_plus_one: true`, which the backend trusts directly for the aggregate payload format. Fixed as a consequence of the `TXN_CONTROL_NAMES` match below, not a separate change.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Transaction-control statements (`BEGIN`/`COMMIT`/`ROLLBACK`/`SAVEPOINT`/`RELEASE`) are now captured as lightweight breadcrumbs (`transaction_events`, with offsets) instead of being tracked as ordinary queries — so a transaction that rolled back before an error shows up explicitly in the request trace, instead of as an anonymous, sanitized `sql_queries` entry.
|
|
13
|
+
|
|
14
|
+
## [0.2.31] - 2026-08-30
|
|
15
|
+
|
|
3
16
|
### Added
|
|
4
17
|
- Monitor thread now sends a synchronous heartbeat on startup before the first collection tick. This ensures remote settings — including `monitor_enabled` — are applied from the very first reporting cycle, so Sidekiq workers and other non-web processes that have not yet sent any metrics still receive the correct configuration immediately on boot rather than waiting up to 60 seconds for the first scheduled tick.
|
|
5
18
|
|
data/README.md
CHANGED
|
@@ -65,115 +65,9 @@ Use the DeadBro UI to turn features on or off, set sample rates, define controll
|
|
|
65
65
|
|
|
66
66
|
You can still set `config.enabled` in Ruby if you need to force the integration off in a given environment before any remote settings arrive; otherwise the dashboard can control `enabled` like other remote settings.
|
|
67
67
|
|
|
68
|
-
##
|
|
68
|
+
## Request Sampling, Exclusions & Whitelisting
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
## Request Sampling
|
|
73
|
-
|
|
74
|
-
DeadBro supports configurable request sampling to reduce the volume of metrics sent to your APM endpoint, which is useful for high-traffic applications. Prefer setting this in the DeadBro app; use Ruby if you need a local override.
|
|
75
|
-
|
|
76
|
-
### Configuration
|
|
77
|
-
|
|
78
|
-
Set the sample rate as a percentage (1-100):
|
|
79
|
-
|
|
80
|
-
```ruby
|
|
81
|
-
# Track 50% of requests
|
|
82
|
-
DeadBro.configure do |config|
|
|
83
|
-
config.sample_rate = 50
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Track 10% of requests (useful for high-traffic apps)
|
|
87
|
-
DeadBro.configure do |config|
|
|
88
|
-
config.sample_rate = 10
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Track all requests (default)
|
|
92
|
-
DeadBro.configure do |config|
|
|
93
|
-
config.sample_rate = 100
|
|
94
|
-
end
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### How It Works
|
|
98
|
-
|
|
99
|
-
- **Random Sampling**: Each request has a random chance of being tracked based on the sample rate
|
|
100
|
-
- **Consistent Per-Request**: The sampling decision is made once per request and applies to all metrics for that request
|
|
101
|
-
- **Debug Logging**: Skipped requests do not count towards the montly limit
|
|
102
|
-
- **Error Tracking**: Errors are still tracked regardless of sampling
|
|
103
|
-
|
|
104
|
-
### Use Cases
|
|
105
|
-
|
|
106
|
-
- **High-Traffic Applications**: Reduce APM data volume and costs
|
|
107
|
-
- **Development/Staging**: Sample fewer requests to reduce noise
|
|
108
|
-
- **Performance Testing**: Track a subset of requests during load testing
|
|
109
|
-
- **Cost Optimization**: Balance monitoring coverage with data costs
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
## Excluding Controllers and Jobs
|
|
113
|
-
|
|
114
|
-
You can exclude specific controllers and jobs from APM tracking (dashboard first; Ruby optional).
|
|
115
|
-
|
|
116
|
-
### Configuration
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
```ruby
|
|
120
|
-
DeadBro.configure do |config|
|
|
121
|
-
# Controller-only or controller#action patterns in one list (wildcards supported)
|
|
122
|
-
config.excluded_controllers = [
|
|
123
|
-
"HealthChecksController",
|
|
124
|
-
"Admin::*",
|
|
125
|
-
"UsersController#show",
|
|
126
|
-
"Admin::ReportsController#index",
|
|
127
|
-
"Admin::*#*"
|
|
128
|
-
]
|
|
129
|
-
|
|
130
|
-
config.excluded_jobs = [
|
|
131
|
-
"ActiveStorage::AnalyzeJob",
|
|
132
|
-
"Admin::*"
|
|
133
|
-
]
|
|
134
|
-
end
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Notes:
|
|
138
|
-
- Wildcards `*` are supported (e.g., `Admin::*`, `Admin::*#*`).
|
|
139
|
-
- Matching uses full names like `UsersController`, `Admin::ReportsController#index`, `MyJob`.
|
|
140
|
-
|
|
141
|
-
## Exclusive Tracking (Whitelist Mode)
|
|
142
|
-
|
|
143
|
-
You can configure DeadBro to **only** track specific controllers, actions, or jobs. Prefer the dashboard; use Ruby for overrides.
|
|
144
|
-
|
|
145
|
-
### Configuration
|
|
146
|
-
|
|
147
|
-
```ruby
|
|
148
|
-
DeadBro.configure do |config|
|
|
149
|
-
# Only track these controllers/actions (patterns can include #action or wildcards)
|
|
150
|
-
config.exclusive_controllers = [
|
|
151
|
-
"UsersController#show",
|
|
152
|
-
"UsersController#index",
|
|
153
|
-
"Admin::ReportsController#*",
|
|
154
|
-
"Api::*#*"
|
|
155
|
-
]
|
|
156
|
-
|
|
157
|
-
config.exclusive_jobs = [
|
|
158
|
-
"PaymentProcessingJob",
|
|
159
|
-
"EmailDeliveryJob",
|
|
160
|
-
"Admin::*"
|
|
161
|
-
]
|
|
162
|
-
end
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### How It Works
|
|
166
|
-
|
|
167
|
-
- **If `exclusive_controllers` or `exclusive_jobs` is empty/not defined**: All controllers/actions/jobs are tracked (default behavior)
|
|
168
|
-
- **If `exclusive_controllers` or `exclusive_jobs` is defined with values**: Only matching controllers/actions/jobs are tracked
|
|
169
|
-
- **Exclusion takes precedence**: If something matches both `excluded_*` and `exclusive_*`, it is excluded (exclusion is checked first)
|
|
170
|
-
|
|
171
|
-
### Use Cases
|
|
172
|
-
|
|
173
|
-
- **Focus on Critical Paths**: Monitor only your most important endpoints
|
|
174
|
-
- **Cost Optimization**: Track only specific high-value operations
|
|
175
|
-
- **Debugging**: Temporarily focus on specific controllers/jobs during investigation
|
|
176
|
-
- **Compliance**: Track only operations that require monitoring for compliance reasons
|
|
70
|
+
To control data volume, DeadBro supports request sampling (track a percentage of requests), excluding specific controllers/jobs from tracking, and whitelisting (tracking *only* specific controllers/jobs). All of this is configured in the DeadBro dashboard — sample rate, `excluded_controllers`/`excluded_jobs`, and `exclusive_controllers`/`exclusive_jobs` patterns (wildcards like `Admin::*` and `Admin::*#*` supported). Exclusion always takes precedence over whitelisting when a pattern matches both.
|
|
177
71
|
|
|
178
72
|
## SQL Query Tracking
|
|
179
73
|
|
|
@@ -198,43 +92,19 @@ DeadBro can automatically capture the query plan (`EXPLAIN`) of slow SELECT quer
|
|
|
198
92
|
- **Value scrubbing**: quoted string literals echoed in plan text (e.g. `Filter: email = 'a@b.com'`) are replaced with `?` before the plan leaves your app.
|
|
199
93
|
- **Bounded overhead**: plans are captured on background threads (max 3 concurrent per request); at request end DeadBro waits at most 0.5s for stragglers, then drops them.
|
|
200
94
|
|
|
201
|
-
### How It Works
|
|
202
|
-
|
|
203
|
-
- **Automatic Detection**: when a SELECT exceeds `slow_query_threshold_ms`, DeadBro captures its plan in the background
|
|
204
|
-
- **Database Support**: PostgreSQL, MySQL, SQLite (`EXPLAIN QUERY PLAN`), and any adapter with a standard `EXPLAIN`
|
|
205
|
-
|
|
206
95
|
### Configuration
|
|
207
96
|
|
|
208
|
-
- **`explain_analyze_enabled`** (default: `false`)
|
|
209
|
-
- **`slow_query_threshold_ms`** (default: `500`) - queries taking longer than this threshold will have their execution plan captured
|
|
97
|
+
- **`explain_analyze_enabled`** (default: `false`) — this is the one setting that must be turned on in Ruby; the dashboard toggle can only turn capture *off*, never on, as an extra safety rail:
|
|
210
98
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
config.explain_analyze_enabled = true
|
|
217
|
-
config.slow_query_threshold_ms = 500
|
|
218
|
-
|
|
219
|
-
# Or use a higher threshold for production
|
|
220
|
-
# config.slow_query_threshold_ms = 1000 # Only explain queries > 1 second
|
|
221
|
-
end
|
|
222
|
-
```
|
|
99
|
+
```ruby
|
|
100
|
+
DeadBro.configure do |config|
|
|
101
|
+
config.explain_analyze_enabled = true
|
|
102
|
+
end
|
|
103
|
+
```
|
|
223
104
|
|
|
224
|
-
|
|
105
|
+
- Everything else — the `slow_query_threshold_ms` cutoff and enabling/disabling the feature — is managed from the dashboard.
|
|
225
106
|
|
|
226
|
-
When a slow query is detected, the `explain_plan` field in the SQL query data
|
|
227
|
-
- **PostgreSQL / MySQL**: `EXPLAIN` output — the planner's chosen strategy, estimated costs and row counts
|
|
228
|
-
- **SQLite**: `EXPLAIN QUERY PLAN` output
|
|
229
|
-
- **Other databases**: standard `EXPLAIN` output
|
|
230
|
-
|
|
231
|
-
This execution plan helps you:
|
|
232
|
-
- Identify missing indexes
|
|
233
|
-
- Understand query execution order
|
|
234
|
-
- Spot full table scans
|
|
235
|
-
- Optimize JOIN operations
|
|
236
|
-
|
|
237
|
-
Because the statement is not executed, plans show the planner's *estimates* rather than actual runtimes — usually exactly what you need to spot a missing index or an unexpected sequential scan.
|
|
107
|
+
When a slow query is detected, the `explain_plan` field in the SQL query data contains the database's `EXPLAIN` (or `EXPLAIN QUERY PLAN` on SQLite) output — the planner's chosen strategy, estimated costs and row counts — useful for spotting missing indexes or unexpected sequential scans. Because the statement is never executed, plans show estimates rather than actual runtimes.
|
|
238
108
|
|
|
239
109
|
## View Rendering Tracking
|
|
240
110
|
|
|
@@ -259,20 +129,7 @@ By default, DeadBro uses **lightweight memory tracking** that has minimal perfor
|
|
|
259
129
|
- **GC Efficiency Analysis**: Monitor garbage collection effectiveness
|
|
260
130
|
- **Zero Allocation Tracking**: No object allocation tracking by default (can be enabled)
|
|
261
131
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
Usually managed in the dashboard; Ruby example:
|
|
265
|
-
|
|
266
|
-
```ruby
|
|
267
|
-
DeadBro.configure do |config|
|
|
268
|
-
config.memory_tracking_enabled = true # Enable lightweight memory tracking (default: true)
|
|
269
|
-
config.allocation_tracking_enabled = false # Enable detailed allocation tracking (default: false)
|
|
270
|
-
config.allocation_sample_rate = 100 # % of requests that pay for allocation tracking when enabled (1-100, default: 100)
|
|
271
|
-
|
|
272
|
-
# Sampling configuration
|
|
273
|
-
config.sample_rate = 100 # Percentage of requests to track (1-100, default: 100)
|
|
274
|
-
end
|
|
275
|
-
```
|
|
132
|
+
`memory_tracking_enabled`, `allocation_tracking_enabled`, and `allocation_sample_rate` are all managed from the dashboard.
|
|
276
133
|
|
|
277
134
|
**Performance Impact:**
|
|
278
135
|
- **Lightweight mode** (`memory_tracking_enabled`, ~0.1ms overhead per request): RSS before/after, GC pressure, the retained-vs-transient signals (`heap_live_slots_growth`, `malloc_increase_bytes`), and per-phase allocation attribution (`allocation_phases` — which of `sql`/`view`/`elasticsearch` allocated the request's objects).
|
|
@@ -310,25 +167,7 @@ Everything is **best effort** and designed to be **safe and low overhead**:
|
|
|
310
167
|
|
|
311
168
|
### Configuration
|
|
312
169
|
|
|
313
|
-
Enable or disable
|
|
314
|
-
|
|
315
|
-
```ruby
|
|
316
|
-
DeadBro.configure do |config|
|
|
317
|
-
# Enable the periodic job queue monitor (disabled by default)
|
|
318
|
-
config.job_queue_monitoring_enabled = true
|
|
319
|
-
|
|
320
|
-
# Enable best-effort collectors (all default to false)
|
|
321
|
-
config.enable_db_stats = true # ActiveRecord pool + ping latency
|
|
322
|
-
config.enable_process_stats = true # pid, hostname, RSS, GC, threads, fds
|
|
323
|
-
config.enable_system_stats = true # CPU%, memory, disk, network
|
|
324
|
-
|
|
325
|
-
# Filesystem paths to report disk usage for (default: ["/"])
|
|
326
|
-
config.disk_paths = ["/", "/var"]
|
|
327
|
-
|
|
328
|
-
# Network interfaces to ignore when computing rx/tx stats
|
|
329
|
-
config.interfaces_ignore = %w[lo docker0]
|
|
330
|
-
end
|
|
331
|
-
```
|
|
170
|
+
Enable or disable the job queue monitor and the individual collectors (`enable_db_stats`, `enable_process_stats`, `enable_system_stats`), plus `disk_paths` and `interfaces_ignore`, from the DeadBro dashboard.
|
|
332
171
|
|
|
333
172
|
### Example Payload Shape
|
|
334
173
|
|
|
@@ -9,7 +9,6 @@ end
|
|
|
9
9
|
module DeadBro
|
|
10
10
|
class JobSubscriber
|
|
11
11
|
JOB_EVENT_NAME = "perform.active_job"
|
|
12
|
-
JOB_EXCEPTION_EVENT_NAME = "exception.active_job"
|
|
13
12
|
|
|
14
13
|
def self.subscribe!(client: Client.new)
|
|
15
14
|
# Snap GC state before the job runs so stop_request_tracking gets a valid diff
|
|
@@ -19,7 +18,28 @@ module DeadBro
|
|
|
19
18
|
rescue
|
|
20
19
|
end
|
|
21
20
|
|
|
22
|
-
# Track job execution
|
|
21
|
+
# Track job execution — success AND failure both land here. ActiveJob wraps
|
|
22
|
+
# perform with `instrument(:perform) { super }` (see
|
|
23
|
+
# ActiveJob::Instrumentation#instrument), and AS::Notifications.instrument
|
|
24
|
+
# still runs its "finish" listeners (with :exception / :exception_object set
|
|
25
|
+
# in the payload) when the block raises, then re-raises. There is no separate
|
|
26
|
+
# "exception.active_job" event anywhere in Rails/ActiveJob — a prior version
|
|
27
|
+
# of this file subscribed to one, so it never fired; the job wasn't dropped,
|
|
28
|
+
# it fired *this* event and built status: "completed" unconditionally,
|
|
29
|
+
# silently reporting every failing job as a success. Branching on
|
|
30
|
+
# data[:exception_object] here is the only place a job failure can actually
|
|
31
|
+
# be detected.
|
|
32
|
+
#
|
|
33
|
+
# Known gap: this only sees an exception that escapes perform_now uncaught.
|
|
34
|
+
# ActiveJob::Base#perform_now (Execution) rescues internally and hands off to
|
|
35
|
+
# rescue_with_handler — which is exactly what retry_on/discard_on/rescue_from
|
|
36
|
+
# are built on (ActiveJob::Exceptions) — before Instrumentation's `super`
|
|
37
|
+
# even returns. A handled retry or discard returns normally with no
|
|
38
|
+
# exception attached, so perform.active_job still reports status:
|
|
39
|
+
# "completed" for every handled attempt; only a retry_on job's final,
|
|
40
|
+
# unhandled raise (attempts exhausted, no block given) is visible here.
|
|
41
|
+
# Covering handled attempts would need separate instrumentation on
|
|
42
|
+
# enqueue_retry.active_job / retry_stopped.active_job / discard.active_job.
|
|
23
43
|
ActiveSupport::Notifications.subscribe(JOB_EVENT_NAME) do |name, started, finished, _unique_id, data|
|
|
24
44
|
begin
|
|
25
45
|
if DeadBro.configuration.skip_tracking?
|
|
@@ -40,12 +60,14 @@ module DeadBro
|
|
|
40
60
|
rescue
|
|
41
61
|
end
|
|
42
62
|
|
|
63
|
+
exception = data[:exception_object]
|
|
64
|
+
has_error = !exception.nil?
|
|
65
|
+
|
|
43
66
|
# Skip out via sampling before we build any payload — jobs can be chatty
|
|
44
67
|
# enough that even the "cheap" stop/analyze work matters under load.
|
|
45
|
-
#
|
|
46
|
-
# always sends errors with force: true.
|
|
68
|
+
# Errors always ship regardless of sampling, matching Subscriber's web path.
|
|
47
69
|
job_type_key = "#{job_class_name}#perform"
|
|
48
|
-
unless DeadBro.configuration.should_sample?(job_type_key)
|
|
70
|
+
unless has_error || DeadBro.configuration.should_sample?(job_type_key)
|
|
49
71
|
drain_job_tracking
|
|
50
72
|
next
|
|
51
73
|
end
|
|
@@ -71,6 +93,7 @@ module DeadBro
|
|
|
71
93
|
|
|
72
94
|
# Get SQL queries executed during this job
|
|
73
95
|
sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
|
|
96
|
+
transaction_events = DeadBro::SqlSubscriber.last_transaction_events
|
|
74
97
|
dependency_events = job_dependency_payload
|
|
75
98
|
db_connection_stats = defined?(DeadBro::DbConnectionSubscriber) ? DeadBro::DbConnectionSubscriber.stop_request_tracking : {}
|
|
76
99
|
gc_pressure = defined?(DeadBro::GcTracker) ? DeadBro::GcTracker.stop_request_tracking : {}
|
|
@@ -117,8 +140,9 @@ module DeadBro
|
|
|
117
140
|
db_connection_checkouts: db_connection_stats[:checkouts],
|
|
118
141
|
gc_pressure: gc_pressure,
|
|
119
142
|
ar_instantiation_count: ar_instantiation_count,
|
|
120
|
-
status: "completed",
|
|
143
|
+
status: has_error ? "failed" : "completed",
|
|
121
144
|
sql_queries: sql_queries,
|
|
145
|
+
transaction_events: transaction_events,
|
|
122
146
|
rails_env: DeadBro.env,
|
|
123
147
|
host: DeadBro.safe_hostname,
|
|
124
148
|
process_kind: DeadBro.process_kind,
|
|
@@ -130,115 +154,19 @@ module DeadBro
|
|
|
130
154
|
logs: DeadBro.logger.logs
|
|
131
155
|
}.merge(dependency_events)
|
|
132
156
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
begin
|
|
141
|
-
if DeadBro.configuration.skip_tracking?
|
|
142
|
-
drain_job_tracking
|
|
143
|
-
next
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
job_class_name = data[:job].class.name
|
|
147
|
-
if DeadBro.configuration.excluded_job?(job_class_name)
|
|
148
|
-
next
|
|
149
|
-
end
|
|
150
|
-
# If exclusive_jobs is defined and not empty, only track matching jobs
|
|
151
|
-
unless DeadBro.configuration.exclusive_job?(job_class_name)
|
|
152
|
-
next
|
|
153
|
-
end
|
|
154
|
-
rescue
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
158
|
-
exception = data[:exception_object]
|
|
159
|
-
queue_duration_ms = job_queue_duration_ms(data[:job], started)
|
|
160
|
-
|
|
161
|
-
# Ensure tracking was started (fallback if perform_start.active_job didn't fire)
|
|
162
|
-
unless DeadBro::SqlSubscriber.tracking_active?
|
|
163
|
-
DeadBro.logger.clear
|
|
164
|
-
Thread.current[DeadBro::TRACKING_START_TIME_KEY] = Time.now
|
|
165
|
-
DeadBro::SqlSubscriber.start_request_tracking
|
|
166
|
-
start_job_dependency_tracking
|
|
167
|
-
DeadBro::DbConnectionSubscriber.start_request_tracking if defined?(DeadBro::DbConnectionSubscriber)
|
|
168
|
-
DeadBro::WatchTracker.start_request_tracking if defined?(DeadBro::WatchTracker)
|
|
169
|
-
if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
|
|
170
|
-
DeadBro::MemoryTrackingSubscriber.start_request_tracking
|
|
171
|
-
else
|
|
172
|
-
DeadBro::LightweightMemoryTracker.start_request_tracking if defined?(DeadBro::LightweightMemoryTracker)
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Get SQL queries executed during this job
|
|
177
|
-
sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
|
|
178
|
-
dependency_events = job_dependency_payload
|
|
179
|
-
db_connection_stats = defined?(DeadBro::DbConnectionSubscriber) ? DeadBro::DbConnectionSubscriber.stop_request_tracking : {}
|
|
180
|
-
gc_pressure = defined?(DeadBro::GcTracker) ? DeadBro::GcTracker.stop_request_tracking : {}
|
|
181
|
-
ar_instantiation_count = defined?(DeadBro::ArObjectTracker) ? DeadBro::ArObjectTracker.stop_request_tracking : nil
|
|
182
|
-
watch_events = defined?(DeadBro::WatchTracker) ? DeadBro::WatchTracker.stop_request_tracking : []
|
|
183
|
-
|
|
184
|
-
# Stop memory tracking and get collected memory data
|
|
185
|
-
if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
|
|
186
|
-
detailed_memory = DeadBro::MemoryTrackingSubscriber.stop_request_tracking
|
|
187
|
-
memory_performance = DeadBro::MemoryTrackingSubscriber.analyze_memory_performance(detailed_memory)
|
|
188
|
-
# Keep memory_events compact and user-friendly (no large raw arrays)
|
|
189
|
-
memory_events = {
|
|
190
|
-
memory_before: detailed_memory[:memory_before],
|
|
191
|
-
memory_after: detailed_memory[:memory_after],
|
|
192
|
-
duration_seconds: detailed_memory[:duration_seconds],
|
|
193
|
-
allocations_count: (detailed_memory[:allocations] || []).length,
|
|
194
|
-
memory_snapshots_count: (detailed_memory[:memory_snapshots] || []).length,
|
|
195
|
-
large_objects_count: (detailed_memory[:large_objects] || []).length
|
|
196
|
-
}
|
|
197
|
-
else
|
|
198
|
-
lightweight_memory = DeadBro::LightweightMemoryTracker.stop_request_tracking
|
|
199
|
-
# Separate raw readings from derived performance metrics to avoid duplicating data
|
|
200
|
-
memory_events = {
|
|
201
|
-
memory_before: lightweight_memory[:memory_before],
|
|
202
|
-
memory_after: lightweight_memory[:memory_after]
|
|
203
|
-
}
|
|
204
|
-
memory_performance = {
|
|
205
|
-
memory_growth_mb: lightweight_memory[:memory_growth_mb],
|
|
206
|
-
gc_count_increase: lightweight_memory[:gc_count_increase],
|
|
207
|
-
heap_pages_increase: lightweight_memory[:heap_pages_increase],
|
|
208
|
-
duration_seconds: lightweight_memory[:duration_seconds]
|
|
209
|
-
}
|
|
157
|
+
if has_error
|
|
158
|
+
payload[:exception_class] = exception.class.name
|
|
159
|
+
payload[:message] = exception.message.to_s[0, 1000]
|
|
160
|
+
payload[:backtrace] = Array(exception.backtrace).first(50)
|
|
161
|
+
payload[:fingerprint] = DeadBro::Subscriber.compute_error_fingerprint(exception)
|
|
162
|
+
payload[:cause_chain] = DeadBro::Subscriber.build_cause_chain(exception)
|
|
163
|
+
payload[:error] = true
|
|
210
164
|
end
|
|
211
165
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
arguments: safe_arguments(data[:job].arguments),
|
|
217
|
-
started_at: started.utc.iso8601(3),
|
|
218
|
-
duration_ms: duration_ms,
|
|
219
|
-
queue_duration_ms: queue_duration_ms,
|
|
220
|
-
db_connection_wait_ms: db_connection_stats[:wait_ms],
|
|
221
|
-
db_connection_checkouts: db_connection_stats[:checkouts],
|
|
222
|
-
gc_pressure: gc_pressure,
|
|
223
|
-
ar_instantiation_count: ar_instantiation_count,
|
|
224
|
-
status: "failed",
|
|
225
|
-
sql_queries: sql_queries,
|
|
226
|
-
exception_class: exception&.class&.name,
|
|
227
|
-
message: exception&.message&.to_s&.[](0, 1000),
|
|
228
|
-
backtrace: Array(exception&.backtrace).first(50),
|
|
229
|
-
rails_env: DeadBro.env,
|
|
230
|
-
host: DeadBro.safe_hostname,
|
|
231
|
-
process_kind: DeadBro.process_kind,
|
|
232
|
-
memory_usage: memory_usage_mb,
|
|
233
|
-
gc_stats: gc_stats,
|
|
234
|
-
memory_events: memory_events,
|
|
235
|
-
memory_performance: memory_performance,
|
|
236
|
-
watch_events: watch_events,
|
|
237
|
-
logs: DeadBro.logger.logs
|
|
238
|
-
}.merge(dependency_events)
|
|
239
|
-
|
|
240
|
-
event_name = exception&.class&.name || "ActiveJob::Exception"
|
|
241
|
-
client.post_metric(event_name: event_name, payload: payload, force: true)
|
|
166
|
+
# force: true — errors always ship, bypassing sampling by design; for
|
|
167
|
+
# completions the sampling decision above already accounted for any
|
|
168
|
+
# per-job-type override, so client#post_metric must not re-roll it globally.
|
|
169
|
+
client.post_metric(event_name: name, payload: payload, force: true)
|
|
242
170
|
end
|
|
243
171
|
rescue
|
|
244
172
|
# Never raise from instrumentation install
|
|
@@ -248,6 +176,7 @@ module DeadBro
|
|
|
248
176
|
# build a payload (excluded job / sampled out). Matches Subscriber.drain_request_tracking.
|
|
249
177
|
def self.drain_job_tracking
|
|
250
178
|
# wait_for_explains: false — result is discarded, don't block on pending plans.
|
|
179
|
+
# stop_request_tracking also pops the transaction-events stack (see its comment).
|
|
251
180
|
DeadBro::SqlSubscriber.stop_request_tracking(wait_for_explains: false) if defined?(DeadBro::SqlSubscriber)
|
|
252
181
|
Thread.current[:dead_bro_http_events] = nil
|
|
253
182
|
DeadBro::CacheSubscriber.stop_request_tracking if defined?(DeadBro::CacheSubscriber)
|
|
@@ -16,7 +16,11 @@ module DeadBro
|
|
|
16
16
|
THREAD_LOCAL_EXPLAIN_PENDING_KEY = :dead_bro_explain_pending
|
|
17
17
|
THREAD_LOCAL_CALL_COUNTS_KEY = :dead_bro_sql_call_counts
|
|
18
18
|
THREAD_LOCAL_AGGREGATES_KEY = :dead_bro_sql_aggregates
|
|
19
|
+
THREAD_LOCAL_TXN_EVENTS_KEY = :dead_bro_sql_txn_events
|
|
19
20
|
MAX_TRACKED_QUERIES = 1000
|
|
21
|
+
# Transaction control breadcrumbs (BEGIN/COMMIT/ROLLBACK/...) are cheap and rare
|
|
22
|
+
# compared to queries, but a pathological retry loop could still spam them.
|
|
23
|
+
MAX_TRACKED_TXN_EVENTS = 200
|
|
20
24
|
|
|
21
25
|
# Number of identical queries within one request that triggers N+1 detection.
|
|
22
26
|
N_PLUS_ONE_THRESHOLD = 5
|
|
@@ -37,6 +41,43 @@ module DeadBro
|
|
|
37
41
|
SANITIZE_SKIP_SENSITIVE_WHEN_NO_KEYWORDS = /password|token|secret|key|ssn|credit_card/i
|
|
38
42
|
SANITIZE_SKIP_WHERE_WHEN_NO_KEYWORD = /WHERE/i
|
|
39
43
|
|
|
44
|
+
# Rails' own adapters (MySQL, PostgreSQL, SQLite3, at least 7.1 through 8.1 —
|
|
45
|
+
# the versions checked directly in this repo) log every transaction-control
|
|
46
|
+
# statement under the single generic name "TRANSACTION"
|
|
47
|
+
# (`internal_execute("BEGIN", "TRANSACTION", ...)`, `internal_execute("COMMIT",
|
|
48
|
+
# "TRANSACTION", ...)`, etc. — see AbstractMysqlAdapter/PostgreSQL::DatabaseStatements/
|
|
49
|
+
# SQLite3::DatabaseStatements). "BEGIN"/"COMMIT"/"ROLLBACK"/"SAVEPOINT"/"RELEASE"
|
|
50
|
+
# as literal `name` values are kept here only as a defensive fallback for
|
|
51
|
+
# adapters/older Rails versions that might still emit them directly — the actual
|
|
52
|
+
# operation for a "TRANSACTION"-named event is derived from the SQL text itself
|
|
53
|
+
# (see transaction_operation_for below), since the name alone can't distinguish
|
|
54
|
+
# BEGIN from COMMIT from a SAVEPOINT.
|
|
55
|
+
TXN_CONTROL_NAMES = %w[BEGIN COMMIT ROLLBACK SAVEPOINT RELEASE TRANSACTION].freeze
|
|
56
|
+
|
|
57
|
+
TXN_OPERATION_FROM_SQL = [
|
|
58
|
+
[/\A\s*ROLLBACK\s+TO\s+SAVEPOINT/i, "ROLLBACK TO SAVEPOINT"],
|
|
59
|
+
[/\A\s*RELEASE\s+SAVEPOINT/i, "RELEASE SAVEPOINT"],
|
|
60
|
+
[/\A\s*SAVEPOINT/i, "SAVEPOINT"],
|
|
61
|
+
[/\A\s*BEGIN/i, "BEGIN"],
|
|
62
|
+
[/\A\s*COMMIT/i, "COMMIT"],
|
|
63
|
+
[/\A\s*ROLLBACK/i, "ROLLBACK"]
|
|
64
|
+
].freeze
|
|
65
|
+
|
|
66
|
+
# data[:name] is only ever "TRANSACTION" in current Rails — this derives the
|
|
67
|
+
# actual verb (BEGIN/COMMIT/ROLLBACK/SAVEPOINT/RELEASE SAVEPOINT/ROLLBACK TO
|
|
68
|
+
# SAVEPOINT) from the SQL text so breadcrumbs are still meaningful. A literal
|
|
69
|
+
# non-"TRANSACTION" name (older Rails/adapter) is passed through unchanged.
|
|
70
|
+
def self.transaction_operation_for(name, sql)
|
|
71
|
+
return name unless name == "TRANSACTION"
|
|
72
|
+
sql_str = sql.to_s
|
|
73
|
+
TXN_OPERATION_FROM_SQL.each do |re, op|
|
|
74
|
+
return op if sql_str.match?(re)
|
|
75
|
+
end
|
|
76
|
+
name
|
|
77
|
+
rescue
|
|
78
|
+
name
|
|
79
|
+
end
|
|
80
|
+
|
|
40
81
|
# True when there is at least one active tracking context (e.g. for nested jobs).
|
|
41
82
|
def self.tracking_active?
|
|
42
83
|
stack = Thread.current[THREAD_LOCAL_KEY]
|
|
@@ -50,6 +91,13 @@ module DeadBro
|
|
|
50
91
|
stack.last
|
|
51
92
|
end
|
|
52
93
|
|
|
94
|
+
# Current transaction-events array (top of stack); nil if no active tracking.
|
|
95
|
+
def self.current_txn_events_array
|
|
96
|
+
stack = Thread.current[THREAD_LOCAL_TXN_EVENTS_KEY]
|
|
97
|
+
return nil unless stack.is_a?(Array) && stack.any?
|
|
98
|
+
stack.last
|
|
99
|
+
end
|
|
100
|
+
|
|
53
101
|
# Sum of SQL counts and durations recorded so far in the active tracking
|
|
54
102
|
# context. Used by WatchTracker to attribute SQL to a DeadBro.watch block.
|
|
55
103
|
def self.current_sql_metrics
|
|
@@ -96,6 +144,27 @@ module DeadBro
|
|
|
96
144
|
sql.to_s.downcase
|
|
97
145
|
end
|
|
98
146
|
|
|
147
|
+
# Records a lightweight breadcrumb for a transaction-control statement
|
|
148
|
+
# (BEGIN/COMMIT/ROLLBACK/SAVEPOINT/RELEASE) — enough to show, e.g., that the
|
|
149
|
+
# queries preceding an error ran inside a transaction that was rolled back,
|
|
150
|
+
# without the cost of treating it like a tracked query (no backtrace/EXPLAIN).
|
|
151
|
+
def self.record_transaction_event(name, started, finished)
|
|
152
|
+
current = current_txn_events_array
|
|
153
|
+
return unless current
|
|
154
|
+
return if current.length >= MAX_TRACKED_TXN_EVENTS
|
|
155
|
+
|
|
156
|
+
tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
|
|
157
|
+
start_offset_ms = tracking_start ? ((started - tracking_start) * 1000.0).round(2) : nil
|
|
158
|
+
|
|
159
|
+
current << {
|
|
160
|
+
event: name,
|
|
161
|
+
duration_ms: ((finished - started) * 1000.0).round(2),
|
|
162
|
+
start_offset_ms: start_offset_ms
|
|
163
|
+
}
|
|
164
|
+
rescue
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
99
168
|
def self.subscribe!
|
|
100
169
|
# Subscribe with a start/finish listener to measure allocations per query
|
|
101
170
|
if ActiveSupport::Notifications.notifier.respond_to?(:subscribe)
|
|
@@ -106,7 +175,13 @@ module DeadBro
|
|
|
106
175
|
end
|
|
107
176
|
|
|
108
177
|
ActiveSupport::Notifications.subscribe(SQL_EVENT_NAME) do |name, started, finished, _unique_id, data|
|
|
109
|
-
next if data[:name] == "SCHEMA" || data[:name] == "CACHE"
|
|
178
|
+
next if data[:name] == "SCHEMA" || data[:name] == "CACHE"
|
|
179
|
+
|
|
180
|
+
if TXN_CONTROL_NAMES.include?(data[:name])
|
|
181
|
+
record_transaction_event(transaction_operation_for(data[:name], data[:sql]), started, finished)
|
|
182
|
+
next
|
|
183
|
+
end
|
|
184
|
+
|
|
110
185
|
# Only track queries that are part of the current request (top of stack for nested jobs)
|
|
111
186
|
current = current_queries_array
|
|
112
187
|
next unless current
|
|
@@ -208,6 +283,7 @@ module DeadBro
|
|
|
208
283
|
Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY] = []
|
|
209
284
|
(Thread.current[THREAD_LOCAL_CALL_COUNTS_KEY] ||= []) << {}
|
|
210
285
|
(Thread.current[THREAD_LOCAL_AGGREGATES_KEY] ||= []) << {}
|
|
286
|
+
(Thread.current[THREAD_LOCAL_TXN_EVENTS_KEY] ||= []) << []
|
|
211
287
|
end
|
|
212
288
|
|
|
213
289
|
# wait_for_explains: false on drain paths (excluded / sampled-out requests)
|
|
@@ -223,6 +299,21 @@ module DeadBro
|
|
|
223
299
|
cc_stack = Thread.current[THREAD_LOCAL_CALL_COUNTS_KEY]
|
|
224
300
|
cc_stack.pop if cc_stack.is_a?(Array) && cc_stack.any?
|
|
225
301
|
|
|
302
|
+
# Transaction-control breadcrumbs are popped here, as part of this method's
|
|
303
|
+
# existing lifecycle, rather than through a separate stop_transaction_tracking
|
|
304
|
+
# call. start_request_tracking has many callers across this gem (and its own
|
|
305
|
+
# specs) that only know about query tracking; requiring every one of them to
|
|
306
|
+
# also remember a second, separately-paired stop call is exactly how a frame
|
|
307
|
+
# gets pushed and never popped — leaking across every later request/job on
|
|
308
|
+
# the same thread. Piggybacking on stop_request_tracking, which is already
|
|
309
|
+
# reliably paired with every start_request_tracking call, means there is
|
|
310
|
+
# nothing new for any caller to remember. A caller that wants the events
|
|
311
|
+
# reads last_transaction_events immediately afterward.
|
|
312
|
+
txn_stack = Thread.current[THREAD_LOCAL_TXN_EVENTS_KEY]
|
|
313
|
+
Thread.current[:dead_bro_last_transaction_events] =
|
|
314
|
+
(txn_stack.is_a?(Array) && txn_stack.any?) ? txn_stack.pop : []
|
|
315
|
+
Thread.current[THREAD_LOCAL_TXN_EVENTS_KEY] = nil if txn_stack.nil? || txn_stack.empty?
|
|
316
|
+
|
|
226
317
|
# Fold any completed EXPLAIN plans from raw queries into their aggregate entry
|
|
227
318
|
raw_queries.each do |q|
|
|
228
319
|
next unless q[:explain_plan]
|
|
@@ -244,6 +335,17 @@ module DeadBro
|
|
|
244
335
|
aggregates_h.values.sort_by { |a| -a[:total_duration_ms] }
|
|
245
336
|
end
|
|
246
337
|
|
|
338
|
+
# The transaction-control breadcrumbs captured during the tracking window that
|
|
339
|
+
# this thread's most recent stop_request_tracking call just ended. Consumes
|
|
340
|
+
# (clears) the thread-local on read, not just on write: otherwise it would sit
|
|
341
|
+
# pinned in Thread.current between requests (a Puma/Sidekiq thread is reused),
|
|
342
|
+
# and — worse — any caller that reads it without an immediately-preceding
|
|
343
|
+
# stop_request_tracking would silently get the *previous* request's
|
|
344
|
+
# breadcrumbs instead of an empty result.
|
|
345
|
+
def self.last_transaction_events
|
|
346
|
+
Thread.current[:dead_bro_last_transaction_events].tap { Thread.current[:dead_bro_last_transaction_events] = nil } || []
|
|
347
|
+
end
|
|
348
|
+
|
|
247
349
|
# Upper bound on pending EXPLAIN threads per request. Each thread checks
|
|
248
350
|
# out an AR pool connection, so this must stay well below common pool
|
|
249
351
|
# sizes (Rails default is 5) to avoid starving the app under a storm.
|
|
@@ -94,6 +94,7 @@ module DeadBro
|
|
|
94
94
|
if defined?(DeadBro::SqlSubscriber)
|
|
95
95
|
Thread.current[:dead_bro_sql_queries]
|
|
96
96
|
Thread.current[:dead_bro_sql_queries] = nil
|
|
97
|
+
Thread.current[DeadBro::SqlSubscriber::THREAD_LOCAL_TXN_EVENTS_KEY] = nil
|
|
97
98
|
end
|
|
98
99
|
|
|
99
100
|
if defined?(DeadBro::CacheSubscriber)
|
data/lib/dead_bro/subscriber.rb
CHANGED
|
@@ -58,6 +58,7 @@ module DeadBro
|
|
|
58
58
|
|
|
59
59
|
# Stop SQL tracking and get collected queries (this was started by the request)
|
|
60
60
|
sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
|
|
61
|
+
transaction_events = DeadBro::SqlSubscriber.last_transaction_events
|
|
61
62
|
|
|
62
63
|
# Stop cache, redis, and elasticsearch tracking
|
|
63
64
|
cache_events = defined?(DeadBro::CacheSubscriber) ? DeadBro::CacheSubscriber.stop_request_tracking : []
|
|
@@ -147,6 +148,42 @@ module DeadBro
|
|
|
147
148
|
})
|
|
148
149
|
end
|
|
149
150
|
|
|
151
|
+
# Everything captured during the request regardless of outcome — shared between
|
|
152
|
+
# the error and success payloads below so an errored request (e.g. a lock wait
|
|
153
|
+
# timeout raised mid-transaction) still ships the SQL/cache/memory/etc data that
|
|
154
|
+
# led up to it, not just the exception. This used to be built only for the
|
|
155
|
+
# success path, so every errored request shipped with sql_count: 0 and no trace
|
|
156
|
+
# even though the gem had already captured it.
|
|
157
|
+
detail_fields = {
|
|
158
|
+
view_runtime_ms: data[:view_runtime],
|
|
159
|
+
db_runtime_ms: data[:db_runtime],
|
|
160
|
+
memory_usage: memory_usage_mb,
|
|
161
|
+
gc_stats: gc_stats,
|
|
162
|
+
sql_count: sql_count(data),
|
|
163
|
+
sql_queries: sql_queries,
|
|
164
|
+
transaction_events: transaction_events,
|
|
165
|
+
http_outgoing: Thread.current[:dead_bro_http_events] || [],
|
|
166
|
+
cache_events: cache_events,
|
|
167
|
+
redis_events: redis_events,
|
|
168
|
+
elasticsearch_events: elasticsearch_events,
|
|
169
|
+
cache_hits: cache_hits(data),
|
|
170
|
+
cache_misses: cache_misses(data),
|
|
171
|
+
view_events: view_events,
|
|
172
|
+
view_performance: view_performance,
|
|
173
|
+
memory_events: memory_events,
|
|
174
|
+
memory_performance: memory_performance,
|
|
175
|
+
allocation_phases: allocation_phases,
|
|
176
|
+
rack_duration_ms: rack_duration_ms,
|
|
177
|
+
queue_duration_ms: Thread.current[:dead_bro_queue_duration_ms],
|
|
178
|
+
db_connection_wait_ms: db_connection_stats[:wait_ms],
|
|
179
|
+
db_connection_checkouts: db_connection_stats[:checkouts],
|
|
180
|
+
gc_pressure: gc_pressure,
|
|
181
|
+
ar_instantiation_count: ar_instantiation_count,
|
|
182
|
+
cpu_time_ms: cpu_time_ms,
|
|
183
|
+
watch_events: watch_events,
|
|
184
|
+
logs: DeadBro.logger.logs
|
|
185
|
+
}
|
|
186
|
+
|
|
150
187
|
# Report exceptions attached to this action (e.g. controller/view errors)
|
|
151
188
|
if data[:exception] || data[:exception_object]
|
|
152
189
|
begin
|
|
@@ -154,7 +191,7 @@ module DeadBro
|
|
|
154
191
|
exception_obj = data[:exception_object]
|
|
155
192
|
backtrace = Array(exception_obj&.backtrace).first(50)
|
|
156
193
|
|
|
157
|
-
error_payload =
|
|
194
|
+
error_payload = detail_fields.merge(
|
|
158
195
|
controller: data[:controller],
|
|
159
196
|
action: data[:action],
|
|
160
197
|
format: data[:format],
|
|
@@ -164,6 +201,7 @@ module DeadBro
|
|
|
164
201
|
duration_ms: duration_ms,
|
|
165
202
|
rails_env: DeadBro.env,
|
|
166
203
|
host: DeadBro.safe_hostname,
|
|
204
|
+
request_host: safe_request_host(data),
|
|
167
205
|
process_kind: DeadBro.process_kind,
|
|
168
206
|
params: safe_params(data),
|
|
169
207
|
user_agent: safe_user_agent(data),
|
|
@@ -173,9 +211,8 @@ module DeadBro
|
|
|
173
211
|
backtrace: backtrace,
|
|
174
212
|
fingerprint: compute_error_fingerprint(exception_obj),
|
|
175
213
|
cause_chain: build_cause_chain(exception_obj),
|
|
176
|
-
error: true
|
|
177
|
-
|
|
178
|
-
}
|
|
214
|
+
error: true
|
|
215
|
+
)
|
|
179
216
|
|
|
180
217
|
event_name = (exception_class || exception_obj&.class&.name || "exception").to_s
|
|
181
218
|
client.post_metric(event_name: event_name, payload: error_payload, force: true)
|
|
@@ -185,7 +222,7 @@ module DeadBro
|
|
|
185
222
|
end
|
|
186
223
|
end
|
|
187
224
|
|
|
188
|
-
payload =
|
|
225
|
+
payload = detail_fields.merge(
|
|
189
226
|
controller: data[:controller],
|
|
190
227
|
action: data[:action],
|
|
191
228
|
format: data[:format],
|
|
@@ -194,39 +231,14 @@ module DeadBro
|
|
|
194
231
|
status: data[:status],
|
|
195
232
|
started_at: started.utc.iso8601(3),
|
|
196
233
|
duration_ms: duration_ms,
|
|
197
|
-
view_runtime_ms: data[:view_runtime],
|
|
198
|
-
db_runtime_ms: data[:db_runtime],
|
|
199
234
|
host: DeadBro.safe_hostname,
|
|
235
|
+
request_host: safe_request_host(data),
|
|
200
236
|
rails_env: DeadBro.env,
|
|
201
237
|
process_kind: DeadBro.process_kind,
|
|
202
238
|
params: safe_params(data),
|
|
203
239
|
user_agent: safe_user_agent(data),
|
|
204
|
-
user_id: extract_user_id(data)
|
|
205
|
-
|
|
206
|
-
gc_stats: gc_stats,
|
|
207
|
-
sql_count: sql_count(data),
|
|
208
|
-
sql_queries: sql_queries,
|
|
209
|
-
http_outgoing: Thread.current[:dead_bro_http_events] || [],
|
|
210
|
-
cache_events: cache_events,
|
|
211
|
-
redis_events: redis_events,
|
|
212
|
-
elasticsearch_events: elasticsearch_events,
|
|
213
|
-
cache_hits: cache_hits(data),
|
|
214
|
-
cache_misses: cache_misses(data),
|
|
215
|
-
view_events: view_events,
|
|
216
|
-
view_performance: view_performance,
|
|
217
|
-
memory_events: memory_events,
|
|
218
|
-
memory_performance: memory_performance,
|
|
219
|
-
allocation_phases: allocation_phases,
|
|
220
|
-
rack_duration_ms: rack_duration_ms,
|
|
221
|
-
queue_duration_ms: Thread.current[:dead_bro_queue_duration_ms],
|
|
222
|
-
db_connection_wait_ms: db_connection_stats[:wait_ms],
|
|
223
|
-
db_connection_checkouts: db_connection_stats[:checkouts],
|
|
224
|
-
gc_pressure: gc_pressure,
|
|
225
|
-
ar_instantiation_count: ar_instantiation_count,
|
|
226
|
-
cpu_time_ms: cpu_time_ms,
|
|
227
|
-
watch_events: watch_events,
|
|
228
|
-
logs: DeadBro.logger.logs
|
|
229
|
-
}
|
|
240
|
+
user_id: extract_user_id(data)
|
|
241
|
+
)
|
|
230
242
|
# force: true — the sampling decision (global or per-request-type) was
|
|
231
243
|
# already made above; client#post_metric must not re-roll it with the
|
|
232
244
|
# global-only rate, which would silently override a per-type sample rate.
|
|
@@ -240,6 +252,7 @@ module DeadBro
|
|
|
240
252
|
def self.drain_request_tracking
|
|
241
253
|
# wait_for_explains: false — the result is discarded, so don't block this
|
|
242
254
|
# thread waiting on pending EXPLAIN plans.
|
|
255
|
+
# stop_request_tracking also pops the transaction-events stack (see its comment).
|
|
243
256
|
DeadBro::SqlSubscriber.stop_request_tracking(wait_for_explains: false) if defined?(DeadBro::SqlSubscriber)
|
|
244
257
|
DeadBro::CacheSubscriber.stop_request_tracking if defined?(DeadBro::CacheSubscriber)
|
|
245
258
|
DeadBro::RedisSubscriber.stop_request_tracking if defined?(DeadBro::RedisSubscriber)
|
|
@@ -330,6 +343,37 @@ module DeadBro
|
|
|
330
343
|
end
|
|
331
344
|
end
|
|
332
345
|
|
|
346
|
+
# The domain the request was served on (request.host) — distinct from `host`,
|
|
347
|
+
# which is the machine's OS hostname. Lets one app that answers on several
|
|
348
|
+
# domains be sliced by domain in the dashboard. Port and userinfo are dropped;
|
|
349
|
+
# value is lowercased so "Example.com" and "example.com" aren't two rows.
|
|
350
|
+
def self.safe_request_host(data)
|
|
351
|
+
raw =
|
|
352
|
+
if data[:request] && data[:request].respond_to?(:host)
|
|
353
|
+
data[:request].host
|
|
354
|
+
elsif data[:headers]
|
|
355
|
+
headers = data[:headers]
|
|
356
|
+
if headers.respond_to?(:[])
|
|
357
|
+
headers["HTTP_HOST"] || headers["Host"] || headers["host"]
|
|
358
|
+
elsif headers.respond_to?(:env)
|
|
359
|
+
headers.env && headers.env["HTTP_HOST"]
|
|
360
|
+
end
|
|
361
|
+
elsif data[:env].is_a?(Hash)
|
|
362
|
+
data[:env]["HTTP_HOST"]
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
host = sanitize_string(raw)
|
|
366
|
+
return "" if host.empty?
|
|
367
|
+
|
|
368
|
+
# Strip any userinfo@ and a trailing :port so only the hostname remains.
|
|
369
|
+
# Only a numeric trailing port is removed, so IPv6 literals ([::1]) survive.
|
|
370
|
+
host = host.split("@").last.to_s
|
|
371
|
+
host = host.sub(/:\d+\z/, "")
|
|
372
|
+
host.downcase[0, 255]
|
|
373
|
+
rescue
|
|
374
|
+
""
|
|
375
|
+
end
|
|
376
|
+
|
|
333
377
|
def self.safe_user_agent(data)
|
|
334
378
|
begin
|
|
335
379
|
# Prefer request object if available
|
data/lib/dead_bro/version.rb
CHANGED