scout_apm 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.
@@ -0,0 +1,44 @@
1
+ # Contains methods specific to logging (initializing the log file, applying the log level, applying the log format, etc.)
2
+ module ScoutApm
3
+ class Agent
4
+ module Logging
5
+ def log_path
6
+ "#{environment.root}/log"
7
+ end
8
+
9
+ def init_logger
10
+ @log_file = "#{log_path}/scout_apm.log"
11
+ begin
12
+ @logger = Logger.new(@log_file)
13
+ @logger.level = log_level
14
+ apply_log_format
15
+ rescue Exception => e
16
+ @logger = Logger.new(STDOUT)
17
+ apply_log_format
18
+ @logger.error "Unable to access log file: #{e.message}"
19
+ end
20
+ @logger
21
+ end
22
+
23
+ def apply_log_format
24
+ def logger.format_message(severity, timestamp, progname, msg)
25
+ # since STDOUT isn't exclusive like the scout_apm.log file, apply a prefix.
26
+ prefix = @logdev.dev == STDOUT ? "scout_apm " : ''
27
+ prefix + "[#{timestamp.strftime("%m/%d/%y %H:%M:%S %z")} #{Socket.gethostname} (#{$$})] #{severity} : #{msg}\n"
28
+ end
29
+ end
30
+
31
+ def log_level
32
+ case config.settings['log_level'].downcase
33
+ when "debug" then Logger::DEBUG
34
+ when "info" then Logger::INFO
35
+ when "warn" then Logger::WARN
36
+ when "error" then Logger::ERROR
37
+ when "fatal" then Logger::FATAL
38
+ else Logger::INFO
39
+ end
40
+ end
41
+ end # module Logging
42
+ include Logging
43
+ end # class Agent
44
+ end # moudle ScoutApm
@@ -0,0 +1,110 @@
1
+ # Methods related to sending metrics to scoutapp.com.
2
+ module ScoutApm
3
+ class Agent
4
+ module Reporting
5
+ CA_FILE = File.join( File.dirname(__FILE__), *%w[.. .. .. data cacert.pem] )
6
+ VERIFY_MODE = OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
7
+
8
+ # Called in the worker thread. Merges in-memory metrics w/those on disk and reports metrics
9
+ # to the server.
10
+ def process_metrics
11
+ logger.debug "Processing metrics"
12
+ run_samplers
13
+ payload = layaway.deposit_and_deliver
14
+ metrics = payload[:metrics]
15
+ slow_transactions = payload[:slow_transactions]
16
+ if payload.any?
17
+ add_metric_ids(metrics)
18
+ logger.warn "Some data may be lost - metric size is at limit" if metrics.size == ScoutApm::Store::MAX_SIZE
19
+ # for debugging, count the total number of requests
20
+ controller_count = 0
21
+ metrics.each do |meta,stats|
22
+ if meta.metric_name =~ /\AController/
23
+ controller_count += stats.call_count
24
+ end
25
+ end
26
+ payload = Marshal.dump(:metrics => metrics, :slow_transactions => slow_transactions)
27
+ slow_transactions_kb = Marshal.dump(slow_transactions).size/1024 # just for performance debugging
28
+ logger.debug "#{config.settings['name']} Delivering total payload [#{payload.size/1024} KB] for #{controller_count} requests and slow transactions [#{slow_transactions_kb} KB] for #{slow_transactions.size} transactions of durations: #{slow_transactions.map(&:total_call_time).join(',')}."
29
+ response = post( checkin_uri,
30
+ payload,
31
+ "Content-Type" => "application/octet-stream" )
32
+ if response and response.is_a?(Net::HTTPSuccess)
33
+ directives = Marshal.load(response.body)
34
+ self.metric_lookup.merge!(directives[:metric_lookup])
35
+ if directives[:reset]
36
+ logger.info "Resetting metric_lookup."
37
+ self.metric_lookup = Hash.new
38
+ end
39
+ logger.debug "Metric Cache Size: #{metric_lookup.size}"
40
+ elsif response
41
+ logger.warn "Error on checkin to #{checkin_uri.to_s}: #{response.inspect}"
42
+ end
43
+ end
44
+ rescue
45
+ logger.warn "Error on checkin to #{checkin_uri.to_s}"
46
+ logger.info $!.message
47
+ logger.debug $!.backtrace
48
+ end
49
+
50
+ # Before reporting, lookup metric_id for each MetricMeta. This speeds up
51
+ # reporting on the server-side.
52
+ def add_metric_ids(metrics)
53
+ metrics.each do |meta,stats|
54
+ if metric_id = metric_lookup[meta]
55
+ meta.metric_id = metric_id
56
+ end
57
+ end
58
+ end
59
+
60
+ def checkin_uri
61
+ URI.parse("#{config.settings['host']}/apps/checkin.scout?key=#{config.settings['key']}&name=#{CGI.escape(config.settings['name'])}")
62
+ end
63
+
64
+ def post(uri, body, headers = Hash.new)
65
+ response = nil
66
+ request(uri) do |connection|
67
+ post = Net::HTTP::Post.new( uri.path +
68
+ (uri.query ? ('?' + uri.query) : ''),
69
+ HTTP_HEADERS.merge(headers) )
70
+ post.body = body
71
+ response=connection.request(post)
72
+ end
73
+ response
74
+ end
75
+
76
+ def request(uri, &connector)
77
+ response = nil
78
+ response = http(uri).start(&connector)
79
+ logger.debug "got response: #{response.inspect}"
80
+ case response
81
+ when Net::HTTPSuccess, Net::HTTPNotModified
82
+ logger.debug "/checkin OK"
83
+ when Net::HTTPBadRequest
84
+ logger.warn "/checkin FAILED: The Account Key [#{config.settings['key']}] is invalid."
85
+ else
86
+ logger.debug "/checkin FAILED: #{response.inspect}"
87
+ end
88
+ rescue Exception
89
+ logger.debug "Exception sending request to server: #{$!.message}\n#{$!.backtrace}"
90
+ ensure
91
+ response
92
+ end
93
+
94
+ # Take care of the http proxy, if specified in config.
95
+ # Given a blank string, the proxy_uri URI instance's host/port/user/pass will be nil.
96
+ # Net::HTTP::Proxy returns a regular Net::HTTP class if the first argument (host) is nil.
97
+ def http(url)
98
+ proxy_uri = URI.parse(config.settings['proxy'].to_s)
99
+ http = Net::HTTP::Proxy(proxy_uri.host,proxy_uri.port,proxy_uri.user,proxy_uri.password).new(url.host, url.port)
100
+ if url.is_a?(URI::HTTPS)
101
+ http.use_ssl = true
102
+ http.ca_file = CA_FILE
103
+ http.verify_mode = VERIFY_MODE
104
+ end
105
+ http
106
+ end
107
+ end # module Reporting
108
+ include Reporting
109
+ end # class Agent
110
+ end # module ScoutApm
@@ -0,0 +1,217 @@
1
+ module ScoutApm
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 tshe 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
+ # see self.instance
11
+ @@instance = nil
12
+
13
+ # Accessors below are for associated classes
14
+ attr_accessor :store
15
+ attr_accessor :layaway
16
+ attr_accessor :config
17
+ attr_accessor :environment
18
+
19
+ attr_accessor :logger
20
+ attr_accessor :log_file # path to the log file
21
+ attr_accessor :options # options passed to the agent when +#start+ is called.
22
+ attr_accessor :metric_lookup # Hash used to lookup metric ids based on their name and scope
23
+
24
+ # All access to the agent is thru this class method to ensure multiple Agent instances are not initialized per-Ruby process.
25
+ def self.instance(options = {})
26
+ @@instance ||= self.new(options)
27
+ end
28
+
29
+ # Note - this doesn't start instruments or the worker thread. This is handled via +#start+ as we don't
30
+ # want to start the worker thread or install instrumentation if (1) disabled for this environment (2) a worker thread shouldn't
31
+ # be started (when forking).
32
+ def initialize(options = {})
33
+ @started = false
34
+ @options ||= options
35
+ @store = ScoutApm::Store.new
36
+ @layaway = ScoutApm::Layaway.new
37
+ @config = ScoutApm::Config.new(options[:config_path])
38
+ @metric_lookup = Hash.new
39
+ @process_cpu=ScoutApm::Instruments::Process::ProcessCpu.new(environment.processors)
40
+ @process_memory=ScoutApm::Instruments::Process::ProcessMemory.new
41
+ end
42
+
43
+ def environment
44
+ @environment ||= ScoutApm::Environment.new
45
+ end
46
+
47
+ # This is called via +ScoutApm::Agent.instance.start+ when ScoutApm is required in a Ruby application.
48
+ # It initializes the agent and starts the worker thread (if appropiate).
49
+ def start(options = {})
50
+ @options.merge!(options)
51
+ init_logger
52
+ logger.info "Attempting to start Scout Agent [#{ScoutApm::VERSION}] on [#{Socket.gethostname}]"
53
+ if !config.settings['monitor'] and !@options[:force]
54
+ logger.warn "Monitoring isn't enabled for the [#{environment.env}] environment."
55
+ return false
56
+ elsif !environment.app_server
57
+ logger.warn "Couldn't find a supported app server. Not starting agent."
58
+ return false
59
+ elsif started?
60
+ logger.warn "Already started agent."
61
+ return false
62
+ end
63
+ @started = true
64
+ logger.info "Starting monitoring. Framework [#{environment.framework}] App Server [#{environment.app_server}]."
65
+ start_instruments
66
+ if !start_background_worker?
67
+ logger.debug "Not starting worker thread"
68
+ install_passenger_events if environment.app_server == :passenger
69
+ install_unicorn_worker_loop if environment.app_server == :unicorn
70
+ install_rainbows_worker_loop if environment.app_server == :rainbows
71
+ return
72
+ end
73
+ start_background_worker
74
+ handle_exit
75
+ logger.info "Scout Agent [#{ScoutApm::VERSION}] Initialized"
76
+ end
77
+
78
+ # at_exit, calls Agent#shutdown to wrapup metric reporting.
79
+ def handle_exit
80
+ if environment.sinatra? || environment.jruby? || environment.rubinius?
81
+ logger.debug "Exit handler not supported"
82
+ else
83
+ at_exit do
84
+ logger.debug "Shutdown!"
85
+ # MRI 1.9 bug drops exit codes.
86
+ # http://bugs.ruby-lang.org/issues/5218
87
+ if environment.ruby_19?
88
+ status = $!.status if $!.is_a?(SystemExit)
89
+ shutdown
90
+ exit status if status
91
+ else
92
+ shutdown
93
+ end
94
+ end # at_exit
95
+ end
96
+ end
97
+
98
+ # Called via an at_exit handler, it (1) stops the background worker and (2) runs it a final time.
99
+ # The final run ensures metrics are stored locally to the layaway / reported to scoutapp.com. Otherwise,
100
+ # in-memory metrics would be lost and a gap would appear on restarts.
101
+ def shutdown
102
+ return if !started?
103
+ @background_worker.stop
104
+ @background_worker.run_once
105
+ end
106
+
107
+ def started?
108
+ @started
109
+ end
110
+
111
+ def gem_root
112
+ File.expand_path(File.join("..","..",".."), __FILE__)
113
+ end
114
+
115
+ # The worker thread will automatically start UNLESS:
116
+ # * A supported application server isn't detected (example: running via Rails console)
117
+ # * A supported application server is detected, but it forks (Passenger). In this case,
118
+ # the agent is started in the forked process.
119
+ def start_background_worker?
120
+ !environment.forking? or environment.app_server == :thin
121
+ end
122
+
123
+ def install_passenger_events
124
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
125
+ logger.debug "Passenger is starting a worker process. Starting worker thread."
126
+ self.class.instance.start_background_worker
127
+ end
128
+ # The agent's at_exit hook doesn't run when a Passenger process stops.
129
+ # This does run when a process stops.
130
+ PhusionPassenger.on_event(:stopping_worker_process) do
131
+ logger.debug "Passenger is stopping a worker process, shutting down the agent."
132
+ ScoutApm::Agent.instance.shutdown
133
+ end
134
+ end
135
+
136
+ def install_unicorn_worker_loop
137
+ logger.debug "Installing Unicorn worker loop."
138
+ Unicorn::HttpServer.class_eval do
139
+ old = instance_method(:worker_loop)
140
+ define_method(:worker_loop) do |worker|
141
+ ScoutApm::Agent.instance.start_background_worker
142
+ old.bind(self).call(worker)
143
+ end
144
+ end
145
+ end
146
+
147
+ def install_rainbows_worker_loop
148
+ logger.debug "Installing Rainbows worker loop."
149
+ Rainbows::HttpServer.class_eval do
150
+ old = instance_method(:worker_loop)
151
+ define_method(:worker_loop) do |worker|
152
+ ScoutApm::Agent.instance.start_background_worker
153
+ old.bind(self).call(worker)
154
+ end
155
+ end
156
+ end
157
+
158
+ # Creates the worker thread. The worker thread is a loop that runs continuously. It sleeps for +Agent#period+ and when it wakes,
159
+ # processes data, either saving it to disk or reporting to Scout.
160
+ def start_background_worker
161
+ logger.debug "Creating worker thread."
162
+ @background_worker = ScoutApm::BackgroundWorker.new
163
+ @background_worker_thread = Thread.new do
164
+ @background_worker.start { process_metrics }
165
+ end # thread new
166
+ logger.debug "Done creating worker thread."
167
+ end
168
+
169
+ # Called from #process_metrics, which is run via the background worker.
170
+ def run_samplers
171
+ begin
172
+ cpu_util=@process_cpu.run # returns a hash
173
+ logger.debug "Process CPU: #{cpu_util.inspect} [#{environment.processors} CPU(s)]"
174
+ store.track!("CPU/Utilization",cpu_util,:scope => nil) if cpu_util
175
+ rescue => e
176
+ logger.info "Error reading ProcessCpu"
177
+ logger.debug e.message
178
+ logger.debug e.backtrace.join("\n")
179
+ end
180
+
181
+ begin
182
+ mem_usage=@process_memory.run # returns a single number, in MB
183
+ logger.debug "Process Memory: #{mem_usage}MB"
184
+ store.track!("Memory/Physical",mem_usage,:scope => nil) if mem_usage
185
+ rescue => e
186
+ logger.info "Error reading ProcessMemory"
187
+ logger.debug e.message
188
+ logger.debug e.backtrace.join("\n")
189
+ end
190
+ end
191
+
192
+ # Loads the instrumention logic.
193
+ def load_instruments
194
+ case environment.framework
195
+ when :rails
196
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/rails/action_controller_instruments.rb'))
197
+ when :rails3_or_4
198
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/rails3_or_4/action_controller_instruments.rb'))
199
+ end
200
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/active_record_instruments.rb'))
201
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/net_http.rb'))
202
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/moped_instruments.rb'))
203
+ require File.expand_path(File.join(File.dirname(__FILE__),'instruments/mongoid_instruments.rb'))
204
+ rescue
205
+ logger.warn "Exception loading instruments:"
206
+ logger.warn $!.message
207
+ logger.warn $!.backtrace
208
+ end
209
+
210
+ # Injects instruments into the Ruby application.
211
+ def start_instruments
212
+ logger.debug "Installing instrumentation"
213
+ load_instruments
214
+ end
215
+
216
+ end # class Agent
217
+ end # module ScoutApm
@@ -0,0 +1,43 @@
1
+ # Used to run a given task every 60 seconds.
2
+ class ScoutApm::BackgroundWorker
3
+ # in seconds, time between when the worker thread wakes up and runs.
4
+ PERIOD = 60
5
+
6
+ def initialize
7
+ @keep_running = true
8
+ end
9
+
10
+ def stop
11
+ @keep_running = false
12
+ end
13
+
14
+ # Runs the task passed to +start+ once.
15
+ def run_once
16
+ @task.call if @task
17
+ end
18
+
19
+ # Starts running the passed block every 60 seconds (starting now).
20
+ def start(&block)
21
+ @task = block
22
+ begin
23
+ ScoutApm::Agent.instance.logger.debug "Starting Background Worker, running every #{PERIOD} seconds"
24
+ next_time = Time.now
25
+ while @keep_running do
26
+ now = Time.now
27
+ while now < next_time
28
+ sleep_time = next_time - now
29
+ sleep(sleep_time) if sleep_time > 0
30
+ now = Time.now
31
+ end
32
+ @task.call
33
+ while next_time <= now
34
+ next_time += PERIOD
35
+ end
36
+ end
37
+ rescue
38
+ ScoutApm::Agent.instance.logger.debug "Background Worker Exception!!!!!!!"
39
+ ScoutApm::Agent.instance.logger.debug $!.message
40
+ ScoutApm::Agent.instance.logger.debug $!.backtrace
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,42 @@
1
+ module ScoutApm
2
+ class Config
3
+ DEFAULTS = {
4
+ 'host' => 'https://apm.scoutapp.com',
5
+ 'log_level' => 'info'
6
+ }
7
+
8
+ def initialize(config_path = nil)
9
+ @config_path = config_path
10
+ end
11
+
12
+ def settings
13
+ return @settings if @settings
14
+ load_file
15
+ end
16
+
17
+ def config_path
18
+ @config_path || File.join(ScoutApm::Agent.instance.environment.root,"config","scout_apm.yml")
19
+ end
20
+
21
+ def config_file
22
+ File.expand_path(config_path)
23
+ end
24
+
25
+ def load_file
26
+ begin
27
+ if !File.exist?(config_file)
28
+ ScoutApm::Agent.instance.logger.warn "No config file found at [#{config_file}]."
29
+ @settings = {}
30
+ else
31
+ @settings = YAML.load(ERB.new(File.read(config_file)).result(binding))[ScoutApm::Agent.instance.environment.env] || {}
32
+ end
33
+ rescue Exception => e
34
+ ScoutApm::Agent.instance.logger.warn "Unable to load the config file."
35
+ ScoutApm::Agent.instance.logger.warn e.message
36
+ ScoutApm::Agent.instance.logger.warn e.backtrace
37
+ @settings = {}
38
+ end
39
+ @settings = DEFAULTS.merge(@settings)
40
+ end
41
+ end # Config
42
+ end # ScoutApm
@@ -0,0 +1,105 @@
1
+ # Encapsulates adding context to requests. Context is stored via a simple Hash.
2
+ #
3
+ # There are 2 types of context: User and Extra.
4
+ # For user-specific context, use @Context#add_user@.
5
+ # For misc context, use @Context#add@.
6
+ class ScoutApm::Context
7
+
8
+ def initialize
9
+ @extra = {}
10
+ @user = {}
11
+ end
12
+
13
+ # Generates a hash representation of the Context.
14
+ # Example: {:monthly_spend => 100, :user => {:ip => '127.0.0.1'}}
15
+ def to_hash
16
+ @extra.merge({:user => @user})
17
+ end
18
+
19
+ def self.current
20
+ Thread.current[:scout_context] ||= new
21
+ end
22
+
23
+ def self.clear!
24
+ Thread.current[:scout_context] = nil
25
+ end
26
+
27
+ # Add context
28
+ # ScoutApm::Context.add(account: current_account.name)
29
+ def add(hash)
30
+ update_context(:extra,hash)
31
+ end
32
+
33
+ def add_user(hash)
34
+ update_context(:user,hash)
35
+ end
36
+
37
+ # Convenience accessor so you can just call @ScoutAPM::Context#add@
38
+ def self.add(hash)
39
+ self.current.add(hash)
40
+ end
41
+
42
+ # Convenience accessor so you can just call @ScoutAPM::Context#add_user@
43
+ def self.add_user(hash)
44
+ self.current.add_user(hash)
45
+ end
46
+
47
+ private
48
+
49
+ def update_context(attr,hash)
50
+ valid_hash = Hash.new
51
+ # iterate over the hash of new context, adding to the valid_hash if validation checks pass.
52
+ hash.each do |key,value|
53
+ # does both checks so we can get logging info on the value even if the key is invalid.
54
+ key_valid = key_valid?({key => value})
55
+ value_valid = value_valid?({key => value})
56
+ if key_valid and value_valid
57
+ valid_hash[key] = value
58
+ end
59
+ end
60
+
61
+ if valid_hash.any?
62
+ instance_variable_get("@#{attr.to_s}").merge!(valid_hash)
63
+ end
64
+ end
65
+
66
+ # Returns true if the obj is one of the provided valid classes.
67
+ def valid_type?(classes, obj)
68
+ valid_type = false
69
+ classes.each do |klass|
70
+ if obj.is_a?(klass)
71
+ valid_type = true
72
+ break
73
+ end
74
+ end
75
+ valid_type
76
+ end
77
+
78
+ # take the entire Hash vs. just the value so the logger output is more helpful on error.
79
+ def value_valid?(key_value)
80
+ # ensure one of our accepted types.
81
+ value = key_value.values.last
82
+ if !valid_type?([String, Symbol, Numeric, Time, Date, TrueClass, FalseClass],value)
83
+ ScoutApm::Agent.instance.logger.warn "The value for [#{key_value.keys.first}] is not a valid type [#{value.class}]."
84
+ false
85
+ else
86
+ true
87
+ end
88
+ end
89
+
90
+ # for consistently with #value_valid?, takes a hash eventhough the value isn't yet used.
91
+ def key_valid?(key_value)
92
+ key = key_value.keys.first
93
+ # ensure a string or a symbol
94
+ if !valid_type?([String, Symbol],key)
95
+ ScoutApm::Agent.instance.logger.warn "The key [#{key}] is not a valid type [#{key.class}]."
96
+ return false
97
+ end
98
+ # only alphanumeric, dash, and underscore allowed.
99
+ if key.to_s.match(/[^\w-]/)
100
+ ScoutApm::Agent.instance.logger.warn "They key name [#{key}] is not valid."
101
+ return false
102
+ end
103
+ true
104
+ end
105
+ end
@@ -0,0 +1,135 @@
1
+ # Used to retrieve environment information for this application.
2
+ module ScoutApm
3
+ class Environment
4
+ def env
5
+ @env ||= case framework
6
+ when :rails then RAILS_ENV.dup
7
+ when :rails3_or_4 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_or_4
20
+ end
21
+ when defined?(::Sinatra) && defined?(::Sinatra::Base) then :sinatra
22
+ else :ruby
23
+ end
24
+ end
25
+
26
+ def processors
27
+ return @processors if @processors
28
+ unless @processors
29
+ proc_file = '/proc/cpuinfo'
30
+ if !File.exist?(proc_file)
31
+ @processors = 1
32
+ elsif `cat #{proc_file} | grep 'model name' | wc -l` =~ /(\d+)/
33
+ @processors = $1.to_i
34
+ end
35
+ if @processors < 1
36
+ @processors = 1
37
+ end
38
+ end
39
+ @processors
40
+ end
41
+
42
+ def root
43
+ if framework == :rails
44
+ RAILS_ROOT.to_s
45
+ elsif framework == :rails3_or_4
46
+ Rails.root
47
+ elsif framework == :sinatra
48
+ Sinatra::Application.root
49
+ else
50
+ '.'
51
+ end
52
+ end
53
+
54
+ # This needs to be improved. Frequently, multiple app servers gem are present and which
55
+ # ever is checked first becomes the designated app server.
56
+ #
57
+ # I've put Thin and Webrick last as they are often used in development and included in Gemfiles
58
+ # but less likely used in production.
59
+ #
60
+ # Next step: (1) list out all detected app servers (2) install hooks for those that need it (passenger, rainbows, unicorn).
61
+ #
62
+ # Believe the biggest downside is the master process for forking app servers will get a background worker. Not sure how this will
63
+ # impact metrics (it shouldn't process requests).
64
+ def app_server
65
+ @app_server ||= if passenger? then :passenger
66
+ elsif rainbows? then :rainbows
67
+ elsif unicorn? then :unicorn
68
+ elsif thin? then :thin
69
+ elsif webrick? then :webrick
70
+ else nil
71
+ end
72
+ end
73
+
74
+ ### app server related-checks
75
+
76
+ def thin?
77
+ if defined?(::Thin) && defined?(::Thin::Server)
78
+ # Ensure Thin is actually initialized. It could just be required and not running.
79
+ ObjectSpace.each_object(Thin::Server) { |x| return true }
80
+ false
81
+ end
82
+ end
83
+
84
+ # Called via +#forking?+ since Passenger forks. Adds an event listener to start the worker thread
85
+ # inside the passenger worker process.
86
+ # Background: http://www.modrails.com/documentation/Users%20guide%20Nginx.html#spawning%5Fmethods%5Fexplained
87
+ def passenger?
88
+ (defined?(::Passenger) && defined?(::Passenger::AbstractServer)) || defined?(::PhusionPassenger)
89
+ end
90
+
91
+ def webrick?
92
+ defined?(::WEBrick) && defined?(::WEBrick::VERSION)
93
+ end
94
+
95
+ def rainbows?
96
+ if defined?(::Rainbows) && defined?(::Rainbows::HttpServer)
97
+ ObjectSpace.each_object(::Rainbows::HttpServer) { |x| return true }
98
+ end
99
+ end
100
+
101
+ def unicorn?
102
+ if defined?(::Unicorn) && defined?(::Unicorn::HttpServer)
103
+ # Ensure Unicorn is actually initialized. It could just be required and not running.
104
+ ObjectSpace.each_object(::Unicorn::HttpServer) { |x| return true }
105
+ end
106
+ end
107
+
108
+ # If forking, don't start worker thread in the master process. Since it's started as a Thread, it won't survive
109
+ # the fork.
110
+ def forking?
111
+ passenger? or unicorn? or rainbows?
112
+ end
113
+
114
+ ### ruby checks
115
+
116
+ def rubinius?
117
+ RUBY_VERSION =~ /rubinius/i
118
+ end
119
+
120
+ def jruby?
121
+ defined?(JRuby)
122
+ end
123
+
124
+ def ruby_19?
125
+ defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby" && RUBY_VERSION.match(/^1\.9/)
126
+ end
127
+
128
+ ### framework checks
129
+
130
+ def sinatra?
131
+ defined?(Sinatra::Application)
132
+ end
133
+
134
+ end # class Environemnt
135
+ end