scout_rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.DS_Store ADDED
Binary file
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in scout_rails.gemspec
4
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,29 @@
1
+ # ScoutRails
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/highgroove/scout-client).
4
+
5
+ ## Getting Started
6
+
7
+ Install the gem:
8
+
9
+ gem install scout_rails
10
+
11
+ Signup for a [Scout](https://scoutapp.com) account and copy the config file to RAILS_ROOT/config/scout_rails.yml.
12
+
13
+ ## Supported Frameworks
14
+
15
+ * Rails 2.2 and greater
16
+
17
+ ## Supported Rubies
18
+
19
+ * Ruby 1.8.7
20
+ * Ruby 1.9.2
21
+
22
+ ## Supported Application Servers
23
+
24
+ * Phusion Passenger
25
+ * Thin
26
+ * WEBrick
27
+
28
+
29
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,31 @@
1
+ module ScoutRails
2
+ end
3
+ require 'socket'
4
+ require 'set'
5
+ require 'net/http'
6
+ require File.expand_path('../scout_rails/version.rb', __FILE__)
7
+ require File.expand_path('../scout_rails/agent.rb', __FILE__)
8
+ require File.expand_path('../scout_rails/layaway.rb', __FILE__)
9
+ require File.expand_path('../scout_rails/layaway_file.rb', __FILE__)
10
+ require File.expand_path('../scout_rails/config.rb', __FILE__)
11
+ require File.expand_path('../scout_rails/environment.rb', __FILE__)
12
+ require File.expand_path('../scout_rails/metric_meta.rb', __FILE__)
13
+ require File.expand_path('../scout_rails/metric_stats.rb', __FILE__)
14
+ require File.expand_path('../scout_rails/stack_item.rb', __FILE__)
15
+ require File.expand_path('../scout_rails/store.rb', __FILE__)
16
+ require File.expand_path('../scout_rails/tracer.rb', __FILE__)
17
+ require File.expand_path('../scout_rails/instruments/process/process_cpu.rb', __FILE__)
18
+ require File.expand_path('../scout_rails/instruments/process/process_memory.rb', __FILE__)
19
+
20
+ if defined?(Rails) and Rails.respond_to?(:version) and Rails.version =~ /^3/
21
+ module ScoutRails
22
+ class Railtie < Rails::Railtie
23
+ initializer "scout_rails.start" do |app|
24
+ ScoutRails::Agent.instance.start
25
+ end
26
+ end
27
+ end
28
+ else
29
+ ScoutRails::Agent.instance.start
30
+ end
31
+
@@ -0,0 +1,316 @@
1
+ module ScoutRails
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
+ # Hash of class (values are Sets of method names) that should be instrumented by examining
26
+ # the ActiveRecord call stack.
27
+ attr_accessor :dynamic_instruments
28
+
29
+ # All access to the agent is thru this class method to ensure multiple Agent instances are not initialized per-Ruby process.
30
+ def self.instance(options = {})
31
+ @@instance ||= self.new(options)
32
+ end
33
+
34
+ # Note - this doesn't start instruments or the worker thread. This is handled via +#start+ as we don't
35
+ # want to start the worker thread or install instrumentation if (1) disabled for this environment (2) a worker thread shouldn't
36
+ # be started (when forking).
37
+ def initialize(options = {})
38
+ @started = false
39
+ @options ||= options
40
+ @store = ScoutRails::Store.new
41
+ @layaway = ScoutRails::Layaway.new
42
+ @config = ScoutRails::Config.new(options[:config_path])
43
+ @dynamic_instruments = Hash.new
44
+ @metric_lookup = Hash.new
45
+ @process_cpu=ScoutRails::Instruments::Process::ProcessCpu.new(1) # TODO: the argument is the number of processors
46
+ @process_memory=ScoutRails::Instruments::Process::ProcessMemory.new
47
+ end
48
+
49
+ def environment
50
+ @environment ||= ScoutRails::Environment.new
51
+ end
52
+
53
+ # This is called via +ScoutRails::Agent.instance.start+ when ScoutRails is required in a Ruby application.
54
+ # It initializes the agent and starts the worker thread (if appropiate).
55
+ def start(options = {})
56
+ @options.merge!(options)
57
+ init_logger
58
+ if !config.settings['monitor'] and !@options[:force]
59
+ logger.warn "Monitoring isn't enabled for the [#{environment.env}] environment."
60
+ return false
61
+ elsif !environment.app_server
62
+ logger.warn "Couldn't find a supported app server. Not starting agent."
63
+ return false
64
+ elsif started?
65
+ logger.warn "Already started agent."
66
+ return false
67
+ end
68
+ @started = true
69
+ logger.info "Starting monitoring. Framework [#{environment.framework}] App Server [#{environment.app_server}]."
70
+ start_instruments
71
+ if !start_worker_thread?
72
+ logger.debug "Not starting worker thread"
73
+ install_passenger_worker_process_event if environment.passenger?
74
+ return
75
+ end
76
+ start_worker_thread
77
+ handle_exit
78
+ log_version_pid
79
+ end
80
+
81
+ # Placeholder: store metrics locally on exit so those in memory aren't lost. Need to decide
82
+ # whether we'll report these immediately or just store locally and risk having stale data.
83
+ def handle_exit
84
+ if environment.sinatra? || environment.jruby? || environment.rubinius?
85
+ logger.debug "Exit handler not supported"
86
+ else
87
+ at_exit { at_exit { logger.debug "Shutdown!" } }
88
+ end
89
+ end
90
+
91
+ def started?
92
+ @started
93
+ end
94
+
95
+ def gem_root
96
+ File.expand_path(File.join("..","..",".."), __FILE__)
97
+ end
98
+
99
+ def init_logger
100
+ @log_file = "#{log_path}/scout_rails.log"
101
+ @logger = Logger.new(@log_file)
102
+ @logger.level = Logger::DEBUG
103
+ def logger.format_message(severity, timestamp, progname, msg)
104
+ prefix = "[#{timestamp.strftime("%m/%d/%y %H:%M:%S %z")} #{Socket.gethostname} (#{$$})] #{severity} : #{msg}\n"
105
+ end
106
+ @logger
107
+ end
108
+
109
+ # The worker thread will automatically start UNLESS:
110
+ # * A supported application server isn't detected (example: running via Rails console)
111
+ # * A supported application server is detected, but it forks (Passenger). In this case,
112
+ # the agent is started in the forked process.
113
+ def start_worker_thread?
114
+ !environment.forking? or environment.thin?
115
+ end
116
+
117
+ def install_passenger_worker_process_event
118
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
119
+ logger.debug "Passenger is starting a worker process. Starting worker thread."
120
+ self.class.instance.start_worker_thread
121
+ end
122
+ end
123
+
124
+ def log_version_pid
125
+ logger.debug "Scout Agent [#{ScoutRails::VERSION}] Initialized"
126
+ end
127
+
128
+ def log_path
129
+ "#{environment.root}/log"
130
+ end
131
+
132
+ # in seconds, time between when the worker thread wakes up and runs.
133
+ def period
134
+ 60
135
+ end
136
+
137
+ # Every time the worker thread completes, it calls this method
138
+ # to add instruments to selected methods in the AR call stack.
139
+ def add_dynamic_instruments
140
+ dynamic_instruments.each do |class_name,to_instrument|
141
+ klass = class_name.constantize
142
+ klass.class_eval do
143
+ if !klass.respond_to?(:instrument_method)
144
+ include ::ScoutRails::Tracer
145
+ end
146
+ to_instrument.each do |m|
147
+ self.instrument_method(m)
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ # Creates the worker thread. The worker thread is a loop that runs continuously. It sleeps for +Agent#period+ and when it wakes,
154
+ # processes data, either saving it to disk or reporting to Scout.
155
+ def start_worker_thread(connection_options = {})
156
+ logger.debug "Creating worker thread."
157
+ @worker_thread = Thread.new do
158
+ begin
159
+ logger.debug "Starting worker thread, running every #{period} seconds"
160
+ next_time = Time.now + period
161
+ while true do
162
+ now = Time.now
163
+ while now < next_time
164
+ sleep_time = next_time - now
165
+ sleep(sleep_time) if sleep_time > 0
166
+ now = Time.now
167
+ end
168
+ process_metrics
169
+ add_dynamic_instruments
170
+ while next_time <= now
171
+ next_time += period
172
+ end
173
+ end
174
+ rescue
175
+ logger.debug "Worker Thread Exception!!!!!!!"
176
+ logger.debug $!.message
177
+ logger.debug $!.backtrace
178
+ end
179
+ end # thread new
180
+ logger.debug "Done creating worker thread."
181
+ end
182
+
183
+ # Writes each payload to a file for auditing.
184
+ def write_to_file(object)
185
+ logger.debug "Writing to file"
186
+ full_path = Pathname.new(RAILS_ROOT+'/log/audit/scout')
187
+ ( full_path +
188
+ "#{Time.now.strftime('%Y-%m-%d_%H:%M:%S')}.json" ).open("w") do |f|
189
+ f.puts object.to_json
190
+ end
191
+ end
192
+
193
+ # Before reporting, lookup metric_id for each MetricMeta. This speeds up
194
+ # reporting on the server-side.
195
+ def add_metric_ids(metrics)
196
+ metrics.each do |meta,stats|
197
+ if metric_id = metric_lookup[meta]
198
+ meta.metric_id = metric_id
199
+ end
200
+ end
201
+ end
202
+
203
+ # Called from #process_metrics, which is run via the worker thread.
204
+ def run_samplers
205
+ begin
206
+ cpu_util=@process_cpu.run # returns a hash
207
+ logger.debug "Process CPU: #{cpu_util.inspect}"
208
+ store.track!("CPU/Utilization",cpu_util) if cpu_util
209
+ rescue => e
210
+ logger.info "Error reading ProcessCpu"
211
+ logger.debug e.message
212
+ logger.debug e.backtrace.join("\n")
213
+ end
214
+
215
+ begin
216
+ mem_usage=@process_memory.run # returns a single number, in MB
217
+ logger.debug "Process Memory: #{mem_usage}MB"
218
+ store.track!("Memory/Physical",mem_usage) if mem_usage
219
+ rescue => e
220
+ logger.info "Error reading ProcessMemory"
221
+ logger.debug e.message
222
+ logger.debug e.backtrace.join("\n")
223
+ end
224
+ end
225
+
226
+ # Called in the worker thread. Merges in-memory metrics w/those on disk and reports metrics
227
+ # to the server.
228
+ def process_metrics
229
+ logger.debug "Processing metrics"
230
+ run_samplers
231
+ metrics = layaway.deposit_and_deliver
232
+ if metrics.any?
233
+ add_metric_ids(metrics)
234
+ # for debugging, count the total number of requests
235
+ controller_count = 0
236
+ metrics.each do |meta,stats|
237
+ if meta.metric_name =~ /\AController/
238
+ controller_count += stats.call_count
239
+ end
240
+ end
241
+ logger.debug "#{config.settings['name']} Delivering metrics for #{controller_count} requests."
242
+ response = post( checkin_uri,
243
+ Marshal.dump(metrics),
244
+ "Content-Type" => "application/json" )
245
+ if response and response.is_a?(Net::HTTPSuccess)
246
+ directives = Marshal.load(response.body)
247
+ self.metric_lookup.merge!(directives[:metric_lookup])
248
+ logger.debug "Metric Cache Size: #{metric_lookup.size}"
249
+ end
250
+ end
251
+ rescue
252
+ logger.debug "Error on checkin to #{checkin_uri.to_s}"
253
+ logger.debug $!.message
254
+ logger.debug $!.backtrace
255
+ end
256
+
257
+ def checkin_uri
258
+ URI.parse("http://#{config.settings['host'] || DEFAULT_HOST}/app/#{config.settings['key']}/checkin.scout?name=#{CGI.escape(config.settings['name'])}")
259
+ end
260
+
261
+ def post(url, body, headers = Hash.new)
262
+ response = nil
263
+ request(url) do |connection|
264
+ post = Net::HTTP::Post.new( url.path +
265
+ (url.query ? ('?' + url.query) : ''),
266
+ HTTP_HEADERS.merge(headers) )
267
+ post.body = body
268
+ response=connection.request(post)
269
+ end
270
+ response
271
+ end
272
+
273
+ def request(url, &connector)
274
+ response = nil
275
+ http = Net::HTTP.new(url.host, url.port)
276
+ response = http.start(&connector)
277
+ logger.debug "got response: #{response.inspect}"
278
+ case response
279
+ when Net::HTTPSuccess, Net::HTTPNotModified
280
+ logger.debug "/checkin OK"
281
+ when Net::HTTPBadRequest
282
+ logger.warn "/checkin FAILED: The Account Key [#{config.settings['key']}] is invalid."
283
+ else
284
+ logger.debug "/checkin FAILED: #{response.inspect}"
285
+ end
286
+ rescue Exception
287
+ logger.debug "Exception sending request to server: #{$!.message}"
288
+ ensure
289
+ response
290
+ end
291
+
292
+ # Loads the instrumention logic.
293
+ def load_instruments
294
+ case environment.framework
295
+ when :rails
296
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/rails/action_controller_instruments.rb'))
297
+ when :rails3
298
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/rails3/action_controller_instruments.rb'))
299
+ when :sinatra
300
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/sinatra_instruments.rb'))
301
+ end
302
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/active_record_instruments.rb'))
303
+ rescue
304
+ logger.warn "Exception loading instruments:"
305
+ logger.warn $!.message
306
+ logger.warn $!.backtrace
307
+ end
308
+
309
+ # Injects instruments into the Ruby application.
310
+ def start_instruments
311
+ logger.debug "Installing instrumentation"
312
+ load_instruments
313
+ end
314
+
315
+ end # class Agent
316
+ end # module ScoutRails
@@ -0,0 +1,34 @@
1
+ module ScoutRails
2
+ class Config
3
+ def initialize(config_path = nil)
4
+ @config_path = config_path
5
+ end
6
+
7
+ def settings
8
+ return @settings if @settings
9
+ load_file
10
+ end
11
+
12
+ def config_path
13
+ @config_path || File.join(ScoutRails::Agent.instance.environment.root,"config","scout_rails.yml")
14
+ end
15
+
16
+ def config_file
17
+ File.expand_path(config_path)
18
+ end
19
+
20
+ def load_file
21
+ if !File.exist?(config_file)
22
+ ScoutRails::Agent.instance.logger.warn "No config file found at [#{config_file}]."
23
+ @settings = {}
24
+ else
25
+ @settings = YAML.load(ERB.new(File.read(config_file)).result(binding))[ScoutRails::Agent.instance.environment.env]
26
+ end
27
+ rescue Exception => e
28
+ ScoutRails::Agent.instance.logger.warn "Unable to load the config file."
29
+ ScoutRails::Agent.instance.logger.warn e.message
30
+ ScoutRails::Agent.instance.logger.warn e.backtrace
31
+ @settings = {}
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,86 @@
1
+ # Used to retrieve environment information for this application.
2
+ module ScoutRails
3
+ class Environment
4
+ def env
5
+ @env ||= case framework
6
+ when :rails then RAILS_ENV.dup
7
+ when :rails3 then Rails.env
8
+ when :sinatra
9
+ ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
10
+ end
11
+ end
12
+
13
+ def framework
14
+ @framework ||= case
15
+ when defined?(::Rails) && defined?(ActionController)
16
+ if Rails::VERSION::MAJOR < 3
17
+ :rails
18
+ else
19
+ :rails3
20
+ end
21
+ when defined?(::Sinatra) && defined?(::Sinatra::Base) then :sinatra
22
+ else :ruby
23
+ end
24
+ end
25
+
26
+ def root
27
+ if framework == :rails
28
+ RAILS_ROOT.to_s
29
+ elsif framework == :rails3
30
+ Rails.root
31
+ elsif framework == :sinatra
32
+ Sinatra::Application.root
33
+ else
34
+ '.'
35
+ end
36
+ end
37
+
38
+ def app_server
39
+ @app_server ||= if thin? then :thin
40
+ elsif passenger? then :passenger
41
+ elsif webrick? then :webrick
42
+ else nil
43
+ end
44
+ end
45
+
46
+ ### app server related-checks
47
+
48
+ def thin?
49
+ defined?(::Thin) && defined?(::Thin::Server)
50
+ end
51
+
52
+ # Called via +#forking?+ since Passenger forks. Adds an event listener to start the worker thread
53
+ # inside the passenger worker process.
54
+ # Background: http://www.modrails.com/documentation/Users%20guide%20Nginx.html#spawning%5Fmethods%5Fexplained
55
+ def passenger?
56
+ (defined?(::Passenger) && defined?(::Passenger::AbstractServer)) || defined?(::IN_PHUSION_PASSENGER)
57
+ end
58
+
59
+ def webrick?
60
+ defined?(::WEBrick) && defined?(::WEBrick::VERSION)
61
+ end
62
+
63
+ # If forking, don't start worker thread in the master process. Since it's started as a Thread, it won't survive
64
+ # the fork.
65
+ def forking?
66
+ passenger?
67
+ end
68
+
69
+ ### ruby checks
70
+
71
+ def rubinius?
72
+ RUBY_VERSION =~ /rubinius/i
73
+ end
74
+
75
+ def jruby?
76
+ defined?(JRuby)
77
+ end
78
+
79
+ ### framework checks
80
+
81
+ def sinatra?
82
+ defined?(Sinatra::Application)
83
+ end
84
+
85
+ end # class Environemnt
86
+ end