scout_rails_proxy_proxy 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
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