techarch-newrelic_rpm 2.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) hide show
  1. data/CHANGELOG +335 -0
  2. data/LICENSE +37 -0
  3. data/Manifest +159 -0
  4. data/README.md +138 -0
  5. data/Rakefile +22 -0
  6. data/bin/mongrel_rpm +33 -0
  7. data/bin/newrelic_cmd +4 -0
  8. data/cert/cacert.pem +34 -0
  9. data/init.rb +38 -0
  10. data/install.rb +45 -0
  11. data/lib/new_relic/agent.rb +281 -0
  12. data/lib/new_relic/agent/agent.rb +636 -0
  13. data/lib/new_relic/agent/chained_call.rb +13 -0
  14. data/lib/new_relic/agent/collection_helper.rb +66 -0
  15. data/lib/new_relic/agent/error_collector.rb +121 -0
  16. data/lib/new_relic/agent/instrumentation/active_merchant.rb +18 -0
  17. data/lib/new_relic/agent/instrumentation/active_record_instrumentation.rb +88 -0
  18. data/lib/new_relic/agent/instrumentation/authlogic.rb +8 -0
  19. data/lib/new_relic/agent/instrumentation/controller_instrumentation.rb +437 -0
  20. data/lib/new_relic/agent/instrumentation/data_mapper.rb +90 -0
  21. data/lib/new_relic/agent/instrumentation/dispatcher_instrumentation.rb +167 -0
  22. data/lib/new_relic/agent/instrumentation/memcache.rb +24 -0
  23. data/lib/new_relic/agent/instrumentation/merb/controller.rb +26 -0
  24. data/lib/new_relic/agent/instrumentation/merb/dispatcher.rb +13 -0
  25. data/lib/new_relic/agent/instrumentation/merb/errors.rb +8 -0
  26. data/lib/new_relic/agent/instrumentation/net.rb +23 -0
  27. data/lib/new_relic/agent/instrumentation/passenger_instrumentation.rb +20 -0
  28. data/lib/new_relic/agent/instrumentation/rack.rb +108 -0
  29. data/lib/new_relic/agent/instrumentation/rails/action_controller.rb +59 -0
  30. data/lib/new_relic/agent/instrumentation/rails/action_web_service.rb +27 -0
  31. data/lib/new_relic/agent/instrumentation/rails/dispatcher.rb +41 -0
  32. data/lib/new_relic/agent/instrumentation/rails/errors.rb +27 -0
  33. data/lib/new_relic/agent/instrumentation/sinatra.rb +39 -0
  34. data/lib/new_relic/agent/method_tracer.rb +349 -0
  35. data/lib/new_relic/agent/patch_const_missing.rb +125 -0
  36. data/lib/new_relic/agent/sampler.rb +12 -0
  37. data/lib/new_relic/agent/samplers/cpu_sampler.rb +49 -0
  38. data/lib/new_relic/agent/samplers/memory_sampler.rb +138 -0
  39. data/lib/new_relic/agent/samplers/mongrel_sampler.rb +22 -0
  40. data/lib/new_relic/agent/shim_agent.rb +21 -0
  41. data/lib/new_relic/agent/stats_engine.rb +22 -0
  42. data/lib/new_relic/agent/stats_engine/metric_stats.rb +111 -0
  43. data/lib/new_relic/agent/stats_engine/samplers.rb +71 -0
  44. data/lib/new_relic/agent/stats_engine/transactions.rb +152 -0
  45. data/lib/new_relic/agent/transaction_sampler.rb +310 -0
  46. data/lib/new_relic/agent/worker_loop.rb +118 -0
  47. data/lib/new_relic/commands/deployments.rb +145 -0
  48. data/lib/new_relic/commands/new_relic_commands.rb +30 -0
  49. data/lib/new_relic/control.rb +473 -0
  50. data/lib/new_relic/control/external.rb +13 -0
  51. data/lib/new_relic/control/merb.rb +22 -0
  52. data/lib/new_relic/control/rails.rb +145 -0
  53. data/lib/new_relic/control/ruby.rb +36 -0
  54. data/lib/new_relic/control/sinatra.rb +14 -0
  55. data/lib/new_relic/histogram.rb +89 -0
  56. data/lib/new_relic/local_environment.rb +328 -0
  57. data/lib/new_relic/merbtasks.rb +6 -0
  58. data/lib/new_relic/metric_data.rb +42 -0
  59. data/lib/new_relic/metric_parser.rb +124 -0
  60. data/lib/new_relic/metric_parser/action_mailer.rb +9 -0
  61. data/lib/new_relic/metric_parser/active_merchant.rb +26 -0
  62. data/lib/new_relic/metric_parser/active_record.rb +25 -0
  63. data/lib/new_relic/metric_parser/controller.rb +54 -0
  64. data/lib/new_relic/metric_parser/controller_cpu.rb +38 -0
  65. data/lib/new_relic/metric_parser/errors.rb +6 -0
  66. data/lib/new_relic/metric_parser/external.rb +50 -0
  67. data/lib/new_relic/metric_parser/mem_cache.rb +12 -0
  68. data/lib/new_relic/metric_parser/view.rb +61 -0
  69. data/lib/new_relic/metric_parser/web_frontend.rb +14 -0
  70. data/lib/new_relic/metric_parser/web_service.rb +9 -0
  71. data/lib/new_relic/metric_spec.rb +52 -0
  72. data/lib/new_relic/metrics.rb +7 -0
  73. data/lib/new_relic/noticed_error.rb +23 -0
  74. data/lib/new_relic/rack/metric_app.rb +56 -0
  75. data/lib/new_relic/rack/newrelic.ru +25 -0
  76. data/lib/new_relic/rack/newrelic.yml +25 -0
  77. data/lib/new_relic/rack_app.rb +5 -0
  78. data/lib/new_relic/recipes.rb +82 -0
  79. data/lib/new_relic/stats.rb +361 -0
  80. data/lib/new_relic/transaction_analysis.rb +121 -0
  81. data/lib/new_relic/transaction_sample.rb +666 -0
  82. data/lib/new_relic/version.rb +54 -0
  83. data/lib/new_relic_api.rb +313 -0
  84. data/lib/newrelic_rpm.rb +40 -0
  85. data/lib/tasks/all.rb +4 -0
  86. data/lib/tasks/install.rake +7 -0
  87. data/lib/tasks/tests.rake +13 -0
  88. data/newrelic.yml +227 -0
  89. data/recipes/newrelic.rb +6 -0
  90. data/techarch-newrelic_rpm.gemspec +32 -0
  91. data/test/active_record_fixtures.rb +55 -0
  92. data/test/config/newrelic.yml +46 -0
  93. data/test/config/test_control.rb +39 -0
  94. data/test/new_relic/agent/active_record_instrumentation_test.rb +264 -0
  95. data/test/new_relic/agent/agent_controller_test.rb +107 -0
  96. data/test/new_relic/agent/agent_test.rb +119 -0
  97. data/test/new_relic/agent/agent_test_controller.rb +44 -0
  98. data/test/new_relic/agent/classloader_patch_test.rb +56 -0
  99. data/test/new_relic/agent/collection_helper_test.rb +125 -0
  100. data/test/new_relic/agent/dispatcher_instrumentation_test.rb +76 -0
  101. data/test/new_relic/agent/error_collector_test.rb +172 -0
  102. data/test/new_relic/agent/method_tracer_test.rb +340 -0
  103. data/test/new_relic/agent/metric_data_test.rb +56 -0
  104. data/test/new_relic/agent/mock_ar_connection.rb +40 -0
  105. data/test/new_relic/agent/mock_scope_listener.rb +23 -0
  106. data/test/new_relic/agent/net_instrumentation_test.rb +63 -0
  107. data/test/new_relic/agent/stats_engine/metric_stats_test.rb +79 -0
  108. data/test/new_relic/agent/stats_engine/samplers_test.rb +81 -0
  109. data/test/new_relic/agent/stats_engine/stats_engine_test.rb +184 -0
  110. data/test/new_relic/agent/task_instrumentation_test.rb +126 -0
  111. data/test/new_relic/agent/testable_agent.rb +13 -0
  112. data/test/new_relic/agent/transaction_sample_builder_test.rb +195 -0
  113. data/test/new_relic/agent/transaction_sample_test.rb +186 -0
  114. data/test/new_relic/agent/transaction_sampler_test.rb +385 -0
  115. data/test/new_relic/agent/worker_loop_test.rb +103 -0
  116. data/test/new_relic/control_test.rb +113 -0
  117. data/test/new_relic/deployments_api_test.rb +68 -0
  118. data/test/new_relic/environment_test.rb +75 -0
  119. data/test/new_relic/metric_parser_test.rb +172 -0
  120. data/test/new_relic/metric_spec_test.rb +177 -0
  121. data/test/new_relic/shim_agent_test.rb +9 -0
  122. data/test/new_relic/stats_test.rb +291 -0
  123. data/test/new_relic/version_number_test.rb +74 -0
  124. data/test/test_helper.rb +38 -0
  125. data/test/ui/newrelic_controller_test.rb +14 -0
  126. data/test/ui/newrelic_helper_test.rb +53 -0
  127. data/ui/controllers/newrelic_controller.rb +220 -0
  128. data/ui/helpers/google_pie_chart.rb +55 -0
  129. data/ui/helpers/newrelic_helper.rb +317 -0
  130. data/ui/views/layouts/newrelic_default.rhtml +47 -0
  131. data/ui/views/newrelic/_explain_plans.rhtml +27 -0
  132. data/ui/views/newrelic/_sample.rhtml +19 -0
  133. data/ui/views/newrelic/_segment.rhtml +28 -0
  134. data/ui/views/newrelic/_segment_limit_message.rhtml +1 -0
  135. data/ui/views/newrelic/_segment_row.rhtml +14 -0
  136. data/ui/views/newrelic/_show_sample_detail.rhtml +24 -0
  137. data/ui/views/newrelic/_show_sample_sql.rhtml +20 -0
  138. data/ui/views/newrelic/_show_sample_summary.rhtml +3 -0
  139. data/ui/views/newrelic/_sql_row.rhtml +11 -0
  140. data/ui/views/newrelic/_stack_trace.rhtml +30 -0
  141. data/ui/views/newrelic/_table.rhtml +12 -0
  142. data/ui/views/newrelic/explain_sql.rhtml +42 -0
  143. data/ui/views/newrelic/images/arrow-close.png +0 -0
  144. data/ui/views/newrelic/images/arrow-open.png +0 -0
  145. data/ui/views/newrelic/images/blue_bar.gif +0 -0
  146. data/ui/views/newrelic/images/file_icon.png +0 -0
  147. data/ui/views/newrelic/images/gray_bar.gif +0 -0
  148. data/ui/views/newrelic/images/new_relic_rpm_desktop.gif +0 -0
  149. data/ui/views/newrelic/images/textmate.png +0 -0
  150. data/ui/views/newrelic/index.rhtml +57 -0
  151. data/ui/views/newrelic/javascript/prototype-scriptaculous.js +7288 -0
  152. data/ui/views/newrelic/javascript/transaction_sample.js +107 -0
  153. data/ui/views/newrelic/sample_not_found.rhtml +2 -0
  154. data/ui/views/newrelic/show_sample.rhtml +80 -0
  155. data/ui/views/newrelic/show_source.rhtml +3 -0
  156. data/ui/views/newrelic/stylesheets/style.css +484 -0
  157. data/ui/views/newrelic/threads.rhtml +52 -0
  158. metadata +330 -0
@@ -0,0 +1,121 @@
1
+ # Add these methods to TransactionSample that enable performance analysis in the user interface.
2
+ module NewRelic::TransactionAnalysis
3
+ def database_time
4
+ time_percentage(/^Database\/.*/)
5
+ end
6
+
7
+ def render_time
8
+ time_percentage(/^View\/.*/)
9
+ end
10
+
11
+ # summarizes performance data for all calls to segments
12
+ # with the same metric_name
13
+ class SegmentSummary
14
+ attr_accessor :metric_name, :total_time, :exclusive_time, :call_count
15
+ def initialize(metric_name, sample)
16
+ @metric_name = metric_name
17
+ @total_time, @exclusive_time, @call_count = 0,0,0
18
+ @sample = sample
19
+ end
20
+
21
+ def <<(segment)
22
+ if metric_name != segment.metric_name
23
+ raise ArgumentError, "Metric Name Mismatch: #{segment.metric_name} != #{metric_name}"
24
+ end
25
+
26
+ @total_time += segment.duration
27
+ @exclusive_time += segment.exclusive_duration
28
+ @call_count += 1
29
+ end
30
+
31
+ def average_time
32
+ @total_time / @call_count
33
+ end
34
+
35
+ def average_exclusive_time
36
+ @exclusive_time / @call_count
37
+ end
38
+
39
+ def exclusive_time_percentage
40
+ return 0 unless @exclusive_time && @sample.duration && @sample.duration > 0
41
+ @exclusive_time / @sample.duration
42
+ end
43
+
44
+ def total_time_percentage
45
+ return 0 unless @total_time && @sample.duration && @sample.duration > 0
46
+ @total_time / @sample.duration
47
+ end
48
+
49
+ def developer_name
50
+ return @metric_name if @metric_name == 'Remainder'
51
+ NewRelic::MetricParser.parse(@metric_name).developer_name
52
+ end
53
+ end
54
+
55
+ # return the data that breaks down the performance of the transaction
56
+ # as an array of SegmentSummary objects. If a limit is specified, then
57
+ # limit the data set to the top n
58
+ def breakdown_data(limit = nil)
59
+ metric_hash = {}
60
+ each_segment do |segment|
61
+ unless segment == root_segment
62
+ metric_name = segment.metric_name
63
+ metric_hash[metric_name] ||= SegmentSummary.new(metric_name, self)
64
+ metric_hash[metric_name] << segment
65
+ end
66
+ end
67
+
68
+ data = metric_hash.values
69
+
70
+ data.sort! do |x,y|
71
+ y.exclusive_time <=> x.exclusive_time
72
+ end
73
+
74
+ if limit && data.length > limit
75
+ data = data[0..limit - 1]
76
+ end
77
+
78
+ # add one last segment for the remaining time if any
79
+ remainder = duration
80
+ data.each do |segment|
81
+ remainder -= segment.exclusive_time
82
+ end
83
+
84
+ if (remainder*1000).round > 0
85
+ remainder_summary = SegmentSummary.new('Remainder', self)
86
+ remainder_summary.total_time = remainder_summary.exclusive_time = remainder
87
+ remainder_summary.call_count = 1
88
+ data << remainder_summary
89
+ end
90
+
91
+ data
92
+ end
93
+
94
+ # return an array of sql statements executed by this transaction
95
+ # each element in the array contains [sql, parent_segment_metric_name, duration]
96
+ def sql_segments
97
+ segments = []
98
+ each_segment do |segment|
99
+ if segment[:sql]
100
+ segments << segment
101
+ end
102
+ if segment[:sql_obfuscated]
103
+ segments << segment
104
+ end
105
+ end
106
+ segments
107
+ end
108
+
109
+ private
110
+ def time_percentage(regex)
111
+ total = 0
112
+ each_segment do |segment|
113
+ if regex =~ segment.metric_name
114
+ total += segment.duration
115
+ end
116
+ end
117
+ fraction = 100.0 * total / duration
118
+ # percent value rounded to two digits:
119
+ return (100 * fraction).round / 100.0
120
+ end
121
+ end
@@ -0,0 +1,666 @@
1
+
2
+ module NewRelic
3
+ COLLAPSE_SEGMENTS_THRESHOLD = 2
4
+
5
+ MYSQL_EXPLAIN_COLUMNS = [
6
+ "Id",
7
+ "Select Type",
8
+ "Table",
9
+ "Type",
10
+ "Possible Keys",
11
+ "Key",
12
+ "Key Length",
13
+ "Ref",
14
+ "Rows",
15
+ "Extra"
16
+ ].freeze
17
+
18
+ class TransactionSample
19
+ EMPTY_ARRAY = [].freeze
20
+
21
+ @@start_time = Time.now
22
+
23
+ include TransactionAnalysis
24
+ class Segment
25
+
26
+
27
+ attr_reader :entry_timestamp
28
+ # The exit timestamp will be relative except for the outermost sample which will
29
+ # have a timestamp.
30
+ attr_reader :exit_timestamp
31
+ attr_reader :parent_segment
32
+ attr_reader :metric_name
33
+ attr_reader :segment_id
34
+
35
+ def initialize(timestamp, metric_name, segment_id)
36
+ @entry_timestamp = timestamp
37
+ @metric_name = metric_name || '<unknown>'
38
+ @segment_id = segment_id || object_id
39
+ end
40
+
41
+ def end_trace(timestamp)
42
+ @exit_timestamp = timestamp
43
+ end
44
+
45
+ def add_called_segment(s)
46
+ @called_segments ||= []
47
+ @called_segments << s
48
+ s.parent_segment = self
49
+ end
50
+
51
+ def to_s
52
+ to_debug_str(0)
53
+ end
54
+
55
+ def to_json
56
+ map = {:entry_timestamp => @entry_timestamp,
57
+ :exit_timestamp => @exit_timestamp,
58
+ :metric_name => @metric_name,
59
+ :segment_id => @segment_id}
60
+ if @called_segments && !@called_segments.empty?
61
+ map[:called_segments] = @called_segments
62
+ end
63
+ if @params && !@params.empty?
64
+ map[:params] = @params
65
+ end
66
+ map.to_json
67
+ end
68
+
69
+ def self.from_json(json, id_generator)
70
+ json = ActiveSupport::JSON.decode(json) if json.is_a?(String)
71
+ if json.is_a?(Array)
72
+ entry_timestamp = json[0].to_f / 1000
73
+ exit_timestamp = json[1].to_f / 1000
74
+ metric_name = json[2]
75
+ params = json[3]
76
+ called_segments = json[4]
77
+ else
78
+ entry_timestamp = json["entry_timestamp"].to_f
79
+ exit_timestamp = json["exit_timestamp"].to_f
80
+ metric_name = json["metric_name"]
81
+ params = json["params"]
82
+
83
+ called_segments = json["called_segments"]
84
+ end
85
+ segment = Segment.new(entry_timestamp, metric_name, id_generator.next_id)
86
+ segment.end_trace exit_timestamp
87
+ if params
88
+ segment.send :params=, HashWithIndifferentAccess.new(params)
89
+ end
90
+ if called_segments
91
+ called_segments.each do |child|
92
+ segment.add_called_segment(self.from_json(child, id_generator))
93
+ end
94
+ end
95
+ segment
96
+ end
97
+
98
+ def path_string
99
+ "#{metric_name}[#{called_segments.collect {|segment| segment.path_string }.join('')}]"
100
+ end
101
+ def to_s_compact
102
+ str = ""
103
+ str << metric_name
104
+ if called_segments.any?
105
+ str << "{#{called_segments.map { | cs | cs.to_s_compact }.join(",")}}"
106
+ end
107
+ str
108
+ end
109
+ def to_debug_str(depth)
110
+ tab = ""
111
+ depth.times {tab << " "}
112
+
113
+ s = tab.clone
114
+ s << ">> #{metric_name}: #{(@entry_timestamp*1000).round}\n"
115
+ unless params.empty?
116
+ s << "#{tab}#{tab}{\n"
117
+ params.each do |k,v|
118
+ s << "#{tab}#{tab}#{k}: #{v}\n"
119
+ end
120
+ s << "#{tab}#{tab}}\n"
121
+ end
122
+ called_segments.each do |cs|
123
+ s << cs.to_debug_str(depth + 1)
124
+ end
125
+ s << tab
126
+ s << "<< #{metric_name}: "
127
+ s << case @exit_timestamp
128
+ when nil then 'n/a'
129
+ when Numeric then (@exit_timestamp*1000).round.to_s
130
+ else @exit_timestamp.to_s
131
+ end << "\n"
132
+ end
133
+
134
+ def called_segments
135
+ @called_segments || EMPTY_ARRAY
136
+ end
137
+
138
+ def freeze
139
+ params.freeze
140
+ if @called_segments
141
+ @called_segments.each do |s|
142
+ s.freeze
143
+ end
144
+ end
145
+ super
146
+ end
147
+
148
+ # return the total duration of this segment
149
+ def duration
150
+ (@exit_timestamp - @entry_timestamp).to_f
151
+ end
152
+
153
+ # return the duration of this segment without
154
+ # including the time in the called segments
155
+ def exclusive_duration
156
+ d = duration
157
+
158
+ if @called_segments
159
+ @called_segments.each do |segment|
160
+ d -= segment.duration
161
+ end
162
+ end
163
+ d
164
+ end
165
+ def count_segments
166
+ count = 1
167
+ @called_segments.each { | seg | count += seg.count_segments } if @called_segments
168
+ count
169
+ end
170
+ # Walk through the tree and truncate the segments
171
+ def truncate(max)
172
+ return max unless @called_segments
173
+ i = 0
174
+ @called_segments.each do | segment |
175
+ max = segment.truncate(max)
176
+ max -= 1
177
+ if max <= 0
178
+ @called_segments = @called_segments[0..i]
179
+ break
180
+ else
181
+ i += 1
182
+ end
183
+ end
184
+ max
185
+ end
186
+
187
+ def []=(key, value)
188
+ # only create a parameters field if a parameter is set; this will save
189
+ # bandwidth etc as most segments have no parameters
190
+ params[key] = value
191
+ end
192
+
193
+ def [](key)
194
+ params[key]
195
+ end
196
+
197
+ def params
198
+ @params ||= {}
199
+ end
200
+
201
+ # call the provided block for this segment and each
202
+ # of the called segments
203
+ def each_segment(&block)
204
+ block.call self
205
+
206
+ if @called_segments
207
+ @called_segments.each do |segment|
208
+ segment.each_segment(&block)
209
+ end
210
+ end
211
+ end
212
+
213
+ def find_segment(id)
214
+ return self if @segment_id == id
215
+ called_segments.each do |segment|
216
+ found = segment.find_segment(id)
217
+ return found if found
218
+ end
219
+ nil
220
+ end
221
+
222
+ # perform this in the runtime environment of a managed application, to explain the sql
223
+ # statement(s) executed within a segment of a transaction sample.
224
+ # returns an array of explanations (which is an array rows consisting of
225
+ # an array of strings for each column returned by the the explain query)
226
+ # Note this happens only for statements whose execution time exceeds a threshold (e.g. 500ms)
227
+ # and only within the slowest transaction in a report period, selected for shipment to RPM
228
+ def explain_sql
229
+ sql = params[:sql]
230
+ return nil unless sql && params[:connection_config]
231
+ statements = sql.split(";\n")
232
+ explanations = []
233
+ statements.each do |statement|
234
+ if statement.split($;, 2)[0].upcase == 'SELECT'
235
+ explain_resultset = []
236
+ begin
237
+ connection = NewRelic::TransactionSample.get_connection(params[:connection_config])
238
+ if connection
239
+ # The resultset type varies for different drivers. Only thing you can count on is
240
+ # that it implements each. Also: can't use select_rows because the native postgres
241
+ # driver doesn't know that method.
242
+ explain_resultset = connection.execute("EXPLAIN #{statement}") if connection
243
+ rows = []
244
+ # Note: we can't use map.
245
+ # Note: have to convert from native column element types to string so we can
246
+ # serialize. Esp. for postgresql.
247
+ # Can't use map. Suck it up.
248
+ if explain_resultset.respond_to?(:each)
249
+ explain_resultset.each { | row | rows << row.map(&:to_s) }
250
+ else
251
+ rows << [ explain_resultset ]
252
+ end
253
+ explanations << rows
254
+ # sleep for a very short period of time in order to yield to the main thread
255
+ # this is because a remote database call will likely hang the VM
256
+ sleep 0.05
257
+ end
258
+ rescue => e
259
+ handle_exception_in_explain(e)
260
+ end
261
+ end
262
+ end
263
+
264
+ explanations
265
+ end
266
+
267
+ def handle_exception_in_explain(e)
268
+ x = 1 # this is here so that code coverage knows we've entered this block
269
+ # swallow failed attempts to run an explain. One example of a failure is the
270
+ # connection for the sql statement is to a different db than the default connection
271
+ # specified in AR::Base
272
+ end
273
+ def obfuscated_sql
274
+ TransactionSample.obfuscate_sql(params[:sql])
275
+ end
276
+
277
+ def called_segments=(segments)
278
+ @called_segments = segments
279
+ end
280
+
281
+ protected
282
+ def parent_segment=(s)
283
+ @parent_segment = s
284
+ end
285
+ def params=(p)
286
+ @params = p
287
+ end
288
+ end
289
+
290
+ class SummarySegment < Segment
291
+
292
+
293
+ def initialize(segment)
294
+ super segment.entry_timestamp, segment.metric_name, nil
295
+
296
+ add_segments segment.called_segments
297
+
298
+ end_trace segment.exit_timestamp
299
+ end
300
+
301
+ def add_segments(segments)
302
+ segments.collect do |segment|
303
+ SummarySegment.new(segment)
304
+ end.each {|segment| add_called_segment(segment)}
305
+ end
306
+
307
+ end
308
+
309
+ class CompositeSegment < Segment
310
+ attr_reader :detail_segments
311
+
312
+ def initialize(segments)
313
+ summary = SummarySegment.new(segments.first)
314
+ super summary.entry_timestamp, "Repeating pattern (#{segments.length} repeats)", nil
315
+
316
+ summary.end_trace(segments.last.exit_timestamp)
317
+
318
+ @detail_segments = segments.clone
319
+
320
+ add_called_segment(summary)
321
+ end_trace summary.exit_timestamp
322
+ end
323
+
324
+ def detail_segments=(segments)
325
+ @detail_segments = segments
326
+ end
327
+
328
+ end
329
+
330
+ class << self
331
+ def obfuscate_sql(sql)
332
+ NewRelic::Agent.instance.obfuscator.call(sql)
333
+ end
334
+
335
+
336
+ def get_connection(config)
337
+ @@connections ||= {}
338
+
339
+ connection = @@connections[config]
340
+
341
+ return connection if connection
342
+
343
+ begin
344
+ connection = ActiveRecord::Base.send("#{config[:adapter]}_connection", config)
345
+ @@connections[config] = connection
346
+ rescue => e
347
+ NewRelic::Agent.agent.log.error("Caught exception #{e} trying to get connection to DB for explain. Control: #{config}")
348
+ NewRelic::Agent.agent.log.error(e.backtrace.join("\n"))
349
+ nil
350
+ end
351
+ end
352
+
353
+ def close_connections
354
+ @@connections ||= {}
355
+ @@connections.values.each do |connection|
356
+ begin
357
+ connection.disconnect!
358
+ rescue
359
+ end
360
+ end
361
+
362
+ @@connections = {}
363
+ end
364
+
365
+ end
366
+
367
+ attr_accessor :profile
368
+ attr_reader :root_segment
369
+ attr_reader :params
370
+ attr_reader :sample_id
371
+
372
+ def initialize(time = Time.now.to_f, sample_id = nil)
373
+ @sample_id = sample_id || object_id
374
+ @start_time = time
375
+ @root_segment = create_segment 0.0, "ROOT"
376
+ @params = {}
377
+ @params[:request_params] = {}
378
+ end
379
+
380
+ def count_segments
381
+ @root_segment.count_segments - 1 # don't count the root segment
382
+ end
383
+
384
+ def truncate(max)
385
+ original_count = count_segments
386
+
387
+ return if original_count <= max
388
+
389
+ @root_segment.truncate(max-1)
390
+
391
+ if params[:segment_count].nil?
392
+ params[:segment_count] = original_count
393
+ end
394
+ end
395
+
396
+ # offset from start of app
397
+ def timestamp
398
+ @start_time - @@start_time.to_f
399
+ end
400
+
401
+ def to_json(options = {})
402
+ map = {:sample_id => @sample_id,
403
+ :start_time => @start_time,
404
+ :root_segment => @root_segment}
405
+ if @params && !@params.empty?
406
+ map[:params] = @params
407
+ end
408
+ map.to_json
409
+ end
410
+
411
+ def self.from_json(json)
412
+ json = ActiveSupport::JSON.decode(json) if json.is_a?(String)
413
+
414
+ if json.is_a?(Array)
415
+ start_time = json[0].to_f / 1000
416
+ custom_params = HashWithIndifferentAccess.new(json[2])
417
+ params = {:request_params => HashWithIndifferentAccess.new(json[1]),
418
+ :custom_params => custom_params}
419
+ cpu_time = custom_params.delete(:cpu_time)
420
+ sample_id = nil
421
+ params[:cpu_time] = cpu_time.to_f / 1000 if cpu_time
422
+ root = json[3]
423
+ else
424
+ start_time = json["start_time"].to_f
425
+ sample_id = json["sample_id"].to_i
426
+ params = json["params"]
427
+ root = json["root_segment"]
428
+ end
429
+
430
+ sample = TransactionSample.new(start_time, sample_id)
431
+
432
+ if params
433
+ sample.send :params=, HashWithIndifferentAccess.new(params)
434
+ end
435
+ if root
436
+ sample.send :root_segment=, Segment.from_json(root, IDGenerator.new)
437
+ end
438
+ sample
439
+ end
440
+
441
+ def start_time
442
+ Time.at(@start_time)
443
+ end
444
+
445
+ def path_string
446
+ @root_segment.path_string
447
+ end
448
+
449
+ def create_segment(relative_timestamp, metric_name, segment_id = nil)
450
+ raise TypeError.new("Frozen Transaction Sample") if frozen?
451
+ NewRelic::TransactionSample::Segment.new(relative_timestamp, metric_name, segment_id)
452
+ end
453
+
454
+ def freeze
455
+ @root_segment.freeze
456
+ super
457
+ end
458
+
459
+ def duration
460
+ root_segment.duration
461
+ end
462
+
463
+ def each_segment(&block)
464
+ @root_segment.each_segment(&block)
465
+ end
466
+
467
+ def to_s_compact
468
+ @root_segment.to_s_compact
469
+ end
470
+
471
+ def find_segment(id)
472
+ @root_segment.find_segment(id)
473
+ end
474
+
475
+ def to_s
476
+ s = "Transaction Sample collected at #{start_time}\n"
477
+ s << " {\n"
478
+ s << " Path: #{params[:path]} \n"
479
+
480
+ params.each do |k,v|
481
+ next if k == :path
482
+ s << " #{k}: " <<
483
+ case v
484
+ when Enumerable then v.map(&:to_s).sort.join("; ")
485
+ when String then v
486
+ when Float then '%6.3s' % v
487
+ else
488
+ raise "unexpected value type for #{k}: '#{v}' (#{v.class})"
489
+ end << "\n"
490
+ end
491
+ s << " }\n\n"
492
+ s << @root_segment.to_debug_str(0)
493
+ end
494
+
495
+ # return a new transaction sample that treats segments
496
+ # with the given regular expression in their name as if they
497
+ # were never called at all. This allows us to strip out segments
498
+ # from traces captured in development environment that would not
499
+ # normally show up in production (like Rails/Application Code Loading)
500
+ def omit_segments_with(regex)
501
+ regex = Regexp.new(regex)
502
+
503
+ sample = TransactionSample.new(@start_time, sample_id)
504
+
505
+ params.each {|k,v| sample.params[k] = v}
506
+
507
+ delta = build_segment_with_omissions(sample, 0.0, @root_segment, sample.root_segment, regex)
508
+ sample.root_segment.end_trace(@root_segment.exit_timestamp - delta)
509
+ sample.profile = self.profile
510
+ sample.freeze
511
+ end
512
+
513
+ # return a new transaction sample that can be sent to the RPM service.
514
+ # this involves potentially one or more of the following options
515
+ # :explain_sql : run EXPLAIN on all queries whose response times equal the value for this key
516
+ # (for example :explain_sql => 2.0 would explain everything over 2 seconds. 0.0 would explain everything.)
517
+ # :keep_backtraces : keep backtraces, significantly increasing size of trace (off by default)
518
+ # :obfuscate_sql : clear sql fields of potentially sensitive values (higher overhead, better security)
519
+ def prepare_to_send(options={})
520
+ sample = TransactionSample.new(@start_time, sample_id)
521
+
522
+ sample.params.merge! self.params
523
+
524
+ begin
525
+ build_segment_for_transfer(sample, @root_segment, sample.root_segment, options)
526
+ ensure
527
+ self.class.close_connections
528
+ end
529
+
530
+ sample.root_segment.end_trace(@root_segment.exit_timestamp)
531
+ sample.freeze
532
+ end
533
+
534
+ def analyze
535
+ sample = self
536
+ original_path_string = nil
537
+ loop do
538
+ original_path_string = sample.path_string.to_s
539
+ new_sample = sample.dup
540
+ new_sample.root_segment = sample.root_segment.dup
541
+ new_sample.root_segment.called_segments = analyze_called_segments(root_segment.called_segments)
542
+ sample = new_sample
543
+ return sample if sample.path_string.to_s == original_path_string
544
+ end
545
+
546
+ end
547
+
548
+ protected
549
+ def root_segment=(segment)
550
+ @root_segment = segment
551
+ end
552
+ def params=(params)
553
+ @params = params
554
+ end
555
+
556
+ private
557
+
558
+ def analyze_called_segments(called_segments)
559
+ path = nil
560
+ like_segments = []
561
+
562
+ segments = []
563
+
564
+ called_segments.each do |segment|
565
+ segment = segment.dup
566
+ segment.called_segments = analyze_called_segments(segment.called_segments)
567
+
568
+ current_path = segment.path_string
569
+ if path == current_path
570
+ like_segments << segment
571
+ else
572
+ segments += summarize_segments(like_segments)
573
+
574
+ like_segments.clear
575
+ like_segments << segment
576
+ path = current_path
577
+ end
578
+ end
579
+ segments += summarize_segments(like_segments)
580
+
581
+ segments
582
+ end
583
+
584
+ def summarize_segments(like_segments)
585
+ if like_segments.length > COLLAPSE_SEGMENTS_THRESHOLD
586
+ [CompositeSegment.new(like_segments)]
587
+ else
588
+ like_segments
589
+ end
590
+ end
591
+
592
+ def build_segment_with_omissions(new_sample, time_delta, source_segment, target_segment, regex)
593
+ source_segment.called_segments.each do |source_called_segment|
594
+ # if this segment's metric name matches the given regular expression, bail
595
+ # here and increase the amount of time that we reduce the target sample with
596
+ # by this omitted segment's duration.
597
+ do_omit = regex =~ source_called_segment.metric_name
598
+
599
+ if do_omit
600
+ time_delta += source_called_segment.duration
601
+ else
602
+ target_called_segment = new_sample.create_segment(
603
+ source_called_segment.entry_timestamp - time_delta,
604
+ source_called_segment.metric_name,
605
+ source_called_segment.segment_id)
606
+
607
+ target_segment.add_called_segment target_called_segment
608
+ source_called_segment.params.each do |k,v|
609
+ target_called_segment[k]=v
610
+ end
611
+
612
+ time_delta = build_segment_with_omissions(
613
+ new_sample, time_delta, source_called_segment, target_called_segment, regex)
614
+ target_called_segment.end_trace(source_called_segment.exit_timestamp - time_delta)
615
+ end
616
+ end
617
+
618
+ return time_delta
619
+ end
620
+
621
+ # see prepare_to_send for what we do with options
622
+ def build_segment_for_transfer(new_sample, source_segment, target_segment, options)
623
+ source_segment.called_segments.each do |source_called_segment|
624
+ target_called_segment = new_sample.create_segment(
625
+ source_called_segment.entry_timestamp,
626
+ source_called_segment.metric_name,
627
+ source_called_segment.segment_id)
628
+
629
+ target_segment.add_called_segment target_called_segment
630
+ source_called_segment.params.each do |k,v|
631
+ case k
632
+ when :backtrace
633
+ target_called_segment[k]=v if options[:keep_backtraces]
634
+ when :sql
635
+ sql = v
636
+
637
+ # run an EXPLAIN on this sql if specified.
638
+ if options[:explain_enabled] && options[:explain_sql] && source_called_segment.duration > options[:explain_sql].to_f
639
+ target_called_segment[:explanation] = source_called_segment.explain_sql
640
+ end
641
+
642
+ target_called_segment[:sql]=sql if options[:record_sql] == :raw
643
+ target_called_segment[:sql_obfuscated] = TransactionSample.obfuscate_sql(sql) if options[:record_sql] == :obfuscated
644
+ when :connection_config
645
+ # don't copy it
646
+ else
647
+ target_called_segment[k]=v
648
+ end
649
+ end
650
+
651
+ build_segment_for_transfer(new_sample, source_called_segment, target_called_segment, options)
652
+ target_called_segment.end_trace(source_called_segment.exit_timestamp)
653
+ end
654
+ end
655
+
656
+ # Generates segment ids for json transaction segments
657
+ class IDGenerator
658
+ def initialize
659
+ @next_id = 0
660
+ end
661
+ def next_id
662
+ @next_id += 1
663
+ end
664
+ end
665
+ end
666
+ end