honkster-newrelic_rpm 2.13.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (174) hide show
  1. data/CHANGELOG +462 -0
  2. data/LICENSE +37 -0
  3. data/README.rdoc +172 -0
  4. data/bin/mongrel_rpm +33 -0
  5. data/bin/newrelic +13 -0
  6. data/bin/newrelic_cmd +5 -0
  7. data/cert/cacert.pem +34 -0
  8. data/install.rb +9 -0
  9. data/lib/new_relic/agent.rb +382 -0
  10. data/lib/new_relic/agent/agent.rb +741 -0
  11. data/lib/new_relic/agent/busy_calculator.rb +91 -0
  12. data/lib/new_relic/agent/chained_call.rb +13 -0
  13. data/lib/new_relic/agent/error_collector.rb +131 -0
  14. data/lib/new_relic/agent/instrumentation/active_merchant.rb +18 -0
  15. data/lib/new_relic/agent/instrumentation/active_record_instrumentation.rb +92 -0
  16. data/lib/new_relic/agent/instrumentation/acts_as_solr.rb +45 -0
  17. data/lib/new_relic/agent/instrumentation/authlogic.rb +8 -0
  18. data/lib/new_relic/agent/instrumentation/controller_instrumentation.rb +409 -0
  19. data/lib/new_relic/agent/instrumentation/data_mapper.rb +58 -0
  20. data/lib/new_relic/agent/instrumentation/delayed_job_instrumentation.rb +22 -0
  21. data/lib/new_relic/agent/instrumentation/memcache.rb +40 -0
  22. data/lib/new_relic/agent/instrumentation/merb/controller.rb +26 -0
  23. data/lib/new_relic/agent/instrumentation/merb/errors.rb +9 -0
  24. data/lib/new_relic/agent/instrumentation/metric_frame.rb +319 -0
  25. data/lib/new_relic/agent/instrumentation/net.rb +17 -0
  26. data/lib/new_relic/agent/instrumentation/passenger_instrumentation.rb +22 -0
  27. data/lib/new_relic/agent/instrumentation/rack.rb +98 -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/rails3/action_controller.rb +45 -0
  32. data/lib/new_relic/agent/instrumentation/rails3/errors.rb +21 -0
  33. data/lib/new_relic/agent/instrumentation/sinatra.rb +46 -0
  34. data/lib/new_relic/agent/instrumentation/sunspot.rb +17 -0
  35. data/lib/new_relic/agent/instrumentation/unicorn_instrumentation.rb +10 -0
  36. data/lib/new_relic/agent/method_tracer.rb +350 -0
  37. data/lib/new_relic/agent/sampler.rb +50 -0
  38. data/lib/new_relic/agent/samplers/cpu_sampler.rb +54 -0
  39. data/lib/new_relic/agent/samplers/delayed_job_lock_sampler.rb +37 -0
  40. data/lib/new_relic/agent/samplers/memory_sampler.rb +142 -0
  41. data/lib/new_relic/agent/samplers/object_sampler.rb +24 -0
  42. data/lib/new_relic/agent/shim_agent.rb +25 -0
  43. data/lib/new_relic/agent/stats_engine.rb +24 -0
  44. data/lib/new_relic/agent/stats_engine/metric_stats.rb +118 -0
  45. data/lib/new_relic/agent/stats_engine/samplers.rb +83 -0
  46. data/lib/new_relic/agent/stats_engine/transactions.rb +149 -0
  47. data/lib/new_relic/agent/transaction_sampler.rb +330 -0
  48. data/lib/new_relic/agent/worker_loop.rb +81 -0
  49. data/lib/new_relic/collection_helper.rb +71 -0
  50. data/lib/new_relic/command.rb +85 -0
  51. data/lib/new_relic/commands/deployments.rb +105 -0
  52. data/lib/new_relic/commands/install.rb +81 -0
  53. data/lib/new_relic/control.rb +203 -0
  54. data/lib/new_relic/control/configuration.rb +149 -0
  55. data/lib/new_relic/control/frameworks/external.rb +13 -0
  56. data/lib/new_relic/control/frameworks/merb.rb +24 -0
  57. data/lib/new_relic/control/frameworks/rails.rb +126 -0
  58. data/lib/new_relic/control/frameworks/rails3.rb +60 -0
  59. data/lib/new_relic/control/frameworks/ruby.rb +36 -0
  60. data/lib/new_relic/control/frameworks/sinatra.rb +18 -0
  61. data/lib/new_relic/control/instrumentation.rb +95 -0
  62. data/lib/new_relic/control/logging_methods.rb +74 -0
  63. data/lib/new_relic/control/profiling.rb +24 -0
  64. data/lib/new_relic/control/server_methods.rb +88 -0
  65. data/lib/new_relic/delayed_job_injection.rb +27 -0
  66. data/lib/new_relic/histogram.rb +91 -0
  67. data/lib/new_relic/local_environment.rb +333 -0
  68. data/lib/new_relic/merbtasks.rb +6 -0
  69. data/lib/new_relic/metric_data.rb +42 -0
  70. data/lib/new_relic/metric_parser.rb +136 -0
  71. data/lib/new_relic/metric_parser/action_mailer.rb +9 -0
  72. data/lib/new_relic/metric_parser/active_merchant.rb +26 -0
  73. data/lib/new_relic/metric_parser/active_record.rb +28 -0
  74. data/lib/new_relic/metric_parser/apdex.rb +88 -0
  75. data/lib/new_relic/metric_parser/controller.rb +62 -0
  76. data/lib/new_relic/metric_parser/controller_cpu.rb +38 -0
  77. data/lib/new_relic/metric_parser/errors.rb +6 -0
  78. data/lib/new_relic/metric_parser/external.rb +50 -0
  79. data/lib/new_relic/metric_parser/mem_cache.rb +50 -0
  80. data/lib/new_relic/metric_parser/other_transaction.rb +36 -0
  81. data/lib/new_relic/metric_parser/view.rb +61 -0
  82. data/lib/new_relic/metric_parser/web_frontend.rb +14 -0
  83. data/lib/new_relic/metric_parser/web_service.rb +9 -0
  84. data/lib/new_relic/metric_spec.rb +67 -0
  85. data/lib/new_relic/metrics.rb +9 -0
  86. data/lib/new_relic/noticed_error.rb +24 -0
  87. data/lib/new_relic/rack/developer_mode.rb +257 -0
  88. data/lib/new_relic/rack/metric_app.rb +64 -0
  89. data/lib/new_relic/rack/mongrel_rpm.ru +26 -0
  90. data/lib/new_relic/rack/newrelic.yml +27 -0
  91. data/lib/new_relic/rack_app.rb +6 -0
  92. data/lib/new_relic/recipes.rb +82 -0
  93. data/lib/new_relic/stats.rb +368 -0
  94. data/lib/new_relic/timer_lib.rb +27 -0
  95. data/lib/new_relic/transaction_analysis.rb +119 -0
  96. data/lib/new_relic/transaction_sample.rb +586 -0
  97. data/lib/new_relic/url_rule.rb +14 -0
  98. data/lib/new_relic/version.rb +55 -0
  99. data/lib/new_relic_api.rb +276 -0
  100. data/lib/newrelic_rpm.rb +49 -0
  101. data/lib/tasks/all.rb +4 -0
  102. data/lib/tasks/install.rake +7 -0
  103. data/lib/tasks/tests.rake +15 -0
  104. data/newrelic.yml +246 -0
  105. data/newrelic_rpm.gemspec +254 -0
  106. data/recipes/newrelic.rb +6 -0
  107. data/test/active_record_fixtures.rb +55 -0
  108. data/test/config/newrelic.yml +48 -0
  109. data/test/config/test_control.rb +36 -0
  110. data/test/new_relic/agent/active_record_instrumentation_test.rb +286 -0
  111. data/test/new_relic/agent/agent_controller_test.rb +294 -0
  112. data/test/new_relic/agent/agent_test_controller.rb +77 -0
  113. data/test/new_relic/agent/busy_calculator_test.rb +81 -0
  114. data/test/new_relic/agent/collection_helper_test.rb +125 -0
  115. data/test/new_relic/agent/error_collector_test.rb +163 -0
  116. data/test/new_relic/agent/memcache_instrumentation_test.rb +103 -0
  117. data/test/new_relic/agent/method_tracer_test.rb +340 -0
  118. data/test/new_relic/agent/metric_data_test.rb +53 -0
  119. data/test/new_relic/agent/metric_frame_test.rb +51 -0
  120. data/test/new_relic/agent/mock_scope_listener.rb +23 -0
  121. data/test/new_relic/agent/net_instrumentation_test.rb +77 -0
  122. data/test/new_relic/agent/rpm_agent_test.rb +142 -0
  123. data/test/new_relic/agent/stats_engine/metric_stats_test.rb +79 -0
  124. data/test/new_relic/agent/stats_engine/samplers_test.rb +72 -0
  125. data/test/new_relic/agent/stats_engine/stats_engine_test.rb +184 -0
  126. data/test/new_relic/agent/task_instrumentation_test.rb +188 -0
  127. data/test/new_relic/agent/testable_agent.rb +13 -0
  128. data/test/new_relic/agent/transaction_sample_builder_test.rb +195 -0
  129. data/test/new_relic/agent/transaction_sample_test.rb +192 -0
  130. data/test/new_relic/agent/transaction_sampler_test.rb +385 -0
  131. data/test/new_relic/agent/worker_loop_test.rb +66 -0
  132. data/test/new_relic/control_test.rb +127 -0
  133. data/test/new_relic/deployments_api_test.rb +69 -0
  134. data/test/new_relic/environment_test.rb +75 -0
  135. data/test/new_relic/metric_parser_test.rb +226 -0
  136. data/test/new_relic/metric_spec_test.rb +177 -0
  137. data/test/new_relic/rack/episodes_test.rb +318 -0
  138. data/test/new_relic/shim_agent_test.rb +9 -0
  139. data/test/new_relic/stats_test.rb +312 -0
  140. data/test/new_relic/version_number_test.rb +89 -0
  141. data/test/test_contexts.rb +28 -0
  142. data/test/test_helper.rb +72 -0
  143. data/ui/helpers/developer_mode_helper.rb +359 -0
  144. data/ui/helpers/google_pie_chart.rb +49 -0
  145. data/ui/views/layouts/newrelic_default.rhtml +47 -0
  146. data/ui/views/newrelic/_explain_plans.rhtml +27 -0
  147. data/ui/views/newrelic/_sample.rhtml +20 -0
  148. data/ui/views/newrelic/_segment.rhtml +29 -0
  149. data/ui/views/newrelic/_segment_limit_message.rhtml +1 -0
  150. data/ui/views/newrelic/_segment_row.rhtml +14 -0
  151. data/ui/views/newrelic/_show_sample_detail.rhtml +24 -0
  152. data/ui/views/newrelic/_show_sample_sql.rhtml +20 -0
  153. data/ui/views/newrelic/_show_sample_summary.rhtml +3 -0
  154. data/ui/views/newrelic/_sql_row.rhtml +16 -0
  155. data/ui/views/newrelic/_stack_trace.rhtml +15 -0
  156. data/ui/views/newrelic/_table.rhtml +12 -0
  157. data/ui/views/newrelic/explain_sql.rhtml +43 -0
  158. data/ui/views/newrelic/file/images/arrow-close.png +0 -0
  159. data/ui/views/newrelic/file/images/arrow-open.png +0 -0
  160. data/ui/views/newrelic/file/images/blue_bar.gif +0 -0
  161. data/ui/views/newrelic/file/images/file_icon.png +0 -0
  162. data/ui/views/newrelic/file/images/gray_bar.gif +0 -0
  163. data/ui/views/newrelic/file/images/new-relic-rpm-desktop.gif +0 -0
  164. data/ui/views/newrelic/file/images/new_relic_rpm_desktop.gif +0 -0
  165. data/ui/views/newrelic/file/images/textmate.png +0 -0
  166. data/ui/views/newrelic/file/javascript/jquery-1.4.2.js +6240 -0
  167. data/ui/views/newrelic/file/javascript/transaction_sample.js +120 -0
  168. data/ui/views/newrelic/file/stylesheets/style.css +484 -0
  169. data/ui/views/newrelic/index.rhtml +59 -0
  170. data/ui/views/newrelic/sample_not_found.rhtml +2 -0
  171. data/ui/views/newrelic/show_sample.rhtml +79 -0
  172. data/ui/views/newrelic/show_source.rhtml +3 -0
  173. data/ui/views/newrelic/threads.rhtml +52 -0
  174. metadata +307 -0
@@ -0,0 +1,741 @@
1
+ require 'socket'
2
+ require 'net/https'
3
+ require 'net/http'
4
+ require 'logger'
5
+ require 'zlib'
6
+ require 'stringio'
7
+
8
+ module NewRelic
9
+ module Agent
10
+
11
+ # The Agent is a singleton that is instantiated when the plugin is
12
+ # activated. It collects performance data from ruby applications
13
+ # in realtime as the application runs, and periodically sends that
14
+ # data to the NewRelic server.
15
+ class Agent
16
+
17
+ # Specifies the version of the agent's communication protocol with
18
+ # the NewRelic hosted site.
19
+
20
+ PROTOCOL_VERSION = 8
21
+ # 14105: v8 (tag 2.10.3)
22
+ # (no v7)
23
+ # 10379: v6 (not tagged)
24
+ # 4078: v5 (tag 2.5.4)
25
+ # 2292: v4 (tag 2.3.6)
26
+ # 1754: v3 (tag 2.3.0)
27
+ # 534: v2 (shows up in 2.1.0, our first tag)
28
+
29
+
30
+ def initialize
31
+
32
+ @launch_time = Time.now
33
+
34
+ @metric_ids = {}
35
+ @histogram = NewRelic::Histogram.new(NewRelic::Control.instance.apdex_t / 10)
36
+ @stats_engine = NewRelic::Agent::StatsEngine.new
37
+ @transaction_sampler = NewRelic::Agent::TransactionSampler.new
38
+ @stats_engine.transaction_sampler = @transaction_sampler
39
+ @error_collector = NewRelic::Agent::ErrorCollector.new
40
+
41
+ @request_timeout = NewRelic::Control.instance.fetch('timeout', 2 * 60)
42
+
43
+ @last_harvest_time = Time.now
44
+ @obfuscator = method(:default_sql_obfuscator)
45
+ end
46
+
47
+ module ClassMethods
48
+ # Should only be called by NewRelic::Control
49
+ def instance
50
+ @instance ||= self.new
51
+ end
52
+ end
53
+
54
+ module InstanceMethods
55
+
56
+ attr_reader :obfuscator
57
+ attr_reader :stats_engine
58
+ attr_reader :transaction_sampler
59
+ attr_reader :error_collector
60
+ attr_reader :record_sql
61
+ attr_reader :histogram
62
+ attr_reader :metric_ids
63
+ attr_reader :url_rules
64
+
65
+ def record_transaction(duration_seconds, options={})
66
+ is_error = options['is_error'] || options['error_message'] || options['exception']
67
+ metric = options['metric']
68
+ metric ||= options['uri'] # normalize this with url rules
69
+ raise "metric or uri arguments required" unless metric
70
+ metric_info = NewRelic::MetricParser.for_metric_named(metric)
71
+
72
+ if metric_info.is_web_transaction?
73
+ NewRelic::Agent::Instrumentation::MetricFrame.record_apdex(metric_info, duration_seconds, duration_seconds, is_error)
74
+ histogram.process(duration_seconds)
75
+ end
76
+ metrics = metric_info.summary_metrics
77
+
78
+ metrics << metric
79
+ metrics.each do |name|
80
+ stats = stats_engine.get_stats_no_scope(name)
81
+ stats.record_data_point(duration_seconds)
82
+ end
83
+
84
+ if is_error
85
+ if options['exception']
86
+ e = options['exception']
87
+ elsif options['error_message']
88
+ e = Exception.new options['error_message']
89
+ else
90
+ e = Exception.new 'Unknown Error'
91
+ end
92
+ error_collector.notice_error e, :uri => options['uri'], :metric => metric
93
+ end
94
+ # busy time ?
95
+ end
96
+
97
+ # This method is deprecated. Use NewRelic::Agent.manual_start
98
+ def manual_start(ignored=nil, also_ignored=nil)
99
+ raise "This method no longer supported. Instead use the class method NewRelic::Agent.manual_start"
100
+ end
101
+
102
+ # This method should be called in a forked process after a fork.
103
+ # It assumes the parent process initialized the agent, but does
104
+ # not assume the agent started.
105
+ #
106
+ # The call is idempotent, but not re-entrant.
107
+ #
108
+ # * It clears any metrics carried over from the parent process
109
+ # * Restarts the sampler thread if necessary
110
+ # * Initiates a new agent run and worker loop unless that was done
111
+ # in the parent process and +:force_reconnect+ is not true
112
+ #
113
+ # Options:
114
+ # * <tt>:force_reconnect => true</tt> to force the spawned process to
115
+ # establish a new connection, such as when forking a long running process.
116
+ # The default is false--it will only connect to the server if the parent
117
+ # had not connected.
118
+ # * <tt>:keep_retrying => false</tt> if we try to initiate a new
119
+ # connection, this tells me to only try it once so this method returns
120
+ # quickly if there is some kind of latency with the server.
121
+ def after_fork(options={})
122
+
123
+ # @connected gets false after we fail to connect or have an error
124
+ # connecting. @connected has nil if we haven't finished trying to connect.
125
+ # or we didn't attempt a connection because this is the master process
126
+
127
+ # log.debug "Agent received after_fork notice in #$$: [#{control.agent_enabled?}; monitor=#{control.monitor_mode?}; connected: #{@connected.inspect}; thread=#{@worker_thread.inspect}]"
128
+ return if !control.agent_enabled? or
129
+ !control.monitor_mode? or
130
+ @connected == false or
131
+ @worker_thread && @worker_thread.alive?
132
+
133
+ log.info "Starting the worker thread in #$$ after forking."
134
+
135
+ # Clear out stats that are left over from parent process
136
+ reset_stats
137
+
138
+ # Don't ever check to see if this is a spawner. If we're in a forked process
139
+ # I'm pretty sure we're not also forking new instances.
140
+ start_worker_thread(options)
141
+ @stats_engine.start_sampler_thread
142
+ end
143
+
144
+ # True if we have initialized and completed 'start'
145
+ def started?
146
+ @started
147
+ end
148
+
149
+ # Return nil if not yet connected, true if successfully started
150
+ # and false if we failed to start.
151
+ def connected?
152
+ @connected
153
+ end
154
+
155
+ # Attempt a graceful shutdown of the agent.
156
+ def shutdown
157
+ return if not started?
158
+ if @worker_loop
159
+ @worker_loop.stop
160
+
161
+ log.debug "Starting Agent shutdown"
162
+
163
+ # if litespeed, then ignore all future SIGUSR1 - it's
164
+ # litespeed trying to shut us down
165
+
166
+ if control.dispatcher == :litespeed
167
+ Signal.trap("SIGUSR1", "IGNORE")
168
+ Signal.trap("SIGTERM", "IGNORE")
169
+ end
170
+
171
+ begin
172
+ NewRelic::Agent.disable_all_tracing do
173
+ graceful_disconnect
174
+ end
175
+ rescue => e
176
+ log.error e
177
+ log.error e.backtrace.join("\n")
178
+ end
179
+ end
180
+ @started = nil
181
+ end
182
+
183
+ def start_transaction
184
+ @stats_engine.start_transaction
185
+ end
186
+
187
+ def end_transaction
188
+ @stats_engine.end_transaction
189
+ end
190
+
191
+ def set_record_sql(should_record)
192
+ prev = Thread::current[:record_sql]
193
+ Thread::current[:record_sql] = should_record
194
+ prev.nil? || prev
195
+ end
196
+
197
+ def set_record_tt(should_record)
198
+ prev = Thread::current[:record_tt]
199
+ Thread::current[:record_tt] = should_record
200
+ prev.nil? || prev
201
+ end
202
+ # Push flag indicating whether we should be tracing in this
203
+ # thread.
204
+ def push_trace_execution_flag(should_trace=false)
205
+ (Thread.current[:newrelic_untraced] ||= []) << should_trace
206
+ end
207
+
208
+ # Pop the current trace execution status. Restore trace execution status
209
+ # to what it was before we pushed the current flag.
210
+ def pop_trace_execution_flag
211
+ Thread.current[:newrelic_untraced].pop if Thread.current[:newrelic_untraced]
212
+ end
213
+
214
+ def set_sql_obfuscator(type, &block)
215
+ if type == :before
216
+ @obfuscator = NewRelic::ChainedCall.new(block, @obfuscator)
217
+ elsif type == :after
218
+ @obfuscator = NewRelic::ChainedCall.new(@obfuscator, block)
219
+ elsif type == :replace
220
+ @obfuscator = block
221
+ else
222
+ fail "unknown sql_obfuscator type #{type}"
223
+ end
224
+ end
225
+
226
+ def log
227
+ NewRelic::Agent.logger
228
+ end
229
+
230
+ # Start up the agent. This verifies that the agent_enabled? is
231
+ # true and initializes the sampler based on the current
232
+ # configuration settings. Then it will fire up the background
233
+ # thread for sending data to the server if applicable.
234
+ def start
235
+ if started?
236
+ control.log! "Agent Started Already!", :error
237
+ return
238
+ end
239
+ return if !control.agent_enabled?
240
+ @started = true
241
+ @local_host = determine_host
242
+
243
+ if control.dispatcher.nil? || control.dispatcher.to_s.empty?
244
+ log.info "No dispatcher detected."
245
+ else
246
+ log.info "Dispatcher: #{control.dispatcher.to_s}"
247
+ end
248
+ log.info "Application: #{control.app_names.join(", ")}" unless control.app_names.empty?
249
+
250
+ sampler_config = control.fetch('transaction_tracer', {})
251
+ # TODO: Should move this state into the transaction sampler instance
252
+ @should_send_samples = @config_should_send_samples = sampler_config.fetch('enabled', true)
253
+ @should_send_random_samples = sampler_config.fetch('random_sample', false)
254
+ @explain_threshold = sampler_config.fetch('explain_threshold', 0.5).to_f
255
+ @explain_enabled = sampler_config.fetch('explain_enabled', true)
256
+ @record_sql = sampler_config.fetch('record_sql', :obfuscated).to_sym
257
+
258
+ # use transaction_threshold: 4.0 to force the TT collection
259
+ # threshold to 4 seconds
260
+ # use transaction_threshold: apdex_f to use your apdex t value
261
+ # multiplied by 4
262
+ # undefined transaction_threshold defaults to 2.0
263
+ apdex_f = 4 * NewRelic::Control.instance.apdex_t
264
+ @slowest_transaction_threshold = sampler_config.fetch('transaction_threshold', 2.0)
265
+ if @slowest_transaction_threshold =~ /apdex_f/i
266
+ @slowest_transaction_threshold = apdex_f
267
+ end
268
+ @slowest_transaction_threshold = @slowest_transaction_threshold.to_f
269
+
270
+ log.warn "Agent is configured to send raw SQL to RPM service" if @record_sql == :raw
271
+
272
+ case
273
+ when !control.monitor_mode?
274
+ log.warn "Agent configured not to send data in this environment - edit newrelic.yml to change this"
275
+ when !control.license_key
276
+ log.error "No license key found. Please edit your newrelic.yml file and insert your license key."
277
+ when control.license_key.length != 40
278
+ log.error "Invalid license key: #{control.license_key}"
279
+ when [:passenger, :unicorn].include?(control.dispatcher)
280
+ log.info "Connecting workers after forking."
281
+ else
282
+ # Do the connect in the foreground if we are in sync mode
283
+ NewRelic::Agent.disable_all_tracing { connect(:keep_retrying => false) } if control.sync_startup
284
+
285
+ # Start the event loop and initiate connection if necessary
286
+ start_worker_thread
287
+
288
+ # Our shutdown handler needs to run after other shutdown handlers
289
+ # that may be doing things like running the app (hello sinatra).
290
+ if control.send_data_on_exit
291
+ if RUBY_VERSION =~ /rubinius/i
292
+ list = at_exit { shutdown }
293
+ # move the shutdown handler to the front of the list, to
294
+ # execute last:
295
+ list.unshift(list.pop)
296
+ elsif !defined?(JRuby) or !defined?(Sinatra::Application)
297
+ at_exit { at_exit { shutdown } }
298
+ end
299
+ end
300
+ end
301
+ log.info "New Relic RPM Agent #{NewRelic::VERSION::STRING} Initialized: pid = #$$"
302
+ log.info "Agent Log found in #{NewRelic::Control.instance.log_file}" if NewRelic::Control.instance.log_file
303
+ end
304
+
305
+ # Clear out the metric data, errors, and transaction traces. Reset the histogram data.
306
+ def reset_stats
307
+ @stats_engine.reset_stats
308
+ @unsent_errors = []
309
+ @traces = nil
310
+ @unsent_timeslice_data = {}
311
+ @last_harvest_time = Time.now
312
+ @launch_time = Time.now
313
+ @histogram = NewRelic::Histogram.new(NewRelic::Control.instance.apdex_t / 10)
314
+ end
315
+
316
+ private
317
+ def collector
318
+ @collector ||= control.server
319
+ end
320
+
321
+ # Try to launch the worker thread and connect to the server.
322
+ #
323
+ # See #connect for a description of connection_options.
324
+ def start_worker_thread(connection_options = {})
325
+ log.debug "Creating RPM worker thread."
326
+ @worker_thread = Thread.new do
327
+ begin
328
+ NewRelic::Agent.disable_all_tracing do
329
+ # We try to connect. If this returns false that means
330
+ # the server rejected us for a licensing reason and we should
331
+ # just exit the thread. If it returns nil
332
+ # that means it didn't try to connect because we're in the master.
333
+ connect(connection_options)
334
+ if @connected
335
+ # disable transaction sampling if disabled by the server and we're not in dev mode
336
+ if !control.developer_mode? && !@should_send_samples
337
+ @transaction_sampler.disable
338
+ else
339
+ @transaction_sampler.enable # otherwise ensure TT's are enabled
340
+ end
341
+
342
+ log.info "Reporting performance data every #{@report_period} seconds."
343
+ log.debug "Running worker loop"
344
+ # Note if the agent attempts to report more frequently than allowed by the server
345
+ # the server will start dropping data.
346
+ @worker_loop = WorkerLoop.new
347
+ @worker_loop.run(@report_period) do
348
+ harvest_and_send_timeslice_data
349
+ harvest_and_send_slowest_sample if @should_send_samples
350
+ harvest_and_send_errors if error_collector.enabled
351
+ end
352
+ else
353
+ log.debug "No connection. Worker thread finished."
354
+ end
355
+ end
356
+ rescue NewRelic::Agent::ForceRestartException => e
357
+ log.info e.message
358
+ # disconnect and start over.
359
+ # clear the stats engine
360
+ reset_stats
361
+ @metric_ids = {}
362
+ @connected = nil
363
+ # Wait a short time before trying to reconnect
364
+ sleep 30
365
+ retry
366
+ rescue NewRelic::Agent::ForceDisconnectException => e
367
+ # when a disconnect is requested, stop the current thread, which
368
+ # is the worker thread that gathers data and talks to the
369
+ # server.
370
+ log.error "RPM forced this agent to disconnect (#{e.message})"
371
+ @connected = false
372
+ rescue NewRelic::Agent::ServerConnectionException => e
373
+ log.error "Unable to establish connection with the server. Run with log level set to debug for more information."
374
+ log.debug("#{e.class.name}: #{e.message}\n#{e.backtrace.first}")
375
+ @connected = false
376
+ rescue Exception => e
377
+ log.error "Terminating worker loop: #{e.class.name}: #{e}\n #{e.backtrace.join("\n ")}"
378
+ @connected = false
379
+ end # begin
380
+ end # thread new
381
+ @worker_thread['newrelic_label'] = 'Worker Loop'
382
+ end
383
+
384
+ def control
385
+ NewRelic::Control.instance
386
+ end
387
+
388
+ # Connect to the server and validate the license. If successful,
389
+ # @connected has true when finished. If not successful, you can
390
+ # keep calling this. Return false if we could not establish a
391
+ # connection with the server and we should not retry, such as if
392
+ # there's a bad license key.
393
+ #
394
+ # Set keep_retrying=false to disable retrying and return asap, such as when
395
+ # invoked in the foreground. Otherwise this runs until a successful
396
+ # connection is made, or the server rejects us.
397
+ #
398
+ # * <tt>:keep_retrying => false</tt> to only try to connect once, and
399
+ # return with the connection set to nil. This ensures we may try again
400
+ # later (default true).
401
+ # * <tt>force_reconnect => true</tt> if you want to establish a new connection
402
+ # to the server before running the worker loop. This means you get a separate
403
+ # agent run and RPM sees it as a separate instance (default is false).
404
+ def connect(options)
405
+ # Don't proceed if we already connected (@connected=true) or if we tried
406
+ # to connect and were rejected with prejudice because of a license issue
407
+ # (@connected=false).
408
+ return if !@connected.nil? && !options[:force_reconnect]
409
+ keep_retrying = options[:keep_retrying].nil? || options[:keep_retrying]
410
+
411
+ # wait a few seconds for the web server to boot, necessary in development
412
+ connect_retry_period = keep_retrying ? 10 : 0
413
+ connect_attempts = 0
414
+ @agent_id = nil
415
+ begin
416
+ sleep connect_retry_period.to_i
417
+ log.debug "Connecting Process to RPM: #$0"
418
+ host = invoke_remote(:get_redirect_host)
419
+ @collector = control.server_from_host(host) if host
420
+ environment = control['send_environment_info'] != false ? control.local_env.snapshot : []
421
+ log.debug "Connecting with validation seed/token: #{control.validate_seed}/#{control.validate_token}" if control.validate_seed
422
+ connect_data = invoke_remote :connect,
423
+ :pid => $$,
424
+ :host => @local_host,
425
+ :app_name => control.app_names,
426
+ :language => 'ruby',
427
+ :agent_version => NewRelic::VERSION::STRING,
428
+ :environment => environment,
429
+ :settings => control.settings,
430
+ :validate => {:seed => control.validate_seed,
431
+ :token => control.validate_token }
432
+
433
+ @agent_id = connect_data['agent_run_id']
434
+ @report_period = connect_data['data_report_period']
435
+ @url_rules = connect_data['url_rules']
436
+
437
+ control.log! "Connected to NewRelic Service at #{@collector}"
438
+ log.debug "Agent Run = #{@agent_id}."
439
+ log.debug "Connection data = #{connect_data.inspect}"
440
+
441
+ # Ask the server for permission to send transaction samples.
442
+ # determined by subscription license.
443
+ @should_send_samples = @config_should_send_samples && connect_data['collect_traces']
444
+
445
+ if @should_send_samples
446
+ if @should_send_random_samples
447
+ @transaction_sampler.random_sampling = true
448
+ @transaction_sampler.sampling_rate = connect_data['sampling_rate']
449
+ log.info "Transaction sampling enabled, rate = #{@transaction_sampler.sampling_rate}"
450
+ end
451
+ log.debug "Transaction tracing threshold is #{@slowest_transaction_threshold} seconds."
452
+ else
453
+ log.debug "Transaction traces will not be sent to the RPM service."
454
+ end
455
+
456
+ # Ask for permission to collect error data
457
+ error_collector.enabled = error_collector.config_enabled && connect_data['collect_errors']
458
+
459
+ log.debug "Errors will be sent to the RPM service." if error_collector.enabled
460
+
461
+ @connected_pid = $$
462
+ @connected = true
463
+
464
+ rescue NewRelic::Agent::LicenseException => e
465
+ log.error e.message
466
+ log.info "Visit NewRelic.com to obtain a valid license key, or to upgrade your account."
467
+ @connected = false
468
+
469
+ rescue Timeout::Error, StandardError => e
470
+ if e.instance_of? NewRelic::Agent::ServerConnectionException
471
+ log.info "Unable to establish connection with New Relic RPM Service at #{control.server}: #{e.message}"
472
+ log.debug e.backtrace.join("\n")
473
+ else
474
+ log.error "Error establishing connection with New Relic RPM Service at #{control.server}: #{e.message}"
475
+ log.debug e.backtrace.join("\n")
476
+ end
477
+ # retry logic
478
+ if keep_retrying
479
+ connect_attempts += 1
480
+ case connect_attempts
481
+ when 1..2
482
+ connect_retry_period, period_msg = 60, "1 minute"
483
+ when 3..5
484
+ connect_retry_period, period_msg = 60 * 2, "2 minutes"
485
+ else
486
+ connect_retry_period, period_msg = 5 * 60, "5 minutes"
487
+ end
488
+ log.info "Will re-attempt in #{period_msg}"
489
+ retry
490
+ else
491
+ @connected = nil
492
+ end
493
+ end
494
+ end
495
+
496
+ def determine_host
497
+ Socket.gethostname
498
+ end
499
+
500
+ def determine_home_directory
501
+ control.root
502
+ end
503
+
504
+ def is_application_spawner?
505
+ $0 =~ /ApplicationSpawner|^unicorn\S* master/
506
+ end
507
+
508
+ def harvest_and_send_timeslice_data
509
+
510
+ NewRelic::Agent::BusyCalculator.harvest_busy
511
+
512
+ now = Time.now
513
+
514
+ @unsent_timeslice_data ||= {}
515
+ @unsent_timeslice_data = @stats_engine.harvest_timeslice_data(@unsent_timeslice_data, @metric_ids)
516
+
517
+ begin
518
+ # In this version of the protocol, we get back an assoc array of spec to id.
519
+ metric_ids = invoke_remote(:metric_data, @agent_id,
520
+ @last_harvest_time.to_f,
521
+ now.to_f,
522
+ @unsent_timeslice_data.values)
523
+
524
+ rescue Timeout::Error
525
+ # assume that the data was received. chances are that it was
526
+ metric_ids = nil
527
+ end
528
+
529
+ metric_ids.each do | spec, id |
530
+ @metric_ids[spec] = id
531
+ end if metric_ids
532
+
533
+ log.debug "#{now}: sent #{@unsent_timeslice_data.length} timeslices (#{@agent_id}) in #{Time.now - now} seconds"
534
+
535
+ # if we successfully invoked this web service, then clear the unsent message cache.
536
+ @unsent_timeslice_data = {}
537
+ @last_harvest_time = now
538
+
539
+ # handle_messages
540
+
541
+ # note - exceptions are logged in invoke_remote. If an exception is encountered here,
542
+ # then the metric data is downsampled for another timeslices
543
+ end
544
+
545
+ def harvest_and_send_slowest_sample
546
+ @traces = @transaction_sampler.harvest(@traces, @slowest_transaction_threshold)
547
+
548
+ unless @traces.empty?
549
+ now = Time.now
550
+ log.debug "Sending (#{@traces.length}) transaction traces"
551
+ begin
552
+ # take the traces and prepare them for sending across the
553
+ # wire. This includes gathering SQL explanations, stripping
554
+ # out stack traces, and normalizing SQL. note that we
555
+ # explain only the sql statements whose segments' execution
556
+ # times exceed our threshold (to avoid unnecessary overhead
557
+ # of running explains on fast queries.)
558
+ options = { :keep_backtraces => true }
559
+ options[:record_sql] = @record_sql unless @record_sql == :off
560
+ options[:explain_sql] = @explain_threshold if @explain_enabled
561
+ traces = @traces.collect {|trace| trace.prepare_to_send(options)}
562
+ invoke_remote :transaction_sample_data, @agent_id, traces
563
+ rescue PostTooBigException
564
+ # we tried to send too much data, drop the first trace and
565
+ # try again
566
+ retry if @traces.shift
567
+ end
568
+
569
+ log.debug "Sent slowest sample (#{@agent_id}) in #{Time.now - now} seconds"
570
+ end
571
+
572
+ # if we succeed sending this sample, then we don't need to keep
573
+ # the slowest sample around - it has been sent already and we
574
+ # can collect the next one
575
+ @traces = nil
576
+
577
+ # note - exceptions are logged in invoke_remote. If an
578
+ # exception is encountered here, then the slowest sample of is
579
+ # determined of the entire period since the last reported
580
+ # sample.
581
+ end
582
+
583
+ def harvest_and_send_errors
584
+ @unsent_errors = @error_collector.harvest_errors(@unsent_errors)
585
+ if @unsent_errors && @unsent_errors.length > 0
586
+ log.debug "Sending #{@unsent_errors.length} errors"
587
+ begin
588
+ invoke_remote :error_data, @agent_id, @unsent_errors
589
+ rescue PostTooBigException
590
+ @unsent_errors.shift
591
+ retry
592
+ end
593
+ # if the remote invocation fails, then we never clear
594
+ # @unsent_errors, and therefore we can re-attempt to send on
595
+ # the next heartbeat. Note the error collector maxes out at
596
+ # 20 instances to prevent leakage
597
+ @unsent_errors = []
598
+ end
599
+ end
600
+
601
+ def compress_data(object)
602
+ dump = Marshal.dump(object)
603
+
604
+ # this checks to make sure mongrel won't choke on big uploads
605
+ check_post_size(dump)
606
+
607
+ # we currently optimize for CPU here since we get roughly a 10x
608
+ # reduction in message size with this, and CPU overhead is at a
609
+ # premium. For extra-large posts, we use the higher compression
610
+ # since otherwise it actually errors out.
611
+
612
+ dump_size = dump.size
613
+
614
+ # Compress if content is smaller than 64kb. There are problems
615
+ # with bugs in Ruby in some versions that expose us to a risk of
616
+ # segfaults if we compress aggressively.
617
+ return [dump, 'identity'] if dump_size < (64*1024)
618
+
619
+ # medium payloads get fast compression, to save CPU
620
+ # big payloads get all the compression possible, to stay under
621
+ # the 2,000,000 byte post threshold
622
+ compression = dump_size < 2000000 ? Zlib::BEST_SPEED : Zlib::BEST_COMPRESSION
623
+
624
+ [Zlib::Deflate.deflate(dump, compression), 'deflate']
625
+ end
626
+
627
+ def check_post_size(post_string)
628
+ # TODO: define this as a config option on the server side
629
+ return if post_string.size < control.post_size_limit
630
+ log.warn "Tried to send too much data: #{post_string.size} bytes"
631
+ raise PostTooBigException
632
+ end
633
+
634
+ def send_request(opts)
635
+ request = Net::HTTP::Post.new(opts[:uri], 'CONTENT-ENCODING' => opts[:encoding], 'HOST' => opts[:collector].name)
636
+ request.content_type = "application/octet-stream"
637
+ request.body = opts[:data]
638
+
639
+ log.debug "Connect to #{opts[:collector]}#{opts[:uri]}"
640
+
641
+ response = nil
642
+ http = control.http_connection(collector)
643
+ http.read_timeout = nil
644
+ begin
645
+ NewRelic::TimerLib.timeout(@request_timeout) do
646
+ response = http.request(request)
647
+ end
648
+ rescue Timeout::Error
649
+ log.warn "Timed out trying to post data to RPM (timeout = #{@request_timeout} seconds)" unless @request_timeout < 30
650
+ raise
651
+ end
652
+ if response.is_a? Net::HTTPServiceUnavailable
653
+ raise NewRelic::Agent::ServerConnectionException, "Service unavailable (#{response.code}): #{response.message}"
654
+ elsif response.is_a? Net::HTTPGatewayTimeOut
655
+ log.debug("Timed out getting response: #{response.message}")
656
+ raise Timeout::Error, response.message
657
+ elsif response.is_a? Net::HTTPRequestEntityTooLarge
658
+ raise PostTooBigException
659
+ elsif !(response.is_a? Net::HTTPSuccess)
660
+ raise NewRelic::Agent::ServerConnectionException, "Unexpected response from server (#{response.code}): #{response.message}"
661
+ end
662
+ response
663
+ end
664
+
665
+ def decompress_response(response)
666
+ if response['content-encoding'] != 'gzip'
667
+ log.debug "Uncompressed content returned"
668
+ return response.body
669
+ end
670
+ log.debug "Decompressing return value"
671
+ i = Zlib::GzipReader.new(StringIO.new(response.body))
672
+ i.read
673
+ end
674
+
675
+ def check_for_exception(response)
676
+ dump = decompress_response(response)
677
+ value = Marshal.load(dump)
678
+ raise value if value.is_a? Exception
679
+ value
680
+ end
681
+
682
+ def remote_method_uri(method)
683
+ uri = "/agent_listener/#{PROTOCOL_VERSION}/#{control.license_key}/#{method}"
684
+ uri << "?run_id=#{@agent_id}" if @agent_id
685
+ uri
686
+ end
687
+
688
+ # send a message via post
689
+ def invoke_remote(method, *args)
690
+ #determines whether to zip the data or send plain
691
+ post_data, encoding = compress_data(args)
692
+
693
+ response = send_request({:uri => remote_method_uri(method), :encoding => encoding, :collector => collector, :data => post_data})
694
+
695
+ # raises the right exception if the remote server tells it to die
696
+ return check_for_exception(response)
697
+ rescue NewRelic::Agent::ForceRestartException => e
698
+ log.info e.message
699
+ raise
700
+ rescue SystemCallError, SocketError => e
701
+ # These include Errno connection errors
702
+ raise NewRelic::Agent::ServerConnectionException, "Recoverable error connecting to the server: #{e}"
703
+ end
704
+
705
+ def graceful_disconnect
706
+ if @connected
707
+ begin
708
+ @request_timeout = 10
709
+ log.debug "Flushing unsent metric data to server"
710
+ @worker_loop.run_task
711
+ if @connected_pid == $$
712
+ log.debug "Sending RPM service agent run shutdown message"
713
+ invoke_remote :shutdown, @agent_id, Time.now.to_f
714
+ else
715
+ log.debug "This agent connected from parent process #{@connected_pid}--not sending shutdown"
716
+ end
717
+ log.debug "Graceful disconnect complete"
718
+ rescue Timeout::Error, StandardError
719
+ end
720
+ else
721
+ log.debug "Bypassing graceful disconnect - agent not connected"
722
+ end
723
+ end
724
+ def default_sql_obfuscator(sql)
725
+ sql = sql.dup
726
+ # This is hardly readable. Use the unit tests.
727
+ # remove single quoted strings:
728
+ sql.gsub!(/'(.*?[^\\'])??'(?!')/, '?')
729
+ # remove double quoted strings:
730
+ sql.gsub!(/"(.*?[^\\"])??"(?!")/, '?')
731
+ # replace all number literals
732
+ sql.gsub!(/\d+/, "?")
733
+ sql
734
+ end
735
+ end
736
+
737
+ extend ClassMethods
738
+ include InstanceMethods
739
+ end
740
+ end
741
+ end