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