scout_rails_proxy_proxy 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .rvmrc
@@ -0,0 +1,50 @@
1
+ # 1.0.3
2
+
3
+ * MetricMeta equality - downcase
4
+ * Suppressing "cat: /proc/cpuinfo: No such file or directory" error on distros that don't support it.
5
+
6
+ # 1.0.2
7
+
8
+ * Net::HTTP instrumentation
9
+ * ActionController::Metal instrumentation
10
+ * Determining number of processors for CPU % calculation
11
+
12
+ # 1.0.1
13
+
14
+ * Unicorn support (requires "preload_app true" in unicorn config file)
15
+ * Fix for Thin detection - ensure it's actually running
16
+ * Fixing name conflict btw Tracer#store and ActiveRecord::Store
17
+
18
+ # 1.0.0
19
+
20
+ * Release!
21
+
22
+ # 0.0.6.pre
23
+
24
+ * Rails 2 - Not collecting traces when an exception occurs
25
+ * Increased Transaction Sample Storage to 2 seconds from 1 second to decrease noise in UI
26
+
27
+ # 0.0.5
28
+
29
+ * Support for custom categories
30
+ * Not raising an exception w/an unbalanced stack
31
+ * Only allows controllers as the entry point for a transaction
32
+
33
+ # 0.0.4
34
+
35
+ * Transaction Sampling
36
+
37
+ # 0.0.3.pre
38
+
39
+ * Removed dynamic ActiveRecord caller instrumentation
40
+ * Fixed issue that prevents the app from loading if ActiveRecord isn't used.
41
+ * Using a metric hash for each request, then merging when complete. Ensures data associated w/requests that overlap a
42
+ minute boundary are correctly associated.
43
+
44
+ # 0.0.2
45
+
46
+ * Doesn't prevent app from loading if no configuration exists for the current environment.
47
+
48
+ # 0.0.1
49
+
50
+ * Boom! Initial Release.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in scout_rails_proxy.gemspec
4
+ gemspec
@@ -0,0 +1,44 @@
1
+ # ScoutRailsProxy
2
+
3
+ A Ruby gem for detailed Rails application performance analysis. Metrics are reported to [Scout](https://scoutapp.com), a hosted server and application monitoring service. For general server monitoring, see our [server monitoring agent](https://github.com/scoutapp/scout-client).
4
+
5
+ ![Scout Rails Monitoring](https://img.skitch.com/20120714-frkr9i1pyjgn58uqrwqh55yfb8.jpg)
6
+
7
+ ## Getting Started
8
+
9
+ Install the gem:
10
+
11
+ gem install scout_rails_proxy
12
+
13
+ Signup for a [Scout](https://scoutapp.com) account and copy the config file to `RAILS_ROOT/config/scout_rails_proxy.yml`.
14
+
15
+ Your config file should look like:
16
+
17
+ common: &defaults
18
+ name: YOUR_APPLICATION_NAME
19
+ key: YOUR_APPLICATION_KEY
20
+ monitor: true
21
+
22
+ production:
23
+ <<: *defaults
24
+
25
+ ## Supported Frameworks
26
+
27
+ * Rails 2.2 and greater
28
+
29
+ ## Supported Rubies
30
+
31
+ * Ruby 1.8.7
32
+ * Ruby 1.9.2
33
+ * Ruby 1.9.3
34
+
35
+ ## Supported Application Servers
36
+
37
+ * Phusion Passenger
38
+ * Thin
39
+ * WEBrick
40
+ * Unicorn (make sure to add `preload_app true` to `config/unicorn.rb`)
41
+
42
+ ## Help
43
+
44
+ See our [troubleshooting tips](https://scoutapp.com/info/support_app_monitoring) and/or email support@scoutapp.com if you need a hand.
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,32 @@
1
+ module ScoutRailsProxy
2
+ end
3
+ require 'socket'
4
+ require 'set'
5
+ require 'net/http'
6
+ require File.expand_path('../scout_rails_proxy/version.rb', __FILE__)
7
+ require File.expand_path('../scout_rails_proxy/agent.rb', __FILE__)
8
+ require File.expand_path('../scout_rails_proxy/layaway.rb', __FILE__)
9
+ require File.expand_path('../scout_rails_proxy/layaway_file.rb', __FILE__)
10
+ require File.expand_path('../scout_rails_proxy/config.rb', __FILE__)
11
+ require File.expand_path('../scout_rails_proxy/environment.rb', __FILE__)
12
+ require File.expand_path('../scout_rails_proxy/metric_meta.rb', __FILE__)
13
+ require File.expand_path('../scout_rails_proxy/metric_stats.rb', __FILE__)
14
+ require File.expand_path('../scout_rails_proxy/stack_item.rb', __FILE__)
15
+ require File.expand_path('../scout_rails_proxy/store.rb', __FILE__)
16
+ require File.expand_path('../scout_rails_proxy/tracer.rb', __FILE__)
17
+ require File.expand_path('../scout_rails_proxy/transaction_sample.rb', __FILE__)
18
+ require File.expand_path('../scout_rails_proxy/instruments/process/process_cpu.rb', __FILE__)
19
+ require File.expand_path('../scout_rails_proxy/instruments/process/process_memory.rb', __FILE__)
20
+
21
+ if defined?(Rails) and Rails.respond_to?(:version) and Rails.version =~ /^3/
22
+ module ScoutRailsProxy
23
+ class Railtie < Rails::Railtie
24
+ initializer "scout_rails_proxy.start" do |app|
25
+ ScoutRailsProxy::Agent.instance.start
26
+ end
27
+ end
28
+ end
29
+ else
30
+ ScoutRailsProxy::Agent.instance.start
31
+ end
32
+
@@ -0,0 +1,319 @@
1
+ module ScoutRailsProxy
2
+ # The agent gathers performance data from a Ruby application. One Agent instance is created per-Ruby process.
3
+ #
4
+ # Each Agent object creates a worker thread (unless monitoring is disabled or we're forking).
5
+ # The worker thread wakes up every +Agent#period+, merges in-memory metrics w/those saved to disk,
6
+ # saves the merged data to disk, and sends it to the Scout server.
7
+ class Agent
8
+ # Headers passed up with all API requests.
9
+ HTTP_HEADERS = { "Agent-Hostname" => Socket.gethostname }
10
+ DEFAULT_HOST = 'scoutapp.com'
11
+ # see self.instance
12
+ @@instance = nil
13
+
14
+ # Accessors below are for associated classes
15
+ attr_accessor :store
16
+ attr_accessor :layaway
17
+ attr_accessor :config
18
+ attr_accessor :environment
19
+
20
+ attr_accessor :logger
21
+ attr_accessor :log_file # path to the log file
22
+ attr_accessor :options # options passed to the agent when +#start+ is called.
23
+ attr_accessor :metric_lookup # Hash used to lookup metric ids based on their name and scope
24
+
25
+ # All access to the agent is thru this class method to ensure multiple Agent instances are not initialized per-Ruby process.
26
+ def self.instance(options = {})
27
+ @@instance ||= self.new(options)
28
+ end
29
+
30
+ # Note - this doesn't start instruments or the worker thread. This is handled via +#start+ as we don't
31
+ # want to start the worker thread or install instrumentation if (1) disabled for this environment (2) a worker thread shouldn't
32
+ # be started (when forking).
33
+ def initialize(options = {})
34
+ @started = false
35
+ @options ||= options
36
+ @store = ScoutRailsProxy::Store.new
37
+ @layaway = ScoutRailsProxy::Layaway.new
38
+ @config = ScoutRailsProxy::Config.new(options[:config_path])
39
+ @metric_lookup = Hash.new
40
+ @process_cpu=ScoutRailsProxy::Instruments::Process::ProcessCpu.new(environment.processors)
41
+ @process_memory=ScoutRailsProxy::Instruments::Process::ProcessMemory.new
42
+ end
43
+
44
+ def environment
45
+ @environment ||= ScoutRailsProxy::Environment.new
46
+ end
47
+
48
+ # This is called via +ScoutRailsProxy::Agent.instance.start+ when ScoutRailsProxy is required in a Ruby application.
49
+ # It initializes the agent and starts the worker thread (if appropiate).
50
+ def start(options = {})
51
+ @options.merge!(options)
52
+ init_logger
53
+ logger.info "Attempting to start Scout Agent [#{ScoutRailsProxy::VERSION}] on [#{Socket.gethostname}]"
54
+ if !config.settings['monitor'] and !@options[:force]
55
+ logger.warn "Monitoring isn't enabled for the [#{environment.env}] environment."
56
+ return false
57
+ elsif !environment.app_server
58
+ logger.warn "Couldn't find a supported app server. Not starting agent."
59
+ return false
60
+ elsif started?
61
+ logger.warn "Already started agent."
62
+ return false
63
+ end
64
+ @started = true
65
+ logger.info "Starting monitoring. Framework [#{environment.framework}] App Server [#{environment.app_server}]."
66
+ start_instruments
67
+ if !start_worker_thread?
68
+ logger.debug "Not starting worker thread"
69
+ install_passenger_worker_process_event if environment.app_server == :passenger
70
+ install_unicorn_worker_loop if environment.app_server == :unicorn
71
+ return
72
+ end
73
+ start_worker_thread
74
+ handle_exit
75
+ logger.info "Scout Agent [#{ScoutRailsProxy::VERSION}] Initialized"
76
+ end
77
+
78
+ # Placeholder: store metrics locally on exit so those in memory aren't lost. Need to decide
79
+ # whether we'll report these immediately or just store locally and risk having stale data.
80
+ def handle_exit
81
+ if environment.sinatra? || environment.jruby? || environment.rubinius?
82
+ logger.debug "Exit handler not supported"
83
+ else
84
+ at_exit { at_exit { logger.debug "Shutdown!" } }
85
+ end
86
+ end
87
+
88
+ def started?
89
+ @started
90
+ end
91
+
92
+ def gem_root
93
+ File.expand_path(File.join("..","..",".."), __FILE__)
94
+ end
95
+
96
+ def init_logger
97
+ @log_file = "#{log_path}/scout_rails_proxy.log"
98
+ @logger = Logger.new(@log_file)
99
+ @logger.level = Logger::DEBUG
100
+ def logger.format_message(severity, timestamp, progname, msg)
101
+ prefix = "[#{timestamp.strftime("%m/%d/%y %H:%M:%S %z")} #{Socket.gethostname} (#{$$})] #{severity} : #{msg}\n"
102
+ end
103
+ @logger
104
+ end
105
+
106
+ # The worker thread will automatically start UNLESS:
107
+ # * A supported application server isn't detected (example: running via Rails console)
108
+ # * A supported application server is detected, but it forks (Passenger). In this case,
109
+ # the agent is started in the forked process.
110
+ def start_worker_thread?
111
+ !environment.forking? or environment.app_server == :thin
112
+ end
113
+
114
+ def install_passenger_worker_process_event
115
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
116
+ logger.debug "Passenger is starting a worker process. Starting worker thread."
117
+ self.class.instance.start_worker_thread
118
+ end
119
+ end
120
+
121
+ def install_unicorn_worker_loop
122
+ logger.debug "Installing Unicorn worker loop."
123
+ Unicorn::HttpServer.class_eval do
124
+ old = instance_method(:worker_loop)
125
+ define_method(:worker_loop) do |worker|
126
+ ScoutRailsProxy::Agent.instance.start_worker_thread
127
+ old.bind(self).call(worker)
128
+ end
129
+ end
130
+ end
131
+
132
+ def log_path
133
+ "#{environment.root}/log"
134
+ end
135
+
136
+ # in seconds, time between when the worker thread wakes up and runs.
137
+ def period
138
+ 60
139
+ end
140
+
141
+ # Creates the worker thread. The worker thread is a loop that runs continuously. It sleeps for +Agent#period+ and when it wakes,
142
+ # processes data, either saving it to disk or reporting to Scout.
143
+ def start_worker_thread
144
+ logger.debug "Creating worker thread."
145
+ @worker_thread = Thread.new do
146
+ begin
147
+ logger.debug "Starting worker thread, running every #{period} seconds"
148
+ next_time = Time.now + period
149
+ while true do
150
+ now = Time.now
151
+ while now < next_time
152
+ sleep_time = next_time - now
153
+ sleep(sleep_time) if sleep_time > 0
154
+ now = Time.now
155
+ end
156
+ process_metrics
157
+ while next_time <= now
158
+ next_time += period
159
+ end
160
+ end
161
+ rescue
162
+ logger.debug "Worker Thread Exception!!!!!!!"
163
+ logger.debug $!.message
164
+ logger.debug $!.backtrace
165
+ end
166
+ end # thread new
167
+ logger.debug "Done creating worker thread."
168
+ end
169
+
170
+ # Writes each payload to a file for auditing.
171
+ def write_to_file(object)
172
+ logger.debug "Writing to file"
173
+ full_path = Pathname.new(RAILS_ROOT+'/log/audit/scout')
174
+ ( full_path +
175
+ "#{Time.now.strftime('%Y-%m-%d_%H:%M:%S')}.json" ).open("w") do |f|
176
+ f.puts object.to_json
177
+ end
178
+ end
179
+
180
+ # Before reporting, lookup metric_id for each MetricMeta. This speeds up
181
+ # reporting on the server-side.
182
+ def add_metric_ids(metrics)
183
+ metrics.each do |meta,stats|
184
+ if metric_id = metric_lookup[meta]
185
+ meta.metric_id = metric_id
186
+ end
187
+ end
188
+ end
189
+
190
+ # Called from #process_metrics, which is run via the worker thread.
191
+ def run_samplers
192
+ begin
193
+ cpu_util=@process_cpu.run # returns a hash
194
+ logger.debug "Process CPU: #{cpu_util.inspect} [#{environment.processors} CPU(s)]"
195
+ store.track!("CPU/Utilization",cpu_util) if cpu_util
196
+ rescue => e
197
+ logger.info "Error reading ProcessCpu"
198
+ logger.debug e.message
199
+ logger.debug e.backtrace.join("\n")
200
+ end
201
+
202
+ begin
203
+ mem_usage=@process_memory.run # returns a single number, in MB
204
+ logger.debug "Process Memory: #{mem_usage}MB"
205
+ store.track!("Memory/Physical",mem_usage) if mem_usage
206
+ rescue => e
207
+ logger.info "Error reading ProcessMemory"
208
+ logger.debug e.message
209
+ logger.debug e.backtrace.join("\n")
210
+ end
211
+ end
212
+
213
+ # Called in the worker thread. Merges in-memory metrics w/those on disk and reports metrics
214
+ # to the server.
215
+ def process_metrics
216
+ logger.debug "Processing metrics"
217
+ run_samplers
218
+ metrics = layaway.deposit_and_deliver
219
+ if metrics.any?
220
+ add_metric_ids(metrics)
221
+ # for debugging, count the total number of requests
222
+ controller_count = 0
223
+ metrics.each do |meta,stats|
224
+ if meta.metric_name =~ /\AController/
225
+ controller_count += stats.call_count
226
+ end
227
+ end
228
+ logger.debug "#{config.settings['name']} Delivering metrics for #{controller_count} requests."
229
+ response = post( checkin_uri,
230
+ Marshal.dump(:metrics => metrics, :sample => store.sample),
231
+ "Content-Type" => "application/json" )
232
+ if response and response.is_a?(Net::HTTPSuccess)
233
+ directives = Marshal.load(response.body)
234
+ self.metric_lookup.merge!(directives[:metric_lookup])
235
+ store.transaction_sample_lock.synchronize do
236
+ store.sample = nil
237
+ end
238
+ logger.debug "Metric Cache Size: #{metric_lookup.size}"
239
+ end
240
+ end
241
+ rescue
242
+ logger.info "Error on checkin to #{checkin_uri.to_s}"
243
+ logger.info $!.message
244
+ logger.debug $!.backtrace
245
+ end
246
+
247
+ def checkin_uri
248
+ URI.parse("http://#{config.settings['host'] || DEFAULT_HOST}/app/#{config.settings['key']}/checkin.scout?name=#{CGI.escape(config.settings['name'])}")
249
+ end
250
+
251
+ def post(url, body, headers = Hash.new)
252
+ response = nil
253
+ request(url) do |connection|
254
+ post = Net::HTTP::Post.new( url.path +
255
+ (url.query ? ('?' + url.query) : ''),
256
+ HTTP_HEADERS.merge(headers) )
257
+ post.body = body
258
+ response=connection.request(post)
259
+ end
260
+ response
261
+ end
262
+
263
+ def request(url, &connector)
264
+ response = nil
265
+
266
+ http_class = if config.settings['proxy']
267
+ Net::HTTP::Proxy(
268
+ config.settings['proxy']['host'],
269
+ config.settings['proxy']['port'],
270
+ config.settings['proxy']['user'],
271
+ config.settings['proxy']['password']
272
+ )
273
+ else
274
+ Net::HTTP
275
+ end
276
+
277
+ http = http_class.new url.host, url.port
278
+ response = http.start(&connector)
279
+ logger.debug "got response: #{response.inspect}"
280
+ case response
281
+ when Net::HTTPSuccess, Net::HTTPNotModified
282
+ logger.debug "/checkin OK"
283
+ when Net::HTTPBadRequest
284
+ logger.warn "/checkin FAILED: The Account Key [#{config.settings['key']}] is invalid."
285
+ else
286
+ logger.debug "/checkin FAILED: #{response.inspect}"
287
+ end
288
+ rescue Exception
289
+ logger.debug "Exception sending request to server: #{$!.message}"
290
+ ensure
291
+ response
292
+ end
293
+
294
+ # Loads the instrumention logic.
295
+ def load_instruments
296
+ case environment.framework
297
+ when :rails
298
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/rails/action_controller_instruments.rb'))
299
+ when :rails3
300
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/rails3/action_controller_instruments.rb'))
301
+ when :sinatra
302
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/sinatra_instruments.rb'))
303
+ end
304
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/active_record_instruments.rb'))
305
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/net_http.rb'))
306
+ rescue
307
+ logger.warn "Exception loading instruments:"
308
+ logger.warn $!.message
309
+ logger.warn $!.backtrace
310
+ end
311
+
312
+ # Injects instruments into the Ruby application.
313
+ def start_instruments
314
+ logger.debug "Installing instrumentation"
315
+ load_instruments
316
+ end
317
+
318
+ end # class Agent
319
+ end # module ScoutRailsProxy