template_streaming 0.0.11 → 0.1.0

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