dead_bro 0.2.36 → 0.2.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f33da7799522248140cc65b3637ae2a1dede4295b3696370bd3aabea0d09c7b
4
- data.tar.gz: 376071ecd3eafe0e41695246074ccea3b3a2c479532d08eb24d46fa0d471c94e
3
+ metadata.gz: 4258f98fde5b0e222593ebfadee095b6402576d94a33a2b57ac26c71f0b75d47
4
+ data.tar.gz: 4a268f4eb24b882bf2dddd36a45db694f664319d3fa9650c3587eb59dd37f2af
5
5
  SHA512:
6
- metadata.gz: ba9539bb294999bcd1d3b1f4bed9bc7612d4b9f3e66fe92769de92a0b50d8991ffadf2daf6e418b3951fcda7b8e1ae9afa5b6980c4de7af49d982ccd07aff6f2
7
- data.tar.gz: 1e9abbd0ef17016ea6f639fa60d6d18edc0b481cdeec3d6495f52f5ae31e73a413db0bb7e7607b274a344d8ab1b33dda098dca608c617a024622aff3796fc8e0
6
+ metadata.gz: 94f6cccf6594f0c50467b61a5d2bf53d6b73e7e72e6f042c2eae3cea2bad12e674e8a2f525bb7880f51f3b6cc679e3f3951dfba224b07e9701b48669060ffd51
7
+ data.tar.gz: eae0e565ae2d1b43f20df7c3fd14311c7543f29b1680f0495c1fa1b4ca86a30c880b20dc94ba9cc147288dff41fa39eb7aae360dafb76139b751dccc380d02e4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.38] - 2026-10-26
4
+
5
+ ### Added
6
+ - **Nested Request Trace.** Each request now sends its span tree as `spans`: the Rack middleware chain, the router, the controller action, view renders (layouts, templates, partials and collections), `DeadBro.watch` blocks, the block a `Rails.cache.fetch` miss runs, and SQL queries, each with an `id` and the id of the span it ran inside (`parent_id`). Repeated calls on the same path share one span with a `count`: a partial rendered 50 times from one template is one span, and the query it runs each time is one span under it. A span's `total_ms` is its calls' summed time with the spans nested in it included, and `start_ms` / `end_ms` are its first call's start and its last call's end. Cache, Redis, HTTP, Elasticsearch and transaction events carry the `parent_id` of the span they ran in, the cache event of a `Rails.cache.fetch` block and `DeadBro.watch` events the `span_id` of their span, and `sql_queries` entries the `id` their SQL spans refer to. DeadBro shows the tree with each span's total time and self time (total minus the spans nested in it). Up to 500 spans are kept per request; `spans_dropped` counts the ones left out. Background jobs don't send `spans`. An action that uses `ActionController::Live` runs on a thread of its own; its spans are recorded there, without the middleware chain.
7
+ - **Middleware timing.** Each middleware of the app's Rails middleware stack and the router it ends in are timed as spans, so the trace shows the time each one spends before and after the app. The dashboard's middleware spans setting turns their recording off and on without a restart, and `config.middleware_spans_enabled = false` in an initializer leaves the middleware stack as Rails builds it, without the probes. Either way, the trace then starts at the controller action.
8
+
9
+ ### Changed
10
+ - A request's payload is sent once the outermost middleware has returned instead of when the controller action finishes, so that the middleware spans are complete. If that middleware never returns through DeadBro's probe, for example when a timeout outside the Rails middleware stack raises into the thread at that moment, the payload is sent when the thread's next request comes in, and the next request's spans start afresh.
11
+
12
+ ### Fixed
13
+ - **An HTTP request made through Faraday was recorded twice:** once by DeadBro's Faraday middleware and once by its `Net::HTTP` or Typhoeus hook underneath, so its time was counted twice. It is now recorded once, by the Faraday middleware, which also sends the request's start offset and exception class as the `Net::HTTP` hook does. Elasticsearch requests sent through Faraday are recorded once too. A request the Faraday middleware leaves out (one to localhost) is still left to the `Net::HTTP` or Typhoeus hook, as before.
14
+
15
+ ## [0.2.37] - 2026-10-26
16
+
17
+ ### Fixed
18
+ - **`sql_count` stopped at 100 on any page that ran more queries.** It was never a query count: the subscriber read `ActiveRecord::Base.connection.query_cache.size`, the number of distinct SELECTs in the query cache — an LRU capped at 100 entries since Rails 7.1. An N+1 page that ran 188 queries reported "SQL Count 100" and "100 calls/request". `sql_count` is now the sum of the per-statement aggregate counts the SQL subscriber already keeps (every query, not only the first `MAX_TRACKED_QUERIES`), with query-cache hits included the way Rails counts them in its `(N queries, M cached)` log line. The old lookup also leased a database connection on requests that ran no SQL; nothing in the subscriber touches `ActiveRecord::Base.connection` any more.
19
+ - Background jobs sent no `sql_count` at all, so the dashboard showed 0 database calls for every job; job payloads now carry the same count.
20
+
21
+ ### Added
22
+ - `sql_cached_count` on request and job payloads: how many of `sql_count` were answered by the query cache.
23
+
24
+ ## [0.2.36] - 2026-10-25
25
+
3
26
  ### Added
4
27
  - **`gc_pressure` now says whether the window's GC had finished.** `gc_state_at_end` is the GC's phase when a request or job window closes — `"none"`, `"marking"` or `"sweeping"`, from `GC.latest_gc_info(:state)` — sent with the other memory-tracking fields. Ruby marks and sweeps in steps between allocations, so a GC counted in the window is often still running when it closes, and until it finishes `heap_live_slots_growth` still counts garbage it hasn't freed. The dashboard uses it so a sample measured mid-GC isn't flagged as a possible leak. It's a single-key read; no hash is built.
5
28
 
data/README.md CHANGED
@@ -118,6 +118,20 @@ DeadBro automatically tracks view rendering performance for each request. This i
118
118
  - **Slow view detection**: Identification of the slowest rendering views
119
119
  - **Frequency analysis**: Most frequently rendered views
120
120
 
121
+ ## Request Trace (span tree)
122
+
123
+ Each web request carries its span tree as `spans`, which DeadBro shows as a nested Request Trace with each span's total and self time:
124
+
125
+ - **Spans**: every middleware of the Rails middleware stack and the router it ends in, the controller action, view renders (layouts, templates, partials, collections), `DeadBro.watch` blocks, the block a `Rails.cache.fetch` miss runs, and SQL queries. Each has an `id`, the `parent_id` of the span it ran inside (`nil` at the top) and a `kind` (`middleware`, `endpoint`, `action`, `view`, `watch`, `cache`, `sql`).
126
+ - **Repeated calls**: calls with the same name under the same parent share one span with a `count`. A partial rendered 50 times from one template is one span; the query it runs each time is one span under it. `Rails.cache.fetch` blocks whose keys differ only in their digits share one span, named by the key with each run of digits as `?`.
127
+ - **Times**: `total_ms` is the calls' summed duration, nested spans included; `start_ms` and `end_ms` are the first call's start and the last call's end, as offsets from the request's tracking start like every `start_offset_ms` (middleware that runs before DeadBro's own has negative offsets).
128
+ - **Other events**: cache, Redis, HTTP, Elasticsearch and transaction events stay in their own lists and carry the `parent_id` of the span they ran in. The cache event of a `Rails.cache.fetch` block and `DeadBro.watch` events carry the `span_id` of their span, and SQL spans refer to their `sql_queries` entry by `ref`.
129
+ - **Limits**: up to 500 spans per request; `spans_dropped` counts the ones left out, whose time stays in their parent's self time.
130
+
131
+ Middleware is timed by a probe Rails places around each middleware when it builds the middleware stack. The middleware stack returns after the controller action has finished, so a request's payload is sent once the outermost middleware has returned. An action that uses `ActionController::Live` runs on a thread of its own, and its spans are recorded there, without the middleware chain. Background jobs don't send `spans`.
132
+
133
+ The dashboard's **Middleware spans** setting (`middleware_spans_enabled`, on by default) turns their recording off and on without a restart. While it's off, the probes stay in place but record nothing: the trace starts at the controller action, and a request's payload is sent when the action finishes. To leave the middleware stack as Rails builds it, without the probes, set `config.middleware_spans_enabled = false` in `config/initializers/dead_bro.rb` (default `true`). It's read when Rails builds the middleware stack, after the app's initializers have run, so a change takes effect when the app restarts; while it's `false`, the dashboard setting can't turn the spans on.
134
+
121
135
  ## Memory Tracking & Leak Detection
122
136
 
123
137
  DeadBro automatically tracks memory usage and detects memory leaks with minimal performance impact. This includes:
@@ -69,7 +69,7 @@ module DeadBro
69
69
  def self.build_event(name, data, duration_ms, start_offset_ms = nil)
70
70
  return nil unless data.is_a?(Hash)
71
71
 
72
- {
72
+ event = {
73
73
  event: name,
74
74
  duration_ms: duration_ms,
75
75
  start_offset_ms: start_offset_ms,
@@ -80,6 +80,17 @@ module DeadBro
80
80
  namespace: safe_namespace(data[:namespace]),
81
81
  at: Time.now.utc.to_i
82
82
  }
83
+ # A Rails.cache.fetch block is a span of the request's tree: its event
84
+ # names that span, which ran in the span the event's parent_id gives.
85
+ node = DeadBro::SpanTracker.cache_node(data)
86
+ if node
87
+ event[:span_id] = node[:id]
88
+ event[:parent_id] = node[:parent_id] if node[:parent_id]
89
+ else
90
+ parent_id = DeadBro::SpanTracker.current_id
91
+ event[:parent_id] = parent_id if parent_id
92
+ end
93
+ event
83
94
  rescue
84
95
  nil
85
96
  end
@@ -19,6 +19,14 @@ module DeadBro
19
19
  alias_method :explain_analyze_enabled, :explain_enabled
20
20
  alias_method :explain_analyze_enabled=, :explain_enabled=
21
21
 
22
+ # Middleware spans. false in the app's initializer leaves the middleware
23
+ # stack as Rails builds it, without a MiddlewareProbe around each
24
+ # middleware and the router, so the span tree starts at the controller
25
+ # action; it's read when Rails builds the stack, after the initializers.
26
+ # With the probes in place, the dashboard's setting of the same name turns
27
+ # their recording on and off (see #middleware_spans_active?).
28
+ attr_accessor :middleware_spans_enabled
29
+
22
30
  # Remote-managed settings (overwritten by backend JSON `settings` on successful API responses)
23
31
  attr_accessor :memory_tracking_enabled, :allocation_tracking_enabled, :allocation_sample_rate,
24
32
  :sample_rate, :slow_query_threshold_ms, :watch_enabled,
@@ -73,7 +81,7 @@ module DeadBro
73
81
  explain_enabled slow_query_threshold_ms max_sql_queries_to_send max_logs_to_send
74
82
  watch_enabled excluded_controllers excluded_jobs exclusive_controllers exclusive_jobs
75
83
  monitor_enabled enable_db_stats enable_process_stats enable_system_stats
76
- sample_rates_by_type report_routing_errors
84
+ sample_rates_by_type report_routing_errors middleware_spans_enabled
77
85
  ].freeze
78
86
 
79
87
  def initialize
@@ -89,6 +97,10 @@ module DeadBro
89
97
  @explicit_deploy_revision = nil
90
98
  @disk_paths = ["/"]
91
99
  @interfaces_ignore = %w[lo lo0 docker0]
100
+ @middleware_spans_enabled = true
101
+ # The dashboard's switch for recording middleware spans; on until a
102
+ # settings response says otherwise (older backends never send it).
103
+ @remote_middleware_spans_enabled = true
92
104
 
93
105
  # Remote-managed defaults (used until backend sends real values)
94
106
  @sample_rate = 100
@@ -193,6 +205,10 @@ module DeadBro
193
205
  # must never be able to switch it on — only off. The local opt-in
194
206
  # (explain_enabled) stays untouched; see #explain_active?
195
207
  @remote_explain_enabled = !!value
208
+ when "middleware_spans_enabled"
209
+ # The probes are already in place (or not) by now; this only turns
210
+ # their recording on and off. The local value stays as it was.
211
+ @remote_middleware_spans_enabled = !!value
196
212
  when "enabled", "memory_tracking_enabled", "allocation_tracking_enabled", "watch_enabled",
197
213
  "monitor_enabled", "enable_db_stats", "enable_process_stats", "enable_system_stats",
198
214
  "report_routing_errors"
@@ -212,6 +228,13 @@ module DeadBro
212
228
  !!(@explain_enabled && @remote_explain_enabled)
213
229
  end
214
230
 
231
+ # Middleware spans are recorded when the app hasn't turned them off in its
232
+ # initializer (which also leaves the stack without probes) and the
233
+ # dashboard hasn't either.
234
+ def middleware_spans_active?
235
+ !!(@middleware_spans_enabled && @remote_middleware_spans_enabled)
236
+ end
237
+
215
238
  alias_method :explain_analyze_active?, :explain_active?
216
239
 
217
240
  def heartbeat_due?
@@ -130,7 +130,7 @@ module DeadBro
130
130
  end
131
131
 
132
132
  def build_event(method, path, status, duration_ms, start_offset_ms = nil)
133
- {
133
+ event = {
134
134
  method: method.to_s.upcase,
135
135
  path: sanitize_path(path),
136
136
  operation: extract_operation(method, path),
@@ -138,6 +138,9 @@ module DeadBro
138
138
  duration_ms: duration_ms,
139
139
  start_offset_ms: start_offset_ms
140
140
  }
141
+ parent_id = DeadBro::SpanTracker.current_id
142
+ event[:parent_id] = parent_id if parent_id
143
+ event
141
144
  end
142
145
  end
143
146
  end
@@ -8,6 +8,10 @@ module DeadBro
8
8
  EVENT_NAME = "outgoing.http"
9
9
  THREAD_LOCAL_KEY = :dead_bro_http_events
10
10
  MAX_TRACKED_EVENTS = 1000
11
+ # Set while DeadBro's Faraday middleware runs the request: the adapter's
12
+ # own Net::HTTP or Typhoeus call is that request, recorded once by the
13
+ # middleware.
14
+ IN_FARADAY_KEY = :dead_bro_http_in_faraday
11
15
 
12
16
  def self.install!(client: Client.new)
13
17
  install_net_http!(client)
@@ -50,7 +54,11 @@ module DeadBro
50
54
  tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
51
55
  start_offset_ms = tracking_start ? ((wall_start - tracking_start) * 1000.0).round(2) : nil
52
56
 
53
- if is_es_host
57
+ if Thread.current[DeadBro::HttpInstrumentation::IN_FARADAY_KEY]
58
+ # Faraday sent it through this adapter; DeadBro's Faraday
59
+ # middleware around the request records it.
60
+ nil
61
+ elsif is_es_host
54
62
  # Route to elasticsearch subscriber instead of http_outgoing
55
63
  if Thread.current[DeadBro::ElasticsearchSubscriber::THREAD_LOCAL_KEY]
56
64
  path = (uri && uri.path) || req.path
@@ -75,6 +83,8 @@ module DeadBro
75
83
  start_offset_ms: start_offset_ms,
76
84
  exception: error && error.class.name
77
85
  }
86
+ parent_id = DeadBro::SpanTracker.current_id
87
+ payload[:parent_id] = parent_id if parent_id
78
88
  if Thread.current[THREAD_LOCAL_KEY] && DeadBro::HttpInstrumentation.should_continue_tracking?
79
89
  Thread.current[THREAD_LOCAL_KEY] << payload
80
90
  end
@@ -106,7 +116,9 @@ module DeadBro
106
116
  (respond_to?(:base_url) ? base_url : nil)
107
117
  end
108
118
 
109
- skip_instrumentation = req_url && (req_url.include?("localhost:3100/apm/v1/metrics") || req_url.include?("deadbro.aberatii.com/apm/v1/metrics"))
119
+ # Inside DeadBro's Faraday middleware, which records the request.
120
+ skip_instrumentation = Thread.current[DeadBro::HttpInstrumentation::IN_FARADAY_KEY] ||
121
+ (req_url && (req_url.include?("localhost:3100/apm/v1/metrics") || req_url.include?("deadbro.aberatii.com/apm/v1/metrics")))
110
122
 
111
123
  unless skip_instrumentation
112
124
  payload = {
@@ -116,6 +128,8 @@ module DeadBro
116
128
  status: response && response.code,
117
129
  duration_ms: duration_ms
118
130
  }
131
+ parent_id = DeadBro::SpanTracker.current_id
132
+ payload[:parent_id] = parent_id if parent_id
119
133
  if Thread.current[THREAD_LOCAL_KEY] && DeadBro::HttpInstrumentation.should_continue_tracking?
120
134
  Thread.current[THREAD_LOCAL_KEY] << payload
121
135
  end
@@ -137,10 +151,21 @@ module DeadBro
137
151
  middleware_klass = Class.new(::Faraday::Middleware) do
138
152
  def call(env)
139
153
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
154
+ wall_start = Time.now
140
155
  response = nil
156
+ error = nil
157
+ # A request this middleware leaves out (below) is left to the
158
+ # adapter's own Net::HTTP or Typhoeus hook, as before.
159
+ records = DeadBro::HttpInstrumentation.faraday_records?(env)
160
+ in_faraday = Thread.current[DeadBro::HttpInstrumentation::IN_FARADAY_KEY]
161
+ Thread.current[DeadBro::HttpInstrumentation::IN_FARADAY_KEY] = true if records
141
162
  begin
142
163
  response = @app.call(env)
164
+ rescue => e
165
+ error = e
166
+ raise
143
167
  ensure
168
+ Thread.current[DeadBro::HttpInstrumentation::IN_FARADAY_KEY] = in_faraday
144
169
  finish_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
145
170
  duration_ms = ((finish_time - start_time) * 1000.0).round(2)
146
171
  begin
@@ -150,7 +175,9 @@ module DeadBro
150
175
  url_str = url.to_s
151
176
 
152
177
  is_es_host = DeadBro::HttpInstrumentation.elasticsearch_host?(host, port)
153
- skip = !is_es_host && (url_str.include?("localhost") || url_str.include?("aberatii.com"))
178
+
179
+ tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
180
+ start_offset_ms = tracking_start ? ((wall_start - tracking_start) * 1000.0).round(2) : nil
154
181
 
155
182
  if is_es_host
156
183
  if Thread.current[DeadBro::ElasticsearchSubscriber::THREAD_LOCAL_KEY]
@@ -158,10 +185,11 @@ module DeadBro
158
185
  method: env.method.to_s.upcase,
159
186
  path: url.path,
160
187
  status: response&.status,
161
- duration_ms: duration_ms
188
+ duration_ms: duration_ms,
189
+ start_offset_ms: start_offset_ms
162
190
  )
163
191
  end
164
- elsif !skip
192
+ elsif records
165
193
  lib = DeadBro::HttpInstrumentation.typesense_host?(host, port) ? "typesense" : "faraday"
166
194
  payload = {
167
195
  library: lib,
@@ -170,8 +198,12 @@ module DeadBro
170
198
  host: host,
171
199
  path: url.path,
172
200
  status: response&.status,
173
- duration_ms: duration_ms
201
+ duration_ms: duration_ms,
202
+ start_offset_ms: start_offset_ms,
203
+ exception: error&.class&.name
174
204
  }
205
+ parent_id = DeadBro::SpanTracker.current_id
206
+ payload[:parent_id] = parent_id if parent_id
175
207
  key = DeadBro::HttpInstrumentation::THREAD_LOCAL_KEY
176
208
  if Thread.current[key] && DeadBro::HttpInstrumentation.should_continue_tracking?
177
209
  Thread.current[key] << payload
@@ -202,6 +234,16 @@ module DeadBro
202
234
  rescue
203
235
  end
204
236
 
237
+ # Whether DeadBro's Faraday middleware records the request: everything but
238
+ # localhost and DeadBro's own endpoint, Elasticsearch hosts always.
239
+ def self.faraday_records?(env)
240
+ url = env.url
241
+ url_str = url.to_s
242
+ elasticsearch_host?(url.host.to_s, url.port) || !(url_str.include?("localhost") || url_str.include?("aberatii.com"))
243
+ rescue *DeadBro::CONTAINED_ERRORS
244
+ false
245
+ end
246
+
205
247
  def self.elasticsearch_host?(host, port)
206
248
  return false if host.nil?
207
249
  return true if port == 9200
@@ -141,6 +141,8 @@ module DeadBro
141
141
  gc_pressure: gc_pressure,
142
142
  ar_instantiation_count: ar_instantiation_count,
143
143
  status: has_error ? "failed" : "completed",
144
+ sql_count: DeadBro::Subscriber.sql_count(sql_queries),
145
+ sql_cached_count: DeadBro::Subscriber.sql_cached_count(sql_queries),
144
146
  sql_queries: sql_queries,
145
147
  transaction_events: transaction_events,
146
148
  rails_env: DeadBro.env,
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadBro
4
+ # Times one Rack middleware — or the router the stack ends in — as a span of
5
+ # the request's tree (see SpanTracker). Rails wraps every middleware in one
6
+ # when it builds the app's middleware stack; the probe reads a clock on the
7
+ # way in and out and allocates nothing.
8
+ class MiddlewareProbe
9
+ attr_reader :span_name, :span_kind
10
+
11
+ def initialize(app, name, kind)
12
+ @app = app
13
+ @span_name = name
14
+ @span_kind = kind
15
+ end
16
+
17
+ def call(env)
18
+ frame = SpanTracker.rack_enter(self)
19
+ @app.call(env)
20
+ ensure
21
+ SpanTracker.rack_leave(frame) if frame
22
+ end
23
+
24
+ # Anything else asked of the middleware goes to it unchanged.
25
+ def respond_to_missing?(method_name, include_private = false)
26
+ @app.respond_to?(method_name, include_private) || super
27
+ end
28
+
29
+ def method_missing(method_name, *args, &block)
30
+ if @app.respond_to?(method_name)
31
+ @app.public_send(method_name, *args, &block)
32
+ else
33
+ super
34
+ end
35
+ end
36
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
37
+
38
+ # ActionDispatch::MiddlewareStack::Middleware#build: one probe per middleware.
39
+ module MiddlewareBuild
40
+ def build(app)
41
+ return super unless MiddlewareProbe.enabled?
42
+
43
+ MiddlewareProbe.wrap(super, inspect, SpanTracker::MIDDLEWARE)
44
+ end
45
+ end
46
+
47
+ # ActionDispatch::MiddlewareStack#build: a probe around the endpoint (the
48
+ # app's routes), so routing and controller setup aren't counted as the
49
+ # innermost middleware's own time.
50
+ module StackBuild
51
+ def build(app = nil, &block)
52
+ endpoint = app || block
53
+ return super if endpoint.nil? || !MiddlewareProbe.enabled?
54
+
55
+ super(MiddlewareProbe.wrap(endpoint, endpoint.class.name, SpanTracker::ENDPOINT))
56
+ end
57
+ end
58
+
59
+ # config.middleware_spans_enabled, read when Rails builds the stack.
60
+ def self.enabled?
61
+ DeadBro.configuration.middleware_spans_enabled ? true : false
62
+ rescue
63
+ true
64
+ end
65
+
66
+ def self.wrap(app, name, kind)
67
+ new(app, name.to_s.dup.freeze, kind)
68
+ rescue
69
+ app
70
+ end
71
+
72
+ # Must run before Rails builds the middleware stack (the
73
+ # build_middleware_stack initializer); a stack built earlier has no probes.
74
+ def self.install!
75
+ return false unless defined?(::ActionDispatch::MiddlewareStack)
76
+
77
+ stack = ::ActionDispatch::MiddlewareStack
78
+ stack.prepend(StackBuild) unless stack.ancestors.include?(StackBuild)
79
+ stack::Middleware.prepend(MiddlewareBuild) unless stack::Middleware.ancestors.include?(MiddlewareBuild)
80
+ true
81
+ rescue
82
+ false
83
+ end
84
+ end
85
+ end
@@ -53,6 +53,11 @@ if defined?(Rails) && defined?(Rails::Railtie)
53
53
  require "dead_bro/view_rendering_subscriber"
54
54
  DeadBro::ViewRenderingSubscriber.subscribe!(client: shared_client)
55
55
 
56
+ # Install the span tree (controller action and view spans; the
57
+ # middleware spans come from the probes installed below)
58
+ require "dead_bro/span_tracker"
59
+ DeadBro::SpanTracker.subscribe!
60
+
56
61
  # Install lightweight memory tracking (default)
57
62
  require "dead_bro/lightweight_memory_tracker"
58
63
  require "dead_bro/memory_leak_detector"
@@ -104,6 +109,17 @@ if defined?(Rails) && defined?(Rails::Railtie)
104
109
  # Never raise in Railtie init
105
110
  end
106
111
 
112
+ # Time every middleware and the router for the span tree. Railtie
113
+ # initializers run before the application's build_middleware_stack, so
114
+ # the probes are in place when Rails builds the stack.
115
+ initializer "dead_bro.middleware_probes" do
116
+ require "dead_bro/span_tracker"
117
+ require "dead_bro/middleware_probe"
118
+ DeadBro::MiddlewareProbe.install!
119
+ rescue
120
+ # Never raise in Railtie init
121
+ end
122
+
107
123
  # Insert SQL tracking middleware
108
124
  initializer "dead_bro.sql_tracking_middleware" do |app|
109
125
  require "dead_bro/sql_tracking_middleware"
@@ -90,6 +90,8 @@ module DeadBro
90
90
  db: RedisSubscriber.safe_db(@db),
91
91
  error: error ? error.class.name : nil
92
92
  }
93
+ parent_id = DeadBro::SpanTracker.current_id
94
+ event[:parent_id] = parent_id if parent_id
93
95
 
94
96
  if Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] && RedisSubscriber.should_continue_tracking?
95
97
  Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] << event
@@ -122,6 +124,8 @@ module DeadBro
122
124
  start_offset_ms: start_offset_ms,
123
125
  db: RedisSubscriber.safe_db(@db)
124
126
  }
127
+ parent_id = DeadBro::SpanTracker.current_id
128
+ event[:parent_id] = parent_id if parent_id
125
129
 
126
130
  if Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] && RedisSubscriber.should_continue_tracking?
127
131
  Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] << event
@@ -154,6 +158,8 @@ module DeadBro
154
158
  start_offset_ms: start_offset_ms,
155
159
  db: RedisSubscriber.safe_db(@db)
156
160
  }
161
+ parent_id = DeadBro::SpanTracker.current_id
162
+ event[:parent_id] = parent_id if parent_id
157
163
 
158
164
  if Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] && RedisSubscriber.should_continue_tracking?
159
165
  Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] << event
@@ -220,7 +226,7 @@ module DeadBro
220
226
 
221
227
  def self.build_event(name, data, duration_ms, start_offset_ms = nil)
222
228
  cmd = extract_command(data)
223
- {
229
+ event = {
224
230
  event: name.to_s,
225
231
  command: cmd[:command],
226
232
  key: cmd[:key],
@@ -229,6 +235,9 @@ module DeadBro
229
235
  start_offset_ms: start_offset_ms,
230
236
  db: safe_db(data[:db])
231
237
  }
238
+ parent_id = DeadBro::SpanTracker.current_id
239
+ event[:parent_id] = parent_id if parent_id
240
+ event
232
241
  rescue
233
242
  nil
234
243
  end