factorylabs-newrelic_rpm 2.10.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (160) hide show
  1. data/CHANGELOG +354 -0
  2. data/LICENSE +37 -0
  3. data/README-2.10.2.2 +10 -0
  4. data/README.md +138 -0
  5. data/bin/mongrel_rpm +33 -0
  6. data/bin/newrelic_cmd +4 -0
  7. data/cert/cacert.pem +34 -0
  8. data/install.rb +45 -0
  9. data/lib/new_relic/agent.rb +315 -0
  10. data/lib/new_relic/agent/agent.rb +647 -0
  11. data/lib/new_relic/agent/busy_calculator.rb +86 -0
  12. data/lib/new_relic/agent/chained_call.rb +13 -0
  13. data/lib/new_relic/agent/collection_helper.rb +66 -0
  14. data/lib/new_relic/agent/error_collector.rb +117 -0
  15. data/lib/new_relic/agent/instrumentation/active_merchant.rb +18 -0
  16. data/lib/new_relic/agent/instrumentation/active_record_instrumentation.rb +91 -0
  17. data/lib/new_relic/agent/instrumentation/authlogic.rb +8 -0
  18. data/lib/new_relic/agent/instrumentation/controller_instrumentation.rb +389 -0
  19. data/lib/new_relic/agent/instrumentation/data_mapper.rb +90 -0
  20. data/lib/new_relic/agent/instrumentation/delayed_job_instrumentation.rb +12 -0
  21. data/lib/new_relic/agent/instrumentation/memcache.rb +24 -0
  22. data/lib/new_relic/agent/instrumentation/merb/controller.rb +26 -0
  23. data/lib/new_relic/agent/instrumentation/merb/errors.rb +8 -0
  24. data/lib/new_relic/agent/instrumentation/metric_frame.rb +199 -0
  25. data/lib/new_relic/agent/instrumentation/net.rb +21 -0
  26. data/lib/new_relic/agent/instrumentation/passenger_instrumentation.rb +20 -0
  27. data/lib/new_relic/agent/instrumentation/rack.rb +109 -0
  28. data/lib/new_relic/agent/instrumentation/rails/action_controller.rb +59 -0
  29. data/lib/new_relic/agent/instrumentation/rails/action_web_service.rb +27 -0
  30. data/lib/new_relic/agent/instrumentation/rails/errors.rb +24 -0
  31. data/lib/new_relic/agent/instrumentation/sinatra.rb +39 -0
  32. data/lib/new_relic/agent/method_tracer.rb +348 -0
  33. data/lib/new_relic/agent/patch_const_missing.rb +125 -0
  34. data/lib/new_relic/agent/sampler.rb +46 -0
  35. data/lib/new_relic/agent/samplers/cpu_sampler.rb +50 -0
  36. data/lib/new_relic/agent/samplers/memory_sampler.rb +141 -0
  37. data/lib/new_relic/agent/samplers/mongrel_sampler.rb +23 -0
  38. data/lib/new_relic/agent/samplers/object_sampler.rb +24 -0
  39. data/lib/new_relic/agent/shim_agent.rb +21 -0
  40. data/lib/new_relic/agent/stats_engine.rb +22 -0
  41. data/lib/new_relic/agent/stats_engine/metric_stats.rb +116 -0
  42. data/lib/new_relic/agent/stats_engine/samplers.rb +74 -0
  43. data/lib/new_relic/agent/stats_engine/transactions.rb +154 -0
  44. data/lib/new_relic/agent/transaction_sampler.rb +315 -0
  45. data/lib/new_relic/agent/worker_loop.rb +118 -0
  46. data/lib/new_relic/commands/deployments.rb +145 -0
  47. data/lib/new_relic/commands/new_relic_commands.rb +30 -0
  48. data/lib/new_relic/control.rb +484 -0
  49. data/lib/new_relic/control/external.rb +13 -0
  50. data/lib/new_relic/control/merb.rb +24 -0
  51. data/lib/new_relic/control/rails.rb +151 -0
  52. data/lib/new_relic/control/ruby.rb +36 -0
  53. data/lib/new_relic/control/sinatra.rb +14 -0
  54. data/lib/new_relic/histogram.rb +89 -0
  55. data/lib/new_relic/local_environment.rb +299 -0
  56. data/lib/new_relic/merbtasks.rb +6 -0
  57. data/lib/new_relic/metric_data.rb +42 -0
  58. data/lib/new_relic/metric_parser.rb +124 -0
  59. data/lib/new_relic/metric_parser/action_mailer.rb +9 -0
  60. data/lib/new_relic/metric_parser/active_merchant.rb +26 -0
  61. data/lib/new_relic/metric_parser/active_record.rb +25 -0
  62. data/lib/new_relic/metric_parser/controller.rb +54 -0
  63. data/lib/new_relic/metric_parser/controller_cpu.rb +38 -0
  64. data/lib/new_relic/metric_parser/errors.rb +6 -0
  65. data/lib/new_relic/metric_parser/external.rb +50 -0
  66. data/lib/new_relic/metric_parser/mem_cache.rb +12 -0
  67. data/lib/new_relic/metric_parser/other_transaction.rb +15 -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 +67 -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/mongrel_rpm.ru +25 -0
  76. data/lib/new_relic/rack/newrelic.yml +26 -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 +362 -0
  80. data/lib/new_relic/transaction_analysis.rb +121 -0
  81. data/lib/new_relic/transaction_sample.rb +671 -0
  82. data/lib/new_relic/version.rb +54 -0
  83. data/lib/new_relic_api.rb +276 -0
  84. data/lib/newrelic_rpm.rb +45 -0
  85. data/lib/tasks/all.rb +4 -0
  86. data/lib/tasks/install.rake +7 -0
  87. data/lib/tasks/tests.rake +15 -0
  88. data/newrelic.yml +227 -0
  89. data/newrelic_rpm.gemspec +214 -0
  90. data/recipes/newrelic.rb +6 -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 +38 -0
  94. data/test/new_relic/agent/active_record_instrumentation_test.rb +268 -0
  95. data/test/new_relic/agent/agent_controller_test.rb +254 -0
  96. data/test/new_relic/agent/agent_test_controller.rb +78 -0
  97. data/test/new_relic/agent/busy_calculator_test.rb +79 -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/error_collector_test.rb +173 -0
  101. data/test/new_relic/agent/method_tracer_test.rb +340 -0
  102. data/test/new_relic/agent/metric_data_test.rb +56 -0
  103. data/test/new_relic/agent/mock_ar_connection.rb +40 -0
  104. data/test/new_relic/agent/mock_scope_listener.rb +23 -0
  105. data/test/new_relic/agent/net_instrumentation_test.rb +63 -0
  106. data/test/new_relic/agent/rpm_agent_test.rb +125 -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 +88 -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 +177 -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 +404 -0
  115. data/test/new_relic/agent/worker_loop_test.rb +103 -0
  116. data/test/new_relic/control_test.rb +110 -0
  117. data/test/new_relic/delayed_job_test.rb +108 -0
  118. data/test/new_relic/deployments_api_test.rb +68 -0
  119. data/test/new_relic/environment_test.rb +75 -0
  120. data/test/new_relic/metric_parser_test.rb +172 -0
  121. data/test/new_relic/metric_spec_test.rb +177 -0
  122. data/test/new_relic/shim_agent_test.rb +9 -0
  123. data/test/new_relic/stats_test.rb +291 -0
  124. data/test/new_relic/version_number_test.rb +76 -0
  125. data/test/test_helper.rb +53 -0
  126. data/test/ui/newrelic_controller_test.rb +14 -0
  127. data/test/ui/newrelic_helper_test.rb +53 -0
  128. data/ui/controllers/newrelic_controller.rb +220 -0
  129. data/ui/helpers/google_pie_chart.rb +49 -0
  130. data/ui/helpers/newrelic_helper.rb +320 -0
  131. data/ui/views/layouts/newrelic_default.rhtml +47 -0
  132. data/ui/views/newrelic/_explain_plans.rhtml +27 -0
  133. data/ui/views/newrelic/_sample.rhtml +19 -0
  134. data/ui/views/newrelic/_segment.rhtml +28 -0
  135. data/ui/views/newrelic/_segment_limit_message.rhtml +1 -0
  136. data/ui/views/newrelic/_segment_row.rhtml +14 -0
  137. data/ui/views/newrelic/_show_sample_detail.rhtml +24 -0
  138. data/ui/views/newrelic/_show_sample_sql.rhtml +20 -0
  139. data/ui/views/newrelic/_show_sample_summary.rhtml +3 -0
  140. data/ui/views/newrelic/_sql_row.rhtml +11 -0
  141. data/ui/views/newrelic/_stack_trace.rhtml +30 -0
  142. data/ui/views/newrelic/_table.rhtml +12 -0
  143. data/ui/views/newrelic/explain_sql.rhtml +42 -0
  144. data/ui/views/newrelic/images/arrow-close.png +0 -0
  145. data/ui/views/newrelic/images/arrow-open.png +0 -0
  146. data/ui/views/newrelic/images/blue_bar.gif +0 -0
  147. data/ui/views/newrelic/images/file_icon.png +0 -0
  148. data/ui/views/newrelic/images/gray_bar.gif +0 -0
  149. data/ui/views/newrelic/images/new-relic-rpm-desktop.gif +0 -0
  150. data/ui/views/newrelic/images/new_relic_rpm_desktop.gif +0 -0
  151. data/ui/views/newrelic/images/textmate.png +0 -0
  152. data/ui/views/newrelic/index.rhtml +57 -0
  153. data/ui/views/newrelic/javascript/prototype-scriptaculous.js +7288 -0
  154. data/ui/views/newrelic/javascript/transaction_sample.js +107 -0
  155. data/ui/views/newrelic/sample_not_found.rhtml +2 -0
  156. data/ui/views/newrelic/show_sample.rhtml +80 -0
  157. data/ui/views/newrelic/show_source.rhtml +3 -0
  158. data/ui/views/newrelic/stylesheets/style.css +484 -0
  159. data/ui/views/newrelic/threads.rhtml +52 -0
  160. metadata +238 -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,671 @@
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 FakeSegment < Segment
291
+
292
+ public :parent_segment=
293
+ end
294
+
295
+ class SummarySegment < Segment
296
+
297
+
298
+ def initialize(segment)
299
+ super segment.entry_timestamp, segment.metric_name, nil
300
+
301
+ add_segments segment.called_segments
302
+
303
+ end_trace segment.exit_timestamp
304
+ end
305
+
306
+ def add_segments(segments)
307
+ segments.collect do |segment|
308
+ SummarySegment.new(segment)
309
+ end.each {|segment| add_called_segment(segment)}
310
+ end
311
+
312
+ end
313
+
314
+ class CompositeSegment < Segment
315
+ attr_reader :detail_segments
316
+
317
+ def initialize(segments)
318
+ summary = SummarySegment.new(segments.first)
319
+ super summary.entry_timestamp, "Repeating pattern (#{segments.length} repeats)", nil
320
+
321
+ summary.end_trace(segments.last.exit_timestamp)
322
+
323
+ @detail_segments = segments.clone
324
+
325
+ add_called_segment(summary)
326
+ end_trace summary.exit_timestamp
327
+ end
328
+
329
+ def detail_segments=(segments)
330
+ @detail_segments = segments
331
+ end
332
+
333
+ end
334
+
335
+ class << self
336
+ def obfuscate_sql(sql)
337
+ NewRelic::Agent.instance.obfuscator.call(sql)
338
+ end
339
+
340
+
341
+ def get_connection(config)
342
+ @@connections ||= {}
343
+
344
+ connection = @@connections[config]
345
+
346
+ return connection if connection
347
+
348
+ begin
349
+ connection = ActiveRecord::Base.send("#{config[:adapter]}_connection", config)
350
+ @@connections[config] = connection
351
+ rescue => e
352
+ NewRelic::Agent.agent.log.error("Caught exception #{e} trying to get connection to DB for explain. Control: #{config}")
353
+ NewRelic::Agent.agent.log.error(e.backtrace.join("\n"))
354
+ nil
355
+ end
356
+ end
357
+
358
+ def close_connections
359
+ @@connections ||= {}
360
+ @@connections.values.each do |connection|
361
+ begin
362
+ connection.disconnect!
363
+ rescue
364
+ end
365
+ end
366
+
367
+ @@connections = {}
368
+ end
369
+
370
+ end
371
+
372
+ attr_accessor :profile
373
+ attr_reader :root_segment
374
+ attr_reader :params
375
+ attr_reader :sample_id
376
+
377
+ def initialize(time = Time.now.to_f, sample_id = nil)
378
+ @sample_id = sample_id || object_id
379
+ @start_time = time
380
+ @root_segment = create_segment 0.0, "ROOT"
381
+ @params = {}
382
+ @params[:request_params] = {}
383
+ end
384
+
385
+ def count_segments
386
+ @root_segment.count_segments - 1 # don't count the root segment
387
+ end
388
+
389
+ def truncate(max)
390
+ original_count = count_segments
391
+
392
+ return if original_count <= max
393
+
394
+ @root_segment.truncate(max-1)
395
+
396
+ if params[:segment_count].nil?
397
+ params[:segment_count] = original_count
398
+ end
399
+ end
400
+
401
+ # offset from start of app
402
+ def timestamp
403
+ @start_time - @@start_time.to_f
404
+ end
405
+
406
+ def to_json(options = {})
407
+ map = {:sample_id => @sample_id,
408
+ :start_time => @start_time,
409
+ :root_segment => @root_segment}
410
+ if @params && !@params.empty?
411
+ map[:params] = @params
412
+ end
413
+ map.to_json
414
+ end
415
+
416
+ def self.from_json(json)
417
+ json = ActiveSupport::JSON.decode(json) if json.is_a?(String)
418
+
419
+ if json.is_a?(Array)
420
+ start_time = json[0].to_f / 1000
421
+ custom_params = HashWithIndifferentAccess.new(json[2])
422
+ params = {:request_params => HashWithIndifferentAccess.new(json[1]),
423
+ :custom_params => custom_params}
424
+ cpu_time = custom_params.delete(:cpu_time)
425
+ sample_id = nil
426
+ params[:cpu_time] = cpu_time.to_f / 1000 if cpu_time
427
+ root = json[3]
428
+ else
429
+ start_time = json["start_time"].to_f
430
+ sample_id = json["sample_id"].to_i
431
+ params = json["params"]
432
+ root = json["root_segment"]
433
+ end
434
+
435
+ sample = TransactionSample.new(start_time, sample_id)
436
+
437
+ if params
438
+ sample.send :params=, HashWithIndifferentAccess.new(params)
439
+ end
440
+ if root
441
+ sample.send :root_segment=, Segment.from_json(root, IDGenerator.new)
442
+ end
443
+ sample
444
+ end
445
+
446
+ def start_time
447
+ Time.at(@start_time)
448
+ end
449
+
450
+ def path_string
451
+ @root_segment.path_string
452
+ end
453
+
454
+ def create_segment(relative_timestamp, metric_name, segment_id = nil)
455
+ raise TypeError.new("Frozen Transaction Sample") if frozen?
456
+ NewRelic::TransactionSample::Segment.new(relative_timestamp, metric_name, segment_id)
457
+ end
458
+
459
+ def freeze
460
+ @root_segment.freeze
461
+ super
462
+ end
463
+
464
+ def duration
465
+ root_segment.duration
466
+ end
467
+
468
+ def each_segment(&block)
469
+ @root_segment.each_segment(&block)
470
+ end
471
+
472
+ def to_s_compact
473
+ @root_segment.to_s_compact
474
+ end
475
+
476
+ def find_segment(id)
477
+ @root_segment.find_segment(id)
478
+ end
479
+
480
+ def to_s
481
+ s = "Transaction Sample collected at #{start_time}\n"
482
+ s << " {\n"
483
+ s << " Path: #{params[:path]} \n"
484
+
485
+ params.each do |k,v|
486
+ next if k == :path
487
+ s << " #{k}: " <<
488
+ case v
489
+ when Enumerable then v.map(&:to_s).sort.join("; ")
490
+ when String then v
491
+ when Float then '%6.3s' % v
492
+ else
493
+ raise "unexpected value type for #{k}: '#{v}' (#{v.class})"
494
+ end << "\n"
495
+ end
496
+ s << " }\n\n"
497
+ s << @root_segment.to_debug_str(0)
498
+ end
499
+
500
+ # return a new transaction sample that treats segments
501
+ # with the given regular expression in their name as if they
502
+ # were never called at all. This allows us to strip out segments
503
+ # from traces captured in development environment that would not
504
+ # normally show up in production (like Rails/Application Code Loading)
505
+ def omit_segments_with(regex)
506
+ regex = Regexp.new(regex)
507
+
508
+ sample = TransactionSample.new(@start_time, sample_id)
509
+
510
+ params.each {|k,v| sample.params[k] = v}
511
+
512
+ delta = build_segment_with_omissions(sample, 0.0, @root_segment, sample.root_segment, regex)
513
+ sample.root_segment.end_trace(@root_segment.exit_timestamp - delta)
514
+ sample.profile = self.profile
515
+ sample.freeze
516
+ end
517
+
518
+ # return a new transaction sample that can be sent to the RPM service.
519
+ # this involves potentially one or more of the following options
520
+ # :explain_sql : run EXPLAIN on all queries whose response times equal the value for this key
521
+ # (for example :explain_sql => 2.0 would explain everything over 2 seconds. 0.0 would explain everything.)
522
+ # :keep_backtraces : keep backtraces, significantly increasing size of trace (off by default)
523
+ # :obfuscate_sql : clear sql fields of potentially sensitive values (higher overhead, better security)
524
+ def prepare_to_send(options={})
525
+ sample = TransactionSample.new(@start_time, sample_id)
526
+
527
+ sample.params.merge! self.params
528
+
529
+ begin
530
+ build_segment_for_transfer(sample, @root_segment, sample.root_segment, options)
531
+ ensure
532
+ self.class.close_connections
533
+ end
534
+
535
+ sample.root_segment.end_trace(@root_segment.exit_timestamp)
536
+ sample.freeze
537
+ end
538
+
539
+ def analyze
540
+ sample = self
541
+ original_path_string = nil
542
+ loop do
543
+ original_path_string = sample.path_string.to_s
544
+ new_sample = sample.dup
545
+ new_sample.root_segment = sample.root_segment.dup
546
+ new_sample.root_segment.called_segments = analyze_called_segments(root_segment.called_segments)
547
+ sample = new_sample
548
+ return sample if sample.path_string.to_s == original_path_string
549
+ end
550
+
551
+ end
552
+
553
+ protected
554
+ def root_segment=(segment)
555
+ @root_segment = segment
556
+ end
557
+ def params=(params)
558
+ @params = params
559
+ end
560
+
561
+ private
562
+
563
+ def analyze_called_segments(called_segments)
564
+ path = nil
565
+ like_segments = []
566
+
567
+ segments = []
568
+
569
+ called_segments.each do |segment|
570
+ segment = segment.dup
571
+ segment.called_segments = analyze_called_segments(segment.called_segments)
572
+
573
+ current_path = segment.path_string
574
+ if path == current_path
575
+ like_segments << segment
576
+ else
577
+ segments += summarize_segments(like_segments)
578
+
579
+ like_segments.clear
580
+ like_segments << segment
581
+ path = current_path
582
+ end
583
+ end
584
+ segments += summarize_segments(like_segments)
585
+
586
+ segments
587
+ end
588
+
589
+ def summarize_segments(like_segments)
590
+ if like_segments.length > COLLAPSE_SEGMENTS_THRESHOLD
591
+ [CompositeSegment.new(like_segments)]
592
+ else
593
+ like_segments
594
+ end
595
+ end
596
+
597
+ def build_segment_with_omissions(new_sample, time_delta, source_segment, target_segment, regex)
598
+ source_segment.called_segments.each do |source_called_segment|
599
+ # if this segment's metric name matches the given regular expression, bail
600
+ # here and increase the amount of time that we reduce the target sample with
601
+ # by this omitted segment's duration.
602
+ do_omit = regex =~ source_called_segment.metric_name
603
+
604
+ if do_omit
605
+ time_delta += source_called_segment.duration
606
+ else
607
+ target_called_segment = new_sample.create_segment(
608
+ source_called_segment.entry_timestamp - time_delta,
609
+ source_called_segment.metric_name,
610
+ source_called_segment.segment_id)
611
+
612
+ target_segment.add_called_segment target_called_segment
613
+ source_called_segment.params.each do |k,v|
614
+ target_called_segment[k]=v
615
+ end
616
+
617
+ time_delta = build_segment_with_omissions(
618
+ new_sample, time_delta, source_called_segment, target_called_segment, regex)
619
+ target_called_segment.end_trace(source_called_segment.exit_timestamp - time_delta)
620
+ end
621
+ end
622
+
623
+ return time_delta
624
+ end
625
+
626
+ # see prepare_to_send for what we do with options
627
+ def build_segment_for_transfer(new_sample, source_segment, target_segment, options)
628
+ source_segment.called_segments.each do |source_called_segment|
629
+ target_called_segment = new_sample.create_segment(
630
+ source_called_segment.entry_timestamp,
631
+ source_called_segment.metric_name,
632
+ source_called_segment.segment_id)
633
+
634
+ target_segment.add_called_segment target_called_segment
635
+ source_called_segment.params.each do |k,v|
636
+ case k
637
+ when :backtrace
638
+ target_called_segment[k]=v if options[:keep_backtraces]
639
+ when :sql
640
+ sql = v
641
+
642
+ # run an EXPLAIN on this sql if specified.
643
+ if options[:explain_enabled] && options[:explain_sql] && source_called_segment.duration > options[:explain_sql].to_f
644
+ target_called_segment[:explanation] = source_called_segment.explain_sql
645
+ end
646
+
647
+ target_called_segment[:sql]=sql if options[:record_sql] == :raw
648
+ target_called_segment[:sql_obfuscated] = TransactionSample.obfuscate_sql(sql) if options[:record_sql] == :obfuscated
649
+ when :connection_config
650
+ # don't copy it
651
+ else
652
+ target_called_segment[k]=v
653
+ end
654
+ end
655
+
656
+ build_segment_for_transfer(new_sample, source_called_segment, target_called_segment, options)
657
+ target_called_segment.end_trace(source_called_segment.exit_timestamp)
658
+ end
659
+ end
660
+
661
+ # Generates segment ids for json transaction segments
662
+ class IDGenerator
663
+ def initialize
664
+ @next_id = 0
665
+ end
666
+ def next_id
667
+ @next_id += 1
668
+ end
669
+ end
670
+ end
671
+ end