-
Notifications
You must be signed in to change notification settings - Fork 8
/
new_relic.rb
555 lines (488 loc) · 18.9 KB
/
new_relic.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
#
# The NewRelic agent won't consider activity outside of the
# controller's #perform_action inside the scope of the request. We
# need to add the rendering that occurs in the response body's #each
# to the stats and traces.
#
# The New Relic agent offers no useful API for this, so we resort to
# fugly, brittle hacks. Hopefully this will improve in a later version
# of the agent.
#
# Here's what we do:
#
# Stats:
#
# * We sandwich the action and view rendering between
# Agent#start_accumulating and Agent#finish_accumulating.
# * During this, the stats for the metric names passed
# to #start_accumulating will be returned from the StatsEngine as
# AccumulatedMethodTraceStats objects. These are stored outside of
# the usual stats table.
# * On #finish_accumulating, the AccumulatedMethodTraceStats
# calculate the accumulated values and add them to the standard
# stats table.
# * We ensure the view stats are in the correct scope by storing the
# metric_frame created during the controller's #perform_action,
# and opening a new metric frame with attributes copied from the
# saved metric frame.
#
# Apdex:
#
# * We stash the first metric frame in the env hash, and tell it not
# to submit apdex values (#hold_apdex). Instead, it records the
# times that would have been used in the apdex calculation.
# * After body.each, we pass the first metric frame to the second to
# accumulate the times for the apdex stat
# (#record_accumulated_apdex)
#
# Histogram:
#
# * We intercept calls to Histogram#process(time) between
# #start_accumulating and #finish_accumulating.
# * On #finish_accumulating, we call the standard Histogram#process
# to add the histogram stat.
# * Because Agent#reset_stats replaces Agent#histogram with a fresh
# instance, and this happens in a second thread outside of a
# critical section, we can't store the accumulating time in the
# histogram. We instead store it in the agent.
#
# Traces:
#
# * We intercept TransactionSampler#notice_scope_empty to stash the
# completed samples in an array of accumulated samples.
# * On #finish_accumulating, we merge the samples into a
# supersample, which replaces the root segments of the accumulated
# samples with one common root segment.
# * The supersample is added to the list for harvesting.
#
# TODO
# ----
#
# * Add support for New Relic developer mode profiling.
#
# Load parts of the agent we need to hack.
def self.expand_load_path_entry(path)
$:.each do |dir|
absolute_path = File.join(dir, path)
return absolute_path if File.exist?(absolute_path)
end
nil
end
require 'new_relic/agent'
# New Relic requires this thing multiple times under different names...
require 'new_relic/agent/instrumentation/metric_frame'
require expand_load_path_entry('new_relic/agent/instrumentation/metric_frame.rb')
require 'new_relic/agent/instrumentation/controller_instrumentation'
module TemplateStreaming
module NewRelic
Error = Class.new(RuntimeError)
# Rack environment keys.
ENV_FRAME_DATA = 'template_streaming.new_relic.frame_data'
ENV_RECORDED_METRICS = 'template_streaming.new_relic.recorded_metrics'
ENV_METRIC_PATH = 'template_streaming.new_relic.metric_path'
ENV_IGNORE_APDEX = 'template_streaming.new_relic.ignore_apdex'
class Middleware
def initialize(app)
@app = app
end
def call(env)
@env = env
status, headers, @body = @app.call(env)
[status, headers, self]
rescue Exception => error
agent.finish_accumulating
raise
end
def each(&block)
in_controller_scope do
@body.each(&block)
end
ensure
agent.finish_accumulating
end
private
def agent
::NewRelic::Agent.instance
end
def in_controller_scope
controller_frame_data = @env[ENV_FRAME_DATA] or
# Didn't hit the action, or do_not_trace was set.
return yield
#return perform_action_with_newrelic_profile(args, &block) if NewRelic::Control.instance.profiling?
# This is based on ControllerInstrumentation#perform_action_with_newrelic_trace.
frame_data = ::NewRelic::Agent::Instrumentation::MetricFrame.current(true)
frame_data.apdex_start = frame_data.start
frame_data.request = controller_frame_data.request
frame_data.push('Controller', @env[ENV_METRIC_PATH])
begin
frame_data.filtered_params = controller_frame_data.filtered_params
::NewRelic::Agent.trace_execution_scoped(@env[ENV_RECORDED_METRICS]) do
begin
frame_data.start_transaction
::NewRelic::Agent::BusyCalculator.dispatcher_start frame_data.start
yield
rescue Exception => e
frame_data.notice_error(e)
raise
end
end
ensure
::NewRelic::Agent::BusyCalculator.dispatcher_finish
frame_data.record_accumulated_apdex(controller_frame_data) unless @env[ENV_IGNORE_APDEX]
frame_data.pop
end
end
end
module Controller
def self.included(base)
base.class_eval do
# Make sure New Relic's hook wraps ours so we have access to the metric frame it sets.
method_name = :perform_action_with_newrelic_trace
method_defined?(method_name) || private_method_defined?(method_name) and
raise "Template Streaming must be loaded before New Relic's controller instrumentation"
alias_method_chain :process, :template_streaming
alias_method_chain :perform_action, :template_streaming
end
end
def process_with_template_streaming(request, response, method = :perform_action, *arguments)
metric_names = ["HttpDispatcher", "Controller/#{newrelic_metric_path(request.parameters['action'])}"]
::NewRelic::Agent.instance.start_accumulating(*metric_names)
process_without_template_streaming(request, response, method, *arguments)
end
def perform_action_with_template_streaming(*args, &block)
unless _is_filtered?('do_not_trace')
frame_data = request.env[ENV_FRAME_DATA] = ::NewRelic::Agent::Instrumentation::MetricFrame.current
frame_data.hold_apdex
# This depends on current scope stack, so stash it too.
request.env[ENV_RECORDED_METRICS] = ::NewRelic::Agent::Instrumentation::MetricFrame.current.recorded_metrics
request.env[ENV_METRIC_PATH] = newrelic_metric_path
request.env[ENV_IGNORE_APDEX] = _is_filtered?('ignore_apdex')
end
perform_action_without_template_streaming(*args, &block)
end
end
module StatsEngine
def self.included(base)
base.class_eval do
alias_method_chain :get_stats_no_scope, :template_streaming
alias_method_chain :get_custom_stats, :template_streaming
alias_method_chain :get_stats, :template_streaming
end
end
#
# Start accumulating the given +metric_names+.
#
# The metric_names can be either strings or MetricSpec's. See
# StatsEngine::MetricStats for which metric names you need to
# accumulate.
#
def start_accumulating(*metric_names)
metric_names.each do |metric_name|
unaccumulated_stats = stats_hash[metric_name] ||= ::NewRelic::MethodTraceStats.new
accumulated_stats = AccumulatedMethodTraceStats.new(unaccumulated_stats)
accumulated_stats_hash[metric_name] ||= accumulated_stats
end
end
#
# Freeze and clear the list of accumulated stats, and add the
# aggregated stats to the unaccumulated stats.
#
def finish_accumulating
accumulated_stats_hash.each do |metric_name, stats|
stats.finish_accumulating
stats.freeze
end
accumulated_stats_hash.clear
end
def get_stats_no_scope_with_template_streaming(metric_name)
accumulated_stats_hash[metric_name] ||
get_stats_no_scope_without_template_streaming(metric_name)
end
def get_custom_stats_with_template_streaming(metric_name, stat_class)
accumulated_stats_hash[metric_name] ||
get_custom_stats_without_template_streaming(metric_name, stat_class)
end
def get_stats_with_template_streaming(metric_name, use_scope = true, scoped_metric_only = false)
key = scoped_metric_only || (use_scope && scope_name && scope_name != metric_name) ?
::NewRelic::MetricSpec.new(metric_name, scope_name) : metric_name
accumulated_stats_hash[key] ||
get_stats_without_template_streaming(metric_name, use_scope, scoped_metric_only)
end
private
def accumulated_stats_hash
@accumulated_stats_hash ||= {}
end
end
#
# An AccumulatedMethodTraceStats is a proxy which aggregates the
# stats given to it, and updates the stats given to it on
# construction when #finish_accumulating is called.
#
# Example:
#
# acc = AccumulatedMethodTraceStats.new(stats)
# acc.trace_call(20, 10)
# acc.trace_call(20, 10)
# acc.finish_accumulating # calls stats.trace_call(40, 20)
#
class AccumulatedMethodTraceStats
def initialize(target_stats)
@target_stats = target_stats
end
def finish_accumulating
if @recorded_data_points
totals = aggregate(@recorded_data_points)
@target_stats.record_data_point(*totals)
end
if @traced_calls
totals = aggregate(@traced_calls)
@target_stats.trace_call(*totals)
end
@record_data_points = @traced_calls = nil
end
def record_data_point(call_time, exclusive_time = call_time)
recorded_data_points << [call_time, exclusive_time]
end
def trace_call(call_time, exclusive_time = call_time)
traced_calls << [call_time, exclusive_time]
end
# No need to aggregate this.
delegate :record_multiple_data_points, :to => '@target_stats'
private
def aggregate(data)
total_call_time = total_exclusive_time = 0
data.each do |call_time, exclusive_time|
total_call_time += call_time
total_exclusive_time += exclusive_time
end
[total_call_time, total_exclusive_time]
end
def recorded_data_points
@recorded_data_points ||= []
end
def traced_calls
@traced_calls ||= []
end
end
module MetricFrame
def self.included(base)
base.alias_method_chain :record_apdex, :template_streaming
end
#
# Tell the MetricFrame to hold on to the times calculated during
# #record_apdex instead of adding the apdex value to the stats.
#
# Call #record_accumulated_apdex on another MetricFrame with
# this frame as an argument to record the total time.
#
def hold_apdex
@hold_apdex = true
end
def record_apdex_with_template_streaming(*args, &block)
return unless recording_web_transaction? && ::NewRelic::Agent.is_execution_traced?
ending = Time.now.to_f
if @hold_apdex
@held_summary_apdex = ending - apdex_start
@held_controller_apdex = ending - start
return
end
record_apdex_without_template_streaming(*args, &block)
end
attr_reader :held_summary_apdex, :held_controller_apdex
def record_accumulated_apdex(*previous_frames)
return unless recording_web_transaction? && ::NewRelic::Agent.is_execution_traced?
ending = Time.now.to_f
total_summary_apdex = previous_frames.map{|frame| frame.held_summary_apdex}.sum
total_controller_apdex = previous_frames.map{|frame| frame.held_controller_apdex}.sum
summary_stat = ::NewRelic::Agent.instance.stats_engine.get_custom_stats("Apdex", ::NewRelic::ApdexStats)
controller_stat = ::NewRelic::Agent.instance.stats_engine.get_custom_stats("Apdex/#{path}", ::NewRelic::ApdexStats)
self.class.update_apdex(summary_stat, total_summary_apdex + ending - apdex_start, exception)
self.class.update_apdex(controller_stat, total_controller_apdex + ending - start, exception)
end
end
module ControllerInstrumentationShim
def self.included(base)
# This shim method takes the wrong number of args. Fix it.
base.module_eval 'def newrelic_metric_path(*args); end', __FILE__, __LINE__ + 1
end
# This is private in the real ControllerInstrumentation module,
# but we need it.
def _is_filtered?(key)
true
end
end
module Histogram
def self.included(base)
base.alias_method_chain :process, :template_streaming
end
def start_accumulating
# Agent#reset_stats replaces #histogram with a fresh one, so
# we can't store accumulating response time in here. Store it
# in the agent instead.
agent.accumulated_histogram_time = 0
end
def finish_accumulating
process_without_template_streaming(agent.accumulated_histogram_time)
agent.accumulated_histogram_time = nil
end
def process_with_template_streaming(response_time)
if agent.accumulated_histogram_time
agent.accumulated_histogram_time += response_time
else
process_without_template_streaming(response_time)
end
end
private
def agent
@agent ||= ::NewRelic::Agent.instance
end
end
module Agent
def start_accumulating(*metric_names)
stats_engine.start_accumulating(*metric_names)
histogram.start_accumulating
transaction_sampler.start_accumulating
end
def finish_accumulating
stats_engine.finish_accumulating
histogram.finish_accumulating
transaction_sampler.finish_accumulating
end
attr_accessor :accumulated_histogram_time
end
module TransactionSampler
def self.included(base)
base.alias_method_chain :notice_scope_empty, :template_streaming
end
def start_accumulating
@accumulated_samples = []
end
def finish_accumulating
supersample = merge_accumulated_samples or
return nil
@accumulated_samples = nil
# Taken from TransactionSampler#notice_scope_empty.
@samples_lock.synchronize do
@last_sample = supersample
@random_sample = @last_sample if @random_sampling
# ensure we don't collect more than a specified number of samples in memory
@samples << @last_sample if ::NewRelic::Control.instance.developer_mode?
@samples.shift while @samples.length > @max_samples
if @slowest_sample.nil? || @slowest_sample.duration < @last_sample.duration
@slowest_sample = @last_sample
end
end
end
def notice_scope_empty_with_template_streaming(time=Time.now.to_f)
if @accumulated_samples
last_builder = builder or
return
last_builder.finish_trace(time)
@accumulated_samples << last_builder.sample
clear_builder
else
notice_scope_empty_without_template_streaming(time)
end
end
private
def merge_accumulated_samples
return nil if @accumulated_samples.empty?
# The RPM transaction trace viewer only shows the first
# segment under the root segment. Move the segment trees of
# subsequent samples under that of the first one.
supersample = @accumulated_samples.shift.dup # samples have been frozen
supersample.incorporate(@accumulated_samples)
supersample
end
end
module TransactionSample
#
# Return a copy of this sample with the segment timestamps all
# incremented by the given delta.
#
# Note that although the returned object is a different
# TransactionSample instance, the segments will be the same
# objects, modified in place. We would modify the
# TransactionSample in place too, only this method is called on
# frozen samples.
#
def bump_by(delta)
root_segment.bump_by(delta)
sample = dup
sample.instance_eval{@start_time += delta}
sample
end
#
# Return the segment under the root.
#
# If the root segment has more than one child, raise an
# error. It appears this is never supposed to happen,
# though--the RPM transaction trace view only ever shows the
# first segment.
#
def subroot_segment
@subroot_segment ||=
begin
(children = @root_segment.called_segments).size == 1 or
raise Error, "multiple top segments found"
children.first
end
end
#
# Put the given samples under this one.
#
# The subroot children of the given samples are moved under this
# sample's subroot.
#
def incorporate(samples)
incorporated_duration = 0.0
samples.each do |sample|
# Bump timestamps by the total length of previous samples.
sample = sample.bump_by(root_segment.duration + incorporated_duration)
incorporated_duration += sample.root_segment.duration
# Merge segments.
sample.subroot_segment.called_segments.each do |segment|
subroot_segment.add_called_segment(segment)
end
# Merge params.
if (request_params = sample.params.delete(:request_params))
params[:request_params].reverse_merge!(request_params)
end
if (custom_params = sample.params.delete(:custom_params))
params[:custom_params] ||= {}
params[:custom_params].reverse_merge!(custom_params)
end
params.reverse_merge!(sample.params)
end
root_segment.exit_timestamp += incorporated_duration
subroot_segment.exit_timestamp += incorporated_duration
end
end
module Segment
attr_reader :called_segments
attr_writer :exit_timestamp
#
# Increment the timestamps by the given delta.
#
def bump_by(delta)
@entry_timestamp += delta
@exit_timestamp += delta
if @called_segments
@called_segments.each do |segment|
segment.bump_by(delta)
end
end
end
end
ActionController::Dispatcher.middleware.insert(0, Middleware)
ActionController::Base.send :include, Controller
::NewRelic::Agent::StatsEngine.send :include, StatsEngine
::NewRelic::Agent::Instrumentation::MetricFrame.send :include, MetricFrame
::NewRelic::Agent::Instrumentation::ControllerInstrumentation::Shim.send :include, ControllerInstrumentationShim
::NewRelic::Histogram.send :include, Histogram
::NewRelic::Agent::Agent.send :include, Agent
::NewRelic::Agent::TransactionSampler.send :include, TransactionSampler
::NewRelic::TransactionSample.send :include, TransactionSample
::NewRelic::TransactionSample::Segment.send :include, Segment
end
end