template_streaming 0.0.11 → 0.1.0

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.
@@ -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