dead_bro 0.2.37 → 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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +14 -0
- data/lib/dead_bro/cache_subscriber.rb +12 -1
- data/lib/dead_bro/configuration.rb +24 -1
- data/lib/dead_bro/elasticsearch_subscriber.rb +4 -1
- data/lib/dead_bro/http_instrumentation.rb +48 -6
- data/lib/dead_bro/middleware_probe.rb +85 -0
- data/lib/dead_bro/railtie.rb +16 -0
- data/lib/dead_bro/redis_subscriber.rb +10 -1
- data/lib/dead_bro/span_tracker.rb +650 -0
- data/lib/dead_bro/sql_subscriber.rb +8 -2
- data/lib/dead_bro/sql_tracking_middleware.rb +6 -0
- data/lib/dead_bro/subscriber.rb +19 -5
- data/lib/dead_bro/version.rb +1 -1
- data/lib/dead_bro/watch_tracker.rb +5 -0
- 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: 4258f98fde5b0e222593ebfadee095b6402576d94a33a2b57ac26c71f0b75d47
|
|
4
|
+
data.tar.gz: 4a268f4eb24b882bf2dddd36a45db694f664319d3fa9650c3587eb59dd37f2af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94f6cccf6594f0c50467b61a5d2bf53d6b73e7e72e6f042c2eae3cea2bad12e674e8a2f525bb7880f51f3b6cc679e3f3951dfba224b07e9701b48669060ffd51
|
|
7
|
+
data.tar.gz: eae0e565ae2d1b43f20df7c3fd14311c7543f29b1680f0495c1fa1b4ca86a30c880b20dc94ba9cc147288dff41fa39eb7aae360dafb76139b751dccc380d02e4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
3
15
|
## [0.2.37] - 2026-10-26
|
|
4
16
|
|
|
5
17
|
### Fixed
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -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
|
data/lib/dead_bro/railtie.rb
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DeadBro
|
|
4
|
+
# Builds a web request's span tree: every span gets an id and the id of the
|
|
5
|
+
# span it ran inside (parent_id), so the dashboard can nest the Request Trace
|
|
6
|
+
# and show each span's total (inclusive) and self (exclusive) time.
|
|
7
|
+
#
|
|
8
|
+
# Nodes are kept for the spans other spans run inside — Rack middleware and
|
|
9
|
+
# the router (MiddlewareProbe), the controller action, view renders,
|
|
10
|
+
# DeadBro.watch blocks and the block a Rails.cache.fetch miss runs — and for
|
|
11
|
+
# SQL queries, and are sent as the request's `spans`. Repeated calls on the
|
|
12
|
+
# same path share one node: a partial rendered 50 times inside one template
|
|
13
|
+
# is one node with count 50, and the query it runs each time is one child
|
|
14
|
+
# node with count 50. Cache, Redis, HTTP, Elasticsearch and transaction
|
|
15
|
+
# events stay in their own lists and carry the parent_id of the span they
|
|
16
|
+
# ran inside; the cache event of a fetch block carries its node's span_id.
|
|
17
|
+
#
|
|
18
|
+
# Node fields: id, parent_id (nil at the top), kind (middleware, endpoint,
|
|
19
|
+
# action, view, watch, cache, sql), name, count, total_ms — the summed
|
|
20
|
+
# duration of the node's calls, nested spans included — and start_ms /
|
|
21
|
+
# end_ms — the first call's start and the last call's end, as offsets from
|
|
22
|
+
# the tracking start like every other start_offset_ms. View nodes add type
|
|
23
|
+
# (template, layout, partial, collection) and cache nodes type generate;
|
|
24
|
+
# SQL nodes carry ref, the id of their sql_queries entry, instead of a name.
|
|
25
|
+
#
|
|
26
|
+
# The middleware chain returns after the controller action has finished, so
|
|
27
|
+
# a request that runs inside it has its payload held until the outermost
|
|
28
|
+
# middleware returns (see .post), and is sent with the middleware spans. If
|
|
29
|
+
# that middleware never returns through its probe, the payload is sent when
|
|
30
|
+
# the thread's next request comes in (see .rack_enter).
|
|
31
|
+
#
|
|
32
|
+
# The tree belongs to the thread that runs the request. ActionController::Live
|
|
33
|
+
# runs the action on a thread of its own, with the request thread's locals
|
|
34
|
+
# copied over; that thread records the action in a tree of its own (see
|
|
35
|
+
# .live_context).
|
|
36
|
+
module SpanTracker
|
|
37
|
+
THREAD_KEY = :dead_bro_span_context
|
|
38
|
+
|
|
39
|
+
# Nodes kept per request; past it, a span's time stays in its parent's self
|
|
40
|
+
# time and the request's spans_dropped counts what was left out.
|
|
41
|
+
MAX_NODES = 500
|
|
42
|
+
# Middleware/router frames kept per request (a Rails stack has ~25).
|
|
43
|
+
MAX_RACK_FRAMES = 200
|
|
44
|
+
# Payloads held for one middleware chain: the request's own, and the
|
|
45
|
+
# exceptions app's when Rails dispatches an error to it. Past this a
|
|
46
|
+
# payload is sent right away.
|
|
47
|
+
MAX_PENDING = 10
|
|
48
|
+
|
|
49
|
+
MIDDLEWARE = "middleware"
|
|
50
|
+
ENDPOINT = "endpoint"
|
|
51
|
+
ACTION = "action"
|
|
52
|
+
VIEW = "view"
|
|
53
|
+
WATCH = "watch"
|
|
54
|
+
CACHE = "cache"
|
|
55
|
+
SQL = "sql"
|
|
56
|
+
|
|
57
|
+
# Frame pushed for a span past MAX_NODES, so leaving it still pops the
|
|
58
|
+
# right frame.
|
|
59
|
+
DROPPED_VIEW = {kind: VIEW}.freeze
|
|
60
|
+
DROPPED_WATCH = {kind: WATCH}.freeze
|
|
61
|
+
DROPPED_CACHE = {kind: CACHE}.freeze
|
|
62
|
+
|
|
63
|
+
# Rails.cache.fetch runs its block inside this event when the key misses.
|
|
64
|
+
CACHE_GENERATE_EVENT = "cache_generate.active_support"
|
|
65
|
+
# Fetch blocks whose keys differ only in their digits share a node.
|
|
66
|
+
CACHE_KEY_DIGITS = /\d+/
|
|
67
|
+
|
|
68
|
+
VIEW_EVENTS = {
|
|
69
|
+
"render_layout.action_view" => "layout",
|
|
70
|
+
"render_template.action_view" => "template",
|
|
71
|
+
"render_partial.action_view" => "partial",
|
|
72
|
+
"render_collection.action_view" => "collection"
|
|
73
|
+
}.freeze
|
|
74
|
+
|
|
75
|
+
# One request's tree, reused by the thread's next request.
|
|
76
|
+
class Context
|
|
77
|
+
attr_reader :nodes, :dropped, :thread
|
|
78
|
+
attr_accessor :pending
|
|
79
|
+
|
|
80
|
+
def initialize
|
|
81
|
+
# The thread this tree is recorded on. Another thread holding this
|
|
82
|
+
# context (ActionController::Live copies the request thread's locals)
|
|
83
|
+
# records nothing into it.
|
|
84
|
+
@thread = Thread.current
|
|
85
|
+
# Open spans inside the middleware chain, innermost last: a node Hash
|
|
86
|
+
# or a DROPPED_* marker. The ids are what starts now gets as
|
|
87
|
+
# parent_id; the starts are Float seconds (CLOCK_REALTIME, the clock
|
|
88
|
+
# behind every other offset in the payload).
|
|
89
|
+
@frames = []
|
|
90
|
+
@frame_ids = []
|
|
91
|
+
@frame_starts = []
|
|
92
|
+
# parent_id => {key => node}, per kind, so repeat calls on a path find
|
|
93
|
+
# their node.
|
|
94
|
+
@view_index = {}
|
|
95
|
+
@watch_index = {}
|
|
96
|
+
@cache_index = {}
|
|
97
|
+
@sql_index = {}
|
|
98
|
+
# Each fetch block's event payload => its node, for CacheSubscriber,
|
|
99
|
+
# whose listener for the same event may run before or after ours.
|
|
100
|
+
@cache_payloads = {}.compare_by_identity
|
|
101
|
+
# Middleware frames, as parallel arrays: the chain runs on every
|
|
102
|
+
# request, so recording it allocates nothing. Frame i is span i + 1
|
|
103
|
+
# (the other spans are numbered after MAX_RACK_FRAMES); @rack_current
|
|
104
|
+
# is the innermost open one and @rack_parents each one's enclosing
|
|
105
|
+
# frame.
|
|
106
|
+
@rack_probes = []
|
|
107
|
+
@rack_parents = []
|
|
108
|
+
@rack_starts = []
|
|
109
|
+
@rack_ends = []
|
|
110
|
+
reset
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def reset
|
|
114
|
+
# A new Array: the last one may still be in a payload waiting to be sent.
|
|
115
|
+
@nodes = []
|
|
116
|
+
@frames.clear
|
|
117
|
+
@frame_ids.clear
|
|
118
|
+
@frame_starts.clear
|
|
119
|
+
@view_index.clear
|
|
120
|
+
@watch_index.clear
|
|
121
|
+
@cache_index.clear
|
|
122
|
+
@sql_index.clear
|
|
123
|
+
@cache_payloads.clear
|
|
124
|
+
@rack_probes.clear
|
|
125
|
+
@rack_parents.clear
|
|
126
|
+
@rack_starts.clear
|
|
127
|
+
@rack_ends.clear
|
|
128
|
+
@rack_current = nil
|
|
129
|
+
@next_id = MAX_RACK_FRAMES
|
|
130
|
+
@base = nil
|
|
131
|
+
@tracking = false
|
|
132
|
+
@recording_rack = false
|
|
133
|
+
@action = nil
|
|
134
|
+
@pending = nil
|
|
135
|
+
@dropped = 0
|
|
136
|
+
@finalized = false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def tracking?
|
|
140
|
+
@tracking
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def idle?
|
|
144
|
+
@frames.empty? && @pending.nil? && !rack_open?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Left over from a request whose outermost middleware never returned
|
|
148
|
+
# through its probe — an exception raised into the thread from outside
|
|
149
|
+
# the Rails stack (an outer timeout) while the probe opened or closed its
|
|
150
|
+
# frame: nothing is open or running, or the outermost probe is entered
|
|
151
|
+
# again while its own frame still is.
|
|
152
|
+
def abandoned?(probe)
|
|
153
|
+
return false if idle?
|
|
154
|
+
|
|
155
|
+
(@rack_current.nil? && @frames.empty?) || (@rack_current == 0 && @rack_probes[0].equal?(probe))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def current_id
|
|
159
|
+
@frame_ids.last || (@rack_current && @rack_current + 1)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The tracking start (TRACKING_START_TIME_KEY), which every offset is
|
|
163
|
+
# measured from.
|
|
164
|
+
def start_tracking(base)
|
|
165
|
+
@base = base
|
|
166
|
+
@tracking = true
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def stop_tracking
|
|
170
|
+
@tracking = false
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# True while the outermost middleware hasn't returned yet.
|
|
174
|
+
def rack_open?
|
|
175
|
+
!@rack_ends.empty? && @rack_ends[0].nil?
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def start_rack
|
|
179
|
+
@recording_rack = true
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# A Rack app called from inside the action isn't recorded: middleware
|
|
183
|
+
# frames are the outermost spans.
|
|
184
|
+
def rack_enter(probe, now)
|
|
185
|
+
return nil unless @recording_rack && @frames.empty?
|
|
186
|
+
|
|
187
|
+
i = @rack_probes.size
|
|
188
|
+
return nil if i >= MAX_RACK_FRAMES
|
|
189
|
+
|
|
190
|
+
@rack_probes << probe
|
|
191
|
+
@rack_parents << @rack_current
|
|
192
|
+
@rack_starts << now
|
|
193
|
+
@rack_ends << nil
|
|
194
|
+
@rack_current = i
|
|
195
|
+
i
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def rack_leave(i, now)
|
|
199
|
+
@rack_ends[i] = now
|
|
200
|
+
@rack_current = @rack_parents[i]
|
|
201
|
+
# Anything still open inside it never finished.
|
|
202
|
+
pop_to(0) unless @frames.empty?
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def enter_action(now)
|
|
206
|
+
node = new_node(ACTION, current_id)
|
|
207
|
+
return nil unless node
|
|
208
|
+
|
|
209
|
+
@action = node
|
|
210
|
+
push(node, node[:id], now)
|
|
211
|
+
node
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Closed with the times process_action reports, which every listener of
|
|
215
|
+
# the event sees, rather than when this listener happens to run.
|
|
216
|
+
def finish_action(started, finished)
|
|
217
|
+
j = innermost(ACTION)
|
|
218
|
+
return unless j
|
|
219
|
+
|
|
220
|
+
node = @frames[j]
|
|
221
|
+
pop_to(j)
|
|
222
|
+
add_call(node, started, finished)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def enter_view(identifier, type, now)
|
|
226
|
+
key = identifier || type
|
|
227
|
+
node = node_for(VIEW, @view_index, key) do |created|
|
|
228
|
+
created[:name] = DeadBro::ViewRenderingSubscriber.safe_identifier(identifier)
|
|
229
|
+
created[:type] = type
|
|
230
|
+
end
|
|
231
|
+
push(node || DROPPED_VIEW, node ? node[:id] : current_id, now)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def leave_view(now)
|
|
235
|
+
j = innermost(VIEW)
|
|
236
|
+
leave_frame(j, now) if j
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def enter_watch(label, now)
|
|
240
|
+
node = node_for(WATCH, @watch_index, label) { |created| created[:name] = label }
|
|
241
|
+
frame = node || DROPPED_WATCH
|
|
242
|
+
push(frame, node ? node[:id] : current_id, now)
|
|
243
|
+
frame
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def leave_watch(frame, now)
|
|
247
|
+
j = @frames.size - 1
|
|
248
|
+
j -= 1 while j >= 0 && !@frames[j].equal?(frame)
|
|
249
|
+
leave_frame(j, now) if j >= 0
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# A Rails.cache.fetch miss: the block it runs nests in this span. Keys
|
|
253
|
+
# that differ only in their digits share a node, named by that pattern.
|
|
254
|
+
def enter_cache_generate(payload, key, now)
|
|
255
|
+
pattern = key.gsub(CACHE_KEY_DIGITS, "?")
|
|
256
|
+
node = node_for(CACHE, @cache_index, pattern) do |created|
|
|
257
|
+
created[:name] = key
|
|
258
|
+
created[:type] = "generate"
|
|
259
|
+
end
|
|
260
|
+
if node
|
|
261
|
+
node[:name] = pattern unless node[:name] == key
|
|
262
|
+
@cache_payloads[payload] = node
|
|
263
|
+
end
|
|
264
|
+
push(node || DROPPED_CACHE, node ? node[:id] : current_id, now)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def leave_cache_generate(now)
|
|
268
|
+
j = innermost(CACHE)
|
|
269
|
+
leave_frame(j, now) if j
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def cache_node(payload)
|
|
273
|
+
@cache_payloads[payload]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def sql(key, ref, started, finished)
|
|
277
|
+
node = node_for(SQL, @sql_index, key) { |created| created[:ref] = ref }
|
|
278
|
+
add_call(node, started, finished) if node
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Rounds the totals and turns the middleware frames into nodes. Once per
|
|
282
|
+
# request, right before the payload is sent.
|
|
283
|
+
def finalize(action_name)
|
|
284
|
+
return if @finalized || @base.nil?
|
|
285
|
+
|
|
286
|
+
@finalized = true
|
|
287
|
+
@action[:name] ||= action_name if @action
|
|
288
|
+
@nodes.each { |node| node[:total_ms] = node[:total_ms].round(3) }
|
|
289
|
+
@rack_probes.each_with_index do |probe, i|
|
|
290
|
+
start = @rack_starts[i]
|
|
291
|
+
finish = @rack_ends[i] || open_frame_end(start)
|
|
292
|
+
parent = @rack_parents[i]
|
|
293
|
+
@nodes << {
|
|
294
|
+
id: i + 1,
|
|
295
|
+
parent_id: parent && parent + 1,
|
|
296
|
+
kind: probe.span_kind,
|
|
297
|
+
name: probe.span_name,
|
|
298
|
+
count: 1,
|
|
299
|
+
total_ms: ((finish - start) * 1000.0).round(3),
|
|
300
|
+
start_ms: offset_ms(start),
|
|
301
|
+
end_ms: offset_ms(finish)
|
|
302
|
+
}
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
private
|
|
307
|
+
|
|
308
|
+
def push(frame, id, now)
|
|
309
|
+
@frames << frame
|
|
310
|
+
@frame_ids << id
|
|
311
|
+
@frame_starts << now
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Pops frame j and every frame still open inside it.
|
|
315
|
+
def pop_to(j)
|
|
316
|
+
(@frames.size - j).times do
|
|
317
|
+
@frames.pop
|
|
318
|
+
@frame_ids.pop
|
|
319
|
+
@frame_starts.pop
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# The innermost open frame of a kind (a node or a DROPPED_* marker).
|
|
324
|
+
# A loop rather than rindex with a block, which allocates.
|
|
325
|
+
def innermost(kind)
|
|
326
|
+
j = @frames.size - 1
|
|
327
|
+
while j >= 0
|
|
328
|
+
frame = @frames[j]
|
|
329
|
+
return j if frame.is_a?(Hash) && frame[:kind] == kind
|
|
330
|
+
|
|
331
|
+
j -= 1
|
|
332
|
+
end
|
|
333
|
+
nil
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def leave_frame(j, now)
|
|
337
|
+
frame = @frames[j]
|
|
338
|
+
start = @frame_starts[j]
|
|
339
|
+
pop_to(j)
|
|
340
|
+
add_call(frame, start, now) unless frame.frozen?
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# The node for key under the innermost open span, created on first use;
|
|
344
|
+
# nil past MAX_NODES.
|
|
345
|
+
def node_for(kind, index, key)
|
|
346
|
+
parent_id = current_id
|
|
347
|
+
by_key = (index[parent_id] ||= {})
|
|
348
|
+
node = by_key[key]
|
|
349
|
+
return node if node
|
|
350
|
+
|
|
351
|
+
node = new_node(kind, parent_id)
|
|
352
|
+
return nil unless node
|
|
353
|
+
|
|
354
|
+
yield node
|
|
355
|
+
by_key[key] = node
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def new_node(kind, parent_id)
|
|
359
|
+
if @nodes.size >= MAX_NODES
|
|
360
|
+
@dropped += 1
|
|
361
|
+
return nil
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
node = {id: (@next_id += 1), parent_id: parent_id, kind: kind, count: 0, total_ms: 0.0, start_ms: nil, end_ms: nil}
|
|
365
|
+
@nodes << node
|
|
366
|
+
node
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def add_call(node, started, finished)
|
|
370
|
+
node[:count] += 1
|
|
371
|
+
node[:total_ms] += (finished - started) * 1000.0
|
|
372
|
+
node[:start_ms] ||= offset_ms(started)
|
|
373
|
+
node[:end_ms] = offset_ms(finished)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def offset_ms(time)
|
|
377
|
+
((time - @base) * 1000.0).round(3)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# A frame sent while still open (its middleware never returned through
|
|
381
|
+
# the probe) ends where the last frame that did close ended.
|
|
382
|
+
def open_frame_end(start)
|
|
383
|
+
finish = start
|
|
384
|
+
@rack_ends.each { |time| finish = time if time && time > finish }
|
|
385
|
+
finish
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Pushes and pops the view spans; start/finish listeners, so a view's
|
|
390
|
+
# children see it as their parent while it renders.
|
|
391
|
+
class ViewListener
|
|
392
|
+
def initialize(type)
|
|
393
|
+
@type = type
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def start(_name, _id, payload)
|
|
397
|
+
SpanTracker.enter_view(payload[:identifier], @type)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def finish(_name, _id, _payload)
|
|
401
|
+
SpanTracker.leave_view
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# start_processing.action_controller fires right before
|
|
406
|
+
# process_action.action_controller starts: the action span opens here and
|
|
407
|
+
# Subscriber closes it.
|
|
408
|
+
class ActionListener
|
|
409
|
+
def start(_name, _id, _payload)
|
|
410
|
+
SpanTracker.enter_action
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def finish(_name, _id, _payload)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# cache_generate wraps the block of a Rails.cache.fetch miss, so what the
|
|
418
|
+
# block runs nests in its span instead of sitting next to it.
|
|
419
|
+
class CacheGenerateListener
|
|
420
|
+
def start(_name, _id, payload)
|
|
421
|
+
SpanTracker.enter_cache_generate(payload)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def finish(_name, _id, _payload)
|
|
425
|
+
SpanTracker.leave_cache_generate
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
module_function
|
|
430
|
+
|
|
431
|
+
def subscribe!
|
|
432
|
+
return unless defined?(ActiveSupport::Notifications)
|
|
433
|
+
|
|
434
|
+
VIEW_EVENTS.each do |event_name, type|
|
|
435
|
+
ActiveSupport::Notifications.subscribe(event_name, ViewListener.new(type))
|
|
436
|
+
end
|
|
437
|
+
ActiveSupport::Notifications.subscribe("start_processing.action_controller", ActionListener.new)
|
|
438
|
+
ActiveSupport::Notifications.subscribe(CACHE_GENERATE_EVENT, CacheGenerateListener.new)
|
|
439
|
+
rescue
|
|
440
|
+
# Never raise from instrumentation install
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def now
|
|
444
|
+
Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# This thread's context, created on first use — and in place of one
|
|
448
|
+
# copied from another thread.
|
|
449
|
+
def context
|
|
450
|
+
own_context || (Thread.current[THREAD_KEY] = Context.new)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def own_context
|
|
454
|
+
ctx = Thread.current[THREAD_KEY]
|
|
455
|
+
ctx if ctx && ctx.thread.equal?(Thread.current)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# The context of a request being tracked on this thread, else nil.
|
|
459
|
+
def tracking_context
|
|
460
|
+
ctx = own_context
|
|
461
|
+
ctx if ctx&.tracking?
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# ActionController::Live runs the action on a thread of its own, with the
|
|
465
|
+
# request thread's locals copied over — that thread's context and the
|
|
466
|
+
# tracking start among them. The request thread leaves the middleware
|
|
467
|
+
# chain once the response is committed, while the action may still run,
|
|
468
|
+
# so the action thread records the action in a context of its own,
|
|
469
|
+
# measured from the same tracking start. Nil anywhere else.
|
|
470
|
+
def live_context
|
|
471
|
+
copied = Thread.current[THREAD_KEY]
|
|
472
|
+
return nil if copied.nil? || copied.thread.equal?(Thread.current)
|
|
473
|
+
|
|
474
|
+
tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
|
|
475
|
+
return nil unless tracking_start
|
|
476
|
+
|
|
477
|
+
ctx = Thread.current[THREAD_KEY] = Context.new
|
|
478
|
+
ctx.start_tracking(tracking_start.to_f)
|
|
479
|
+
ctx
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# The id of the span running now — the parent_id of an event recorded now.
|
|
483
|
+
def current_id
|
|
484
|
+
tracking_context&.current_id
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Called by SqlTrackingMiddleware once tracking is set up. Without
|
|
488
|
+
# MiddlewareProbe frames around it (no Rails middleware stack) the request
|
|
489
|
+
# starts here.
|
|
490
|
+
def start_request(tracking_start)
|
|
491
|
+
ctx = context
|
|
492
|
+
ctx.reset if ctx.idle?
|
|
493
|
+
ctx.start_tracking(tracking_start.to_f)
|
|
494
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def stop_request
|
|
498
|
+
own_context&.stop_tracking
|
|
499
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Returns a frame token for rack_leave, or nil when nothing is recorded.
|
|
503
|
+
def rack_enter(probe)
|
|
504
|
+
config = DeadBro.configuration
|
|
505
|
+
return nil unless config.enabled
|
|
506
|
+
|
|
507
|
+
ctx = own_context
|
|
508
|
+
if ctx&.abandoned?(probe)
|
|
509
|
+
# Send what the abandoned request held, and start over — also while
|
|
510
|
+
# the dashboard has middleware spans off, so it doesn't linger.
|
|
511
|
+
begin
|
|
512
|
+
flush(ctx)
|
|
513
|
+
ensure
|
|
514
|
+
ctx.reset
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
return nil unless config.middleware_spans_active?
|
|
518
|
+
|
|
519
|
+
ctx ||= context
|
|
520
|
+
if ctx.idle?
|
|
521
|
+
ctx.reset
|
|
522
|
+
ctx.start_rack
|
|
523
|
+
end
|
|
524
|
+
ctx.rack_enter(probe, now)
|
|
525
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
526
|
+
nil
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# Leaving the outermost frame (0) sends the payloads held for it.
|
|
530
|
+
def rack_leave(frame)
|
|
531
|
+
ctx = own_context
|
|
532
|
+
return unless ctx
|
|
533
|
+
|
|
534
|
+
ctx.rack_leave(frame, now)
|
|
535
|
+
return unless frame == 0
|
|
536
|
+
|
|
537
|
+
begin
|
|
538
|
+
flush(ctx)
|
|
539
|
+
ensure
|
|
540
|
+
ctx.reset
|
|
541
|
+
end
|
|
542
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def enter_action
|
|
546
|
+
(tracking_context || live_context)&.enter_action(now)
|
|
547
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def finish_action(started, finished)
|
|
551
|
+
tracking_context&.finish_action(started.to_f, finished.to_f)
|
|
552
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def enter_view(identifier, type)
|
|
556
|
+
tracking_context&.enter_view(identifier, type, now)
|
|
557
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def leave_view
|
|
561
|
+
tracking_context&.leave_view(now)
|
|
562
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Returns the frame to pass to leave_watch, nil when not tracking.
|
|
566
|
+
def enter_watch(label)
|
|
567
|
+
tracking_context&.enter_watch(label, now)
|
|
568
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
569
|
+
nil
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def leave_watch(frame)
|
|
573
|
+
return unless frame
|
|
574
|
+
|
|
575
|
+
tracking_context&.leave_watch(frame, now)
|
|
576
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def enter_cache_generate(payload)
|
|
580
|
+
ctx = tracking_context
|
|
581
|
+
return unless ctx
|
|
582
|
+
|
|
583
|
+
key = defined?(DeadBro::CacheSubscriber) ? DeadBro::CacheSubscriber.safe_key(payload[:key]) : payload[:key]
|
|
584
|
+
ctx.enter_cache_generate(payload, key.to_s, now)
|
|
585
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def leave_cache_generate
|
|
589
|
+
tracking_context&.leave_cache_generate(now)
|
|
590
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# The node of a fetch block's cache_generate event, by the event's
|
|
594
|
+
# payload; nil for every other event.
|
|
595
|
+
def cache_node(payload)
|
|
596
|
+
tracking_context&.cache_node(payload)
|
|
597
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
598
|
+
nil
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# A query of the request's own SQL tracking (sql_depth 1: an inline job's
|
|
602
|
+
# queries go to the job's payload, whose sql_queries ids are its own).
|
|
603
|
+
def sql(key, ref, started, finished, sql_depth)
|
|
604
|
+
return unless sql_depth == 1
|
|
605
|
+
|
|
606
|
+
tracking_context&.sql(key, ref, started.to_f, finished.to_f)
|
|
607
|
+
rescue *DeadBro::CONTAINED_ERRORS
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# The request's nodes for the payload (finished by .post), or nil when the
|
|
611
|
+
# request isn't being tracked — e.g. the exceptions app Rails dispatches to
|
|
612
|
+
# after DeadBro's middleware has returned, whose context still holds the
|
|
613
|
+
# failed request's tree.
|
|
614
|
+
def payload_spans
|
|
615
|
+
ctx = tracking_context
|
|
616
|
+
return nil unless ctx && !ctx.nodes.empty?
|
|
617
|
+
|
|
618
|
+
ctx.nodes
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def payload_spans_dropped
|
|
622
|
+
ctx = tracking_context
|
|
623
|
+
ctx.dropped if ctx && ctx.dropped > 0
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Sends a request's payload. While the middleware chain is still running
|
|
627
|
+
# the payload is held and sent when its outermost middleware returns, with
|
|
628
|
+
# the middleware spans closed.
|
|
629
|
+
def post(client, event_name, payload, action_name)
|
|
630
|
+
ctx = own_context
|
|
631
|
+
if ctx&.rack_open? && (ctx.pending.nil? || ctx.pending.size < MAX_PENDING)
|
|
632
|
+
(ctx.pending ||= []) << [client, event_name, payload, action_name]
|
|
633
|
+
else
|
|
634
|
+
ctx&.finalize(action_name)
|
|
635
|
+
client.post_metric(event_name: event_name, payload: payload, force: true)
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def flush(ctx)
|
|
640
|
+
pending = ctx.pending
|
|
641
|
+
return unless pending
|
|
642
|
+
|
|
643
|
+
ctx.pending = nil
|
|
644
|
+
pending.each do |client, event_name, payload, action_name|
|
|
645
|
+
ctx.finalize(action_name)
|
|
646
|
+
client.post_metric(event_name: event_name, payload: payload, force: true)
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
end
|
|
@@ -156,11 +156,14 @@ module DeadBro
|
|
|
156
156
|
tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
|
|
157
157
|
start_offset_ms = tracking_start ? ((started - tracking_start) * 1000.0).round(2) : nil
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
event = {
|
|
160
160
|
event: name,
|
|
161
161
|
duration_ms: ((finished - started) * 1000.0).round(2),
|
|
162
162
|
start_offset_ms: start_offset_ms
|
|
163
163
|
}
|
|
164
|
+
parent_id = DeadBro::SpanTracker.current_id
|
|
165
|
+
event[:parent_id] = parent_id if parent_id
|
|
166
|
+
current << event
|
|
164
167
|
rescue
|
|
165
168
|
nil
|
|
166
169
|
end
|
|
@@ -251,7 +254,9 @@ module DeadBro
|
|
|
251
254
|
agg[:n_plus_one] = true if call_count >= N_PLUS_ONE_THRESHOLD
|
|
252
255
|
agg[:backtrace] = captured_trace if agg[:backtrace].empty? && !captured_trace.empty?
|
|
253
256
|
else
|
|
254
|
-
aggregates_h[normalized_key] = {
|
|
257
|
+
agg = aggregates_h[normalized_key] = {
|
|
258
|
+
# What the request's span tree refers to this query by (ref).
|
|
259
|
+
id: aggregates_h.size + 1,
|
|
255
260
|
sql: sanitized_sql,
|
|
256
261
|
name: data[:name],
|
|
257
262
|
count: 1,
|
|
@@ -266,6 +271,7 @@ module DeadBro
|
|
|
266
271
|
explain_plan: nil
|
|
267
272
|
}
|
|
268
273
|
end
|
|
274
|
+
DeadBro::SpanTracker.sql(normalized_key, agg[:id], started, finished, agg_stack.length)
|
|
269
275
|
end
|
|
270
276
|
|
|
271
277
|
if should_continue_tracking?(current, MAX_TRACKED_QUERIES)
|
|
@@ -38,6 +38,9 @@ module DeadBro
|
|
|
38
38
|
DeadBro::SqlSubscriber.start_request_tracking
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# Start the span tree, measured from the same rack entry
|
|
42
|
+
DeadBro::SpanTracker.start_request(rack_entry) if defined?(DeadBro::SpanTracker)
|
|
43
|
+
|
|
41
44
|
# Start cache tracking for this request
|
|
42
45
|
if defined?(DeadBro::CacheSubscriber)
|
|
43
46
|
DeadBro::CacheSubscriber.start_request_tracking
|
|
@@ -158,6 +161,9 @@ module DeadBro
|
|
|
158
161
|
ensure
|
|
159
162
|
Thread.current[:dead_bro_alloc_active] = nil
|
|
160
163
|
Thread.current[DeadBro::TRACKING_START_TIME_KEY] = nil
|
|
164
|
+
# Keeps the spans (and a payload held for the middleware chain) until
|
|
165
|
+
# the outermost middleware returns.
|
|
166
|
+
DeadBro::SpanTracker.stop_request if defined?(DeadBro::SpanTracker)
|
|
161
167
|
end
|
|
162
168
|
|
|
163
169
|
def parse_queue_start(env, rack_entry)
|
data/lib/dead_bro/subscriber.rb
CHANGED
|
@@ -13,6 +13,10 @@ module DeadBro
|
|
|
13
13
|
# drop with an excluded request) — ErrorMiddleware must not send it again.
|
|
14
14
|
mark_reported(data)
|
|
15
15
|
|
|
16
|
+
# Closes the action span opened at start_processing, whether or not
|
|
17
|
+
# this request is sent.
|
|
18
|
+
DeadBro::SpanTracker.finish_action(started, finished)
|
|
19
|
+
|
|
16
20
|
# When disabled remotely, fire a heartbeat at most once per minute so the gem
|
|
17
21
|
# can detect when tracking has been re-enabled, then skip all tracking.
|
|
18
22
|
unless DeadBro.configuration.enabled
|
|
@@ -191,6 +195,14 @@ module DeadBro
|
|
|
191
195
|
watch_events: watch_events,
|
|
192
196
|
logs: DeadBro.logger.logs
|
|
193
197
|
}
|
|
198
|
+
# The span tree (see SpanTracker); the middleware spans are added when
|
|
199
|
+
# the payload is sent.
|
|
200
|
+
spans = DeadBro::SpanTracker.payload_spans
|
|
201
|
+
if spans
|
|
202
|
+
detail_fields[:spans] = spans
|
|
203
|
+
spans_dropped = DeadBro::SpanTracker.payload_spans_dropped
|
|
204
|
+
detail_fields[:spans_dropped] = spans_dropped if spans_dropped
|
|
205
|
+
end
|
|
194
206
|
|
|
195
207
|
# Report exceptions attached to this action (e.g. controller/view errors)
|
|
196
208
|
if data[:exception] || data[:exception_object]
|
|
@@ -223,7 +235,7 @@ module DeadBro
|
|
|
223
235
|
)
|
|
224
236
|
|
|
225
237
|
event_name = (exception_class || exception_obj&.class&.name || "exception").to_s
|
|
226
|
-
|
|
238
|
+
DeadBro::SpanTracker.post(client, event_name, error_payload, request_type_key)
|
|
227
239
|
rescue
|
|
228
240
|
ensure
|
|
229
241
|
next
|
|
@@ -247,10 +259,12 @@ module DeadBro
|
|
|
247
259
|
user_agent: safe_user_agent(data),
|
|
248
260
|
user_id: extract_user_id(data)
|
|
249
261
|
)
|
|
250
|
-
# force: true — the sampling decision (global or
|
|
251
|
-
# already made above; client#post_metric must not
|
|
252
|
-
# global-only rate, which would silently override a
|
|
253
|
-
|
|
262
|
+
# Sent with force: true — the sampling decision (global or
|
|
263
|
+
# per-request-type) was already made above; client#post_metric must not
|
|
264
|
+
# re-roll it with the global-only rate, which would silently override a
|
|
265
|
+
# per-type sample rate. Held until the middleware chain returns when the
|
|
266
|
+
# request runs inside one (see SpanTracker.post).
|
|
267
|
+
DeadBro::SpanTracker.post(client, name, payload, request_type_key)
|
|
254
268
|
rescue *DeadBro::CONTAINED_ERRORS
|
|
255
269
|
# Anything raised here would fail the host's request (see
|
|
256
270
|
# DeadBro::CONTAINED_ERRORS) — drop this request's metrics instead.
|
data/lib/dead_bro/version.rb
CHANGED
|
@@ -91,6 +91,8 @@ module DeadBro
|
|
|
91
91
|
frame[:slot] = events.length
|
|
92
92
|
events << nil
|
|
93
93
|
active_stack << frame
|
|
94
|
+
# The block's span in the request's span tree (nil in a job).
|
|
95
|
+
span_frame = DeadBro::SpanTracker.enter_watch(sanitized_label)
|
|
94
96
|
|
|
95
97
|
begin
|
|
96
98
|
yield
|
|
@@ -104,6 +106,8 @@ module DeadBro
|
|
|
104
106
|
raise
|
|
105
107
|
ensure
|
|
106
108
|
active_stack.pop if active_stack.last.equal?(frame)
|
|
109
|
+
DeadBro::SpanTracker.leave_watch(span_frame)
|
|
110
|
+
frame[:span_id] = span_frame[:id] if span_frame.is_a?(Hash)
|
|
107
111
|
append_completed_span(events, frame)
|
|
108
112
|
end
|
|
109
113
|
end
|
|
@@ -178,6 +182,7 @@ module DeadBro
|
|
|
178
182
|
error: frame[:error] == true
|
|
179
183
|
}
|
|
180
184
|
span[:exception_class] = frame[:exception_class] if frame[:exception_class]
|
|
185
|
+
span[:span_id] = frame[:span_id] if frame[:span_id]
|
|
181
186
|
tags = frame[:tags]
|
|
182
187
|
span[:tags] = tags if tags.is_a?(Hash) && tags.any?
|
|
183
188
|
|
data/lib/dead_bro.rb
CHANGED
|
@@ -30,6 +30,8 @@ module DeadBro
|
|
|
30
30
|
autoload :MemoryDetails, "dead_bro/memory_details"
|
|
31
31
|
autoload :Logger, "dead_bro/logger"
|
|
32
32
|
autoload :WatchTracker, "dead_bro/watch_tracker"
|
|
33
|
+
autoload :SpanTracker, "dead_bro/span_tracker"
|
|
34
|
+
autoload :MiddlewareProbe, "dead_bro/middleware_probe"
|
|
33
35
|
autoload :Sanitizer, "dead_bro/sanitizer"
|
|
34
36
|
begin
|
|
35
37
|
require "dead_bro/railtie"
|
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.38
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Emanuel Comsa
|
|
@@ -51,10 +51,12 @@ files:
|
|
|
51
51
|
- lib/dead_bro/memory_leak_detector.rb
|
|
52
52
|
- lib/dead_bro/memory_phase_tracker.rb
|
|
53
53
|
- lib/dead_bro/memory_tracking_subscriber.rb
|
|
54
|
+
- lib/dead_bro/middleware_probe.rb
|
|
54
55
|
- lib/dead_bro/monitor.rb
|
|
55
56
|
- lib/dead_bro/railtie.rb
|
|
56
57
|
- lib/dead_bro/redis_subscriber.rb
|
|
57
58
|
- lib/dead_bro/sanitizer.rb
|
|
59
|
+
- lib/dead_bro/span_tracker.rb
|
|
58
60
|
- lib/dead_bro/sql_subscriber.rb
|
|
59
61
|
- lib/dead_bro/sql_tracking_middleware.rb
|
|
60
62
|
- lib/dead_bro/subscriber.rb
|