scout_apm 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/CHANGELOG.markdown +3 -0
- data/Gemfile +4 -0
- data/README.markdown +42 -0
- data/Rakefile +1 -0
- data/data/cacert.pem +3988 -0
- data/lib/scout_apm/agent/logging.rb +44 -0
- data/lib/scout_apm/agent/reporting.rb +110 -0
- data/lib/scout_apm/agent.rb +217 -0
- data/lib/scout_apm/background_worker.rb +43 -0
- data/lib/scout_apm/config.rb +42 -0
- data/lib/scout_apm/context.rb +105 -0
- data/lib/scout_apm/environment.rb +135 -0
- data/lib/scout_apm/instruments/active_record_instruments.rb +85 -0
- data/lib/scout_apm/instruments/mongoid_instruments.rb +10 -0
- data/lib/scout_apm/instruments/moped_instruments.rb +24 -0
- data/lib/scout_apm/instruments/net_http.rb +14 -0
- data/lib/scout_apm/instruments/process/process_cpu.rb +27 -0
- data/lib/scout_apm/instruments/process/process_memory.rb +40 -0
- data/lib/scout_apm/instruments/rails/action_controller_instruments.rb +46 -0
- data/lib/scout_apm/instruments/rails3_or_4/action_controller_instruments.rb +38 -0
- data/lib/scout_apm/layaway.rb +100 -0
- data/lib/scout_apm/layaway_file.rb +72 -0
- data/lib/scout_apm/metric_meta.rb +34 -0
- data/lib/scout_apm/metric_stats.rb +49 -0
- data/lib/scout_apm/slow_transaction.rb +35 -0
- data/lib/scout_apm/stack_item.rb +18 -0
- data/lib/scout_apm/store.rb +200 -0
- data/lib/scout_apm/tracer.rb +112 -0
- data/lib/scout_apm/version.rb +3 -0
- data/lib/scout_apm.rb +41 -0
- data/scout_apm.gemspec +24 -0
- metadata +78 -0
@@ -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
|