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 +4 -4
- data/CHANGELOG.md +23 -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/job_subscriber.rb +2 -0
- 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 +39 -20
- data/lib/dead_bro/version.rb +1 -2
- data/lib/dead_bro/watch_tracker.rb +5 -0
- data/lib/dead_bro.rb +2 -0
- metadata +3 -1
|
@@ -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)
|