scout_rails 0.0.1

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.
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