dead_bro 0.2.14 → 0.2.16
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 +14 -0
- data/lib/dead_bro/ar_object_tracker.rb +31 -0
- data/lib/dead_bro/gc_tracker.rb +47 -0
- data/lib/dead_bro/job_subscriber.rb +17 -0
- data/lib/dead_bro/railtie.rb +4 -0
- data/lib/dead_bro/sql_tracking_middleware.rb +9 -0
- data/lib/dead_bro/subscriber.rb +10 -0
- data/lib/dead_bro/version.rb +1 -1
- data/lib/dead_bro.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0bfea7b6292743b5e6a0f7226c1bd8877ebfbe1131f95d5c1679f07377768ed2
|
|
4
|
+
data.tar.gz: '08ab07422afbd9d2d340e5195f47ceda9a184faa9a870e5464d6169dfc86ee6b'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76822c848e35d76f6cb54d7953e9ad7bb68de09b706be6f7586c4c67a3e430074fcd33515394965aeeb1d41c74577295ce56942badb05d96fd390e1daefeb410
|
|
7
|
+
data.tar.gz: 2cd76061aa3f443723dfc3e934b1cd36e7e7576257f6637a4a1deab4e05c3154d309c9d4b59d084b46e89521774e47475a84fdc711db42d03b1dab496682d532
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.16] - 2026-05-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `ArObjectTracker`: subscribes to Rails' built-in `instantiation.active_record` notification to count the total number of ActiveRecord model instances hydrated during each request or background job. The count is reported as `ar_instantiation_count` in every payload. Uses a thread-local counter with an idempotent `subscribe!` guard, matching the same start/stop lifecycle as `GcTracker`. No monkey-patching required — Rails emits this event natively with a `record_count` field that accumulates correctly across batch loads.
|
|
7
|
+
|
|
8
|
+
## [0.2.14] - 2026-05-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `DbConnectionSubscriber`: measures the time threads spend waiting to acquire a connection from the ActiveRecord connection pool and counts checkouts per request. Uses `prepend` on `ActiveRecord::ConnectionAdapters::ConnectionPool#checkout` so the overhead is minimal and the instrumentation is invisible to application code. `db_connection_wait_ms` and `db_connection_checkouts` are included in both request and job payloads.
|
|
12
|
+
- Queue duration tracking for web requests via `X-Request-Start` / `X-Queue-Start` headers. Parses both the Heroku microsecond format (`t=<µs>`) and the nginx seconds format (`t=<s.ms>`), and applies a 60 s clock-skew cap so a misconfigured proxy timestamp cannot produce a nonsensical value.
|
|
13
|
+
- Queue duration tracking for background jobs: time from `job.enqueued_at` to when `perform` begins is reported as `queue_duration_ms` in every job payload.
|
|
14
|
+
- `RedisSubscriber`: prepend-based instrumentation on `Redis::Client` that records every individual command, pipeline, and `MULTI`/`EXEC` block with command name, key, duration in ms, and database index. Falls back to an `ActiveSupport::Notifications` subscription for `redis.*` events emitted by other libraries. Capped at 1 000 events per request to bound memory growth.
|
|
15
|
+
- DB connection tracking wired into background jobs: `DbConnectionSubscriber` is started and stopped around job execution in both the normal completion path and the exception handler fallback path, with cleanup in `drain_job_tracking`.
|
|
16
|
+
|
|
3
17
|
## [0.1.0] - 2025-08-28
|
|
4
18
|
|
|
5
19
|
- Initial release
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
|
|
5
|
+
module DeadBro
|
|
6
|
+
module ArObjectTracker
|
|
7
|
+
THREAD_KEY = :dead_bro_ar_objects
|
|
8
|
+
|
|
9
|
+
def self.subscribe!
|
|
10
|
+
return if @subscribed
|
|
11
|
+
@subscribed = true
|
|
12
|
+
ActiveSupport::Notifications.subscribe("instantiation.active_record") do |_name, _started, _finished, _id, data|
|
|
13
|
+
count = Thread.current[THREAD_KEY]
|
|
14
|
+
next unless count
|
|
15
|
+
Thread.current[THREAD_KEY] = count + (data[:record_count] || 1).to_i
|
|
16
|
+
end
|
|
17
|
+
rescue StandardError
|
|
18
|
+
# Never raise from instrumentation install
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.start_request_tracking
|
|
22
|
+
Thread.current[THREAD_KEY] = 0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.stop_request_tracking
|
|
26
|
+
Thread.current[THREAD_KEY]
|
|
27
|
+
ensure
|
|
28
|
+
Thread.current[THREAD_KEY] = nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DeadBro
|
|
4
|
+
module GcTracker
|
|
5
|
+
THREAD_KEY = :dead_bro_gc_start
|
|
6
|
+
|
|
7
|
+
def self.start_request_tracking
|
|
8
|
+
Thread.current[THREAD_KEY] = snapshot
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.stop_request_tracking
|
|
12
|
+
before = Thread.current[THREAD_KEY]
|
|
13
|
+
return {} if before.nil? || before.empty?
|
|
14
|
+
diff(before, snapshot)
|
|
15
|
+
ensure
|
|
16
|
+
Thread.current[THREAD_KEY] = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.snapshot
|
|
20
|
+
return {} unless defined?(GC) && GC.respond_to?(:stat)
|
|
21
|
+
stat = GC.stat
|
|
22
|
+
{
|
|
23
|
+
minor_gc_count: stat[:minor_gc_count] || 0,
|
|
24
|
+
major_gc_count: stat[:major_gc_count] || 0,
|
|
25
|
+
total_allocated_objects: stat[:total_allocated_objects] || 0,
|
|
26
|
+
gc_time_ns: GC.respond_to?(:total_time) ? GC.total_time : nil
|
|
27
|
+
}
|
|
28
|
+
rescue
|
|
29
|
+
{}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.diff(before, after)
|
|
33
|
+
return {} if before.empty? || after.empty?
|
|
34
|
+
gc_time_ms = if before[:gc_time_ns] && after[:gc_time_ns]
|
|
35
|
+
((after[:gc_time_ns] - before[:gc_time_ns]) / 1_000_000.0).round(3)
|
|
36
|
+
end
|
|
37
|
+
{
|
|
38
|
+
minor_gc_runs: (after[:minor_gc_count] || 0) - (before[:minor_gc_count] || 0),
|
|
39
|
+
major_gc_runs: (after[:major_gc_count] || 0) - (before[:major_gc_count] || 0),
|
|
40
|
+
allocated_objects: (after[:total_allocated_objects] || 0) - (before[:total_allocated_objects] || 0),
|
|
41
|
+
gc_time_ms: gc_time_ms
|
|
42
|
+
}
|
|
43
|
+
rescue
|
|
44
|
+
{}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -12,6 +12,13 @@ module DeadBro
|
|
|
12
12
|
JOB_EXCEPTION_EVENT_NAME = "exception.active_job"
|
|
13
13
|
|
|
14
14
|
def self.subscribe!(client: Client.new)
|
|
15
|
+
# Snap GC state before the job runs so stop_request_tracking gets a valid diff
|
|
16
|
+
ActiveSupport::Notifications.subscribe("perform_start.active_job") do |_name, _started, _finished, _unique_id, _data|
|
|
17
|
+
DeadBro::GcTracker.start_request_tracking if defined?(DeadBro::GcTracker)
|
|
18
|
+
DeadBro::ArObjectTracker.start_request_tracking if defined?(DeadBro::ArObjectTracker)
|
|
19
|
+
rescue
|
|
20
|
+
end
|
|
21
|
+
|
|
15
22
|
# Track job execution
|
|
16
23
|
ActiveSupport::Notifications.subscribe(JOB_EVENT_NAME) do |name, started, finished, _unique_id, data|
|
|
17
24
|
begin
|
|
@@ -62,6 +69,8 @@ module DeadBro
|
|
|
62
69
|
# Get SQL queries executed during this job
|
|
63
70
|
sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
|
|
64
71
|
db_connection_stats = defined?(DeadBro::DbConnectionSubscriber) ? DeadBro::DbConnectionSubscriber.stop_request_tracking : {}
|
|
72
|
+
gc_pressure = defined?(DeadBro::GcTracker) ? DeadBro::GcTracker.stop_request_tracking : {}
|
|
73
|
+
ar_instantiation_count = defined?(DeadBro::ArObjectTracker) ? DeadBro::ArObjectTracker.stop_request_tracking : nil
|
|
65
74
|
|
|
66
75
|
# Stop memory tracking and get collected memory data
|
|
67
76
|
if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
|
|
@@ -100,6 +109,8 @@ module DeadBro
|
|
|
100
109
|
queue_duration_ms: queue_duration_ms,
|
|
101
110
|
db_connection_wait_ms: db_connection_stats[:wait_ms],
|
|
102
111
|
db_connection_checkouts: db_connection_stats[:checkouts],
|
|
112
|
+
gc_pressure: gc_pressure,
|
|
113
|
+
ar_instantiation_count: ar_instantiation_count,
|
|
103
114
|
status: "completed",
|
|
104
115
|
sql_queries: sql_queries,
|
|
105
116
|
rails_env: DeadBro.env,
|
|
@@ -153,6 +164,8 @@ module DeadBro
|
|
|
153
164
|
# Get SQL queries executed during this job
|
|
154
165
|
sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
|
|
155
166
|
db_connection_stats = defined?(DeadBro::DbConnectionSubscriber) ? DeadBro::DbConnectionSubscriber.stop_request_tracking : {}
|
|
167
|
+
gc_pressure = defined?(DeadBro::GcTracker) ? DeadBro::GcTracker.stop_request_tracking : {}
|
|
168
|
+
ar_instantiation_count = defined?(DeadBro::ArObjectTracker) ? DeadBro::ArObjectTracker.stop_request_tracking : nil
|
|
156
169
|
|
|
157
170
|
# Stop memory tracking and get collected memory data
|
|
158
171
|
if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
|
|
@@ -191,6 +204,8 @@ module DeadBro
|
|
|
191
204
|
queue_duration_ms: queue_duration_ms,
|
|
192
205
|
db_connection_wait_ms: db_connection_stats[:wait_ms],
|
|
193
206
|
db_connection_checkouts: db_connection_stats[:checkouts],
|
|
207
|
+
gc_pressure: gc_pressure,
|
|
208
|
+
ar_instantiation_count: ar_instantiation_count,
|
|
194
209
|
status: "failed",
|
|
195
210
|
sql_queries: sql_queries,
|
|
196
211
|
exception_class: exception&.class&.name,
|
|
@@ -217,6 +232,8 @@ module DeadBro
|
|
|
217
232
|
def self.drain_job_tracking
|
|
218
233
|
DeadBro::SqlSubscriber.stop_request_tracking if defined?(DeadBro::SqlSubscriber)
|
|
219
234
|
DeadBro::DbConnectionSubscriber.stop_request_tracking if defined?(DeadBro::DbConnectionSubscriber)
|
|
235
|
+
DeadBro::GcTracker.stop_request_tracking if defined?(DeadBro::GcTracker)
|
|
236
|
+
DeadBro::ArObjectTracker.stop_request_tracking if defined?(DeadBro::ArObjectTracker)
|
|
220
237
|
DeadBro::LightweightMemoryTracker.stop_request_tracking if defined?(DeadBro::LightweightMemoryTracker)
|
|
221
238
|
if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
|
|
222
239
|
DeadBro::MemoryTrackingSubscriber.stop_request_tracking
|
data/lib/dead_bro/railtie.rb
CHANGED
|
@@ -40,6 +40,10 @@ if defined?(Rails) && defined?(Rails::Railtie)
|
|
|
40
40
|
require "dead_bro/db_connection_subscriber"
|
|
41
41
|
DeadBro::DbConnectionSubscriber.install!
|
|
42
42
|
|
|
43
|
+
# Install ActiveRecord object instantiation tracking
|
|
44
|
+
require "dead_bro/ar_object_tracker"
|
|
45
|
+
DeadBro::ArObjectTracker.subscribe!
|
|
46
|
+
|
|
43
47
|
# Install view rendering tracking
|
|
44
48
|
require "dead_bro/view_rendering_subscriber"
|
|
45
49
|
DeadBro::ViewRenderingSubscriber.subscribe!(client: shared_client)
|
|
@@ -61,6 +61,12 @@ module DeadBro
|
|
|
61
61
|
DeadBro::DbConnectionSubscriber.start_request_tracking
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# Start GC pressure tracking — snapshot before any app code runs
|
|
65
|
+
DeadBro::GcTracker.start_request_tracking if defined?(DeadBro::GcTracker)
|
|
66
|
+
|
|
67
|
+
# Start AR object instantiation counting for this request
|
|
68
|
+
DeadBro::ArObjectTracker.start_request_tracking if defined?(DeadBro::ArObjectTracker)
|
|
69
|
+
|
|
64
70
|
# Start outgoing HTTP accumulation for this request
|
|
65
71
|
Thread.current[:dead_bro_http_events] = []
|
|
66
72
|
|
|
@@ -97,6 +103,9 @@ module DeadBro
|
|
|
97
103
|
Thread.current[:dead_bro_http_events] = nil
|
|
98
104
|
Thread.current[:dead_bro_queue_duration_ms] = nil
|
|
99
105
|
DeadBro::DbConnectionSubscriber.stop_request_tracking if defined?(DeadBro::DbConnectionSubscriber)
|
|
106
|
+
Thread.current[DeadBro::GcTracker::THREAD_KEY] = nil if defined?(DeadBro::GcTracker)
|
|
107
|
+
# Bypass stop_request_tracking intentionally — cleanup only, no return value needed here.
|
|
108
|
+
Thread.current[DeadBro::ArObjectTracker::THREAD_KEY] = nil if defined?(DeadBro::ArObjectTracker)
|
|
100
109
|
Thread.current[DeadBro::TRACKING_START_TIME_KEY] = nil
|
|
101
110
|
end
|
|
102
111
|
|
data/lib/dead_bro/subscriber.rb
CHANGED
|
@@ -65,6 +65,12 @@ module DeadBro
|
|
|
65
65
|
# Stop DB connection pool wait tracking
|
|
66
66
|
db_connection_stats = defined?(DeadBro::DbConnectionSubscriber) ? DeadBro::DbConnectionSubscriber.stop_request_tracking : {}
|
|
67
67
|
|
|
68
|
+
# Stop GC pressure tracking — diff minor/major runs, objects allocated, GC time
|
|
69
|
+
gc_pressure = defined?(DeadBro::GcTracker) ? DeadBro::GcTracker.stop_request_tracking : {}
|
|
70
|
+
|
|
71
|
+
# Stop AR object instantiation tracking
|
|
72
|
+
ar_instantiation_count = defined?(DeadBro::ArObjectTracker) ? DeadBro::ArObjectTracker.stop_request_tracking : nil
|
|
73
|
+
|
|
68
74
|
# Stop view rendering tracking and get collected view events
|
|
69
75
|
view_events = DeadBro::ViewRenderingSubscriber.stop_request_tracking
|
|
70
76
|
view_performance = DeadBro::ViewRenderingSubscriber.analyze_view_performance(view_events)
|
|
@@ -178,6 +184,8 @@ module DeadBro
|
|
|
178
184
|
queue_duration_ms: Thread.current[:dead_bro_queue_duration_ms],
|
|
179
185
|
db_connection_wait_ms: db_connection_stats[:wait_ms],
|
|
180
186
|
db_connection_checkouts: db_connection_stats[:checkouts],
|
|
187
|
+
gc_pressure: gc_pressure,
|
|
188
|
+
ar_instantiation_count: ar_instantiation_count,
|
|
181
189
|
logs: DeadBro.logger.logs
|
|
182
190
|
}
|
|
183
191
|
client.post_metric(event_name: name, payload: payload)
|
|
@@ -199,6 +207,8 @@ module DeadBro
|
|
|
199
207
|
end
|
|
200
208
|
Thread.current[:dead_bro_http_events] = nil
|
|
201
209
|
DeadBro::DbConnectionSubscriber.stop_request_tracking if defined?(DeadBro::DbConnectionSubscriber)
|
|
210
|
+
DeadBro::GcTracker.stop_request_tracking if defined?(DeadBro::GcTracker)
|
|
211
|
+
DeadBro::ArObjectTracker.stop_request_tracking if defined?(DeadBro::ArObjectTracker)
|
|
202
212
|
rescue
|
|
203
213
|
# Best effort — draining must never raise from the notifications callback.
|
|
204
214
|
end
|
data/lib/dead_bro/version.rb
CHANGED
data/lib/dead_bro.rb
CHANGED
|
@@ -18,6 +18,8 @@ module DeadBro
|
|
|
18
18
|
autoload :MemoryTrackingSubscriber, "dead_bro/memory_tracking_subscriber"
|
|
19
19
|
autoload :MemoryLeakDetector, "dead_bro/memory_leak_detector"
|
|
20
20
|
autoload :LightweightMemoryTracker, "dead_bro/lightweight_memory_tracker"
|
|
21
|
+
autoload :GcTracker, "dead_bro/gc_tracker"
|
|
22
|
+
autoload :ArObjectTracker, "dead_bro/ar_object_tracker"
|
|
21
23
|
autoload :MemoryHelpers, "dead_bro/memory_helpers"
|
|
22
24
|
autoload :JobSubscriber, "dead_bro/job_subscriber"
|
|
23
25
|
autoload :JobSqlTrackingMiddleware, "dead_bro/job_sql_tracking_middleware"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dead_bro
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Emanuel Comsa
|
|
@@ -21,6 +21,7 @@ files:
|
|
|
21
21
|
- FEATURES.md
|
|
22
22
|
- README.md
|
|
23
23
|
- lib/dead_bro.rb
|
|
24
|
+
- lib/dead_bro/ar_object_tracker.rb
|
|
24
25
|
- lib/dead_bro/cache_subscriber.rb
|
|
25
26
|
- lib/dead_bro/circuit_breaker.rb
|
|
26
27
|
- lib/dead_bro/client.rb
|
|
@@ -37,6 +38,7 @@ files:
|
|
|
37
38
|
- lib/dead_bro/dispatcher.rb
|
|
38
39
|
- lib/dead_bro/elasticsearch_subscriber.rb
|
|
39
40
|
- lib/dead_bro/error_middleware.rb
|
|
41
|
+
- lib/dead_bro/gc_tracker.rb
|
|
40
42
|
- lib/dead_bro/http_instrumentation.rb
|
|
41
43
|
- lib/dead_bro/job_sql_tracking_middleware.rb
|
|
42
44
|
- lib/dead_bro/job_subscriber.rb
|