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,85 @@
|
|
1
|
+
module ScoutApm::Instruments
|
2
|
+
# Contains ActiveRecord instrument, aliasing +ActiveRecord::ConnectionAdapters::AbstractAdapter#log+ calls
|
3
|
+
# to trace calls to the database.
|
4
|
+
module ActiveRecordInstruments
|
5
|
+
def self.included(instrumented_class)
|
6
|
+
ScoutApm::Agent.instance.logger.debug "Instrumenting #{instrumented_class.inspect}"
|
7
|
+
instrumented_class.class_eval do
|
8
|
+
unless instrumented_class.method_defined?(:log_without_scout_instruments)
|
9
|
+
alias_method :log_without_scout_instruments, :log
|
10
|
+
alias_method :log, :log_with_scout_instruments
|
11
|
+
protected :log
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end # self.included
|
15
|
+
|
16
|
+
def log_with_scout_instruments(*args, &block)
|
17
|
+
sql, name = args
|
18
|
+
self.class.instrument(scout_ar_metric_name(sql,name), :desc => scout_sanitize_sql(sql)) do
|
19
|
+
log_without_scout_instruments(sql, name, &block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def scout_ar_metric_name(sql,name)
|
24
|
+
# sql: SELECT "places".* FROM "places" ORDER BY "places"."position" ASC
|
25
|
+
# name: Place Load
|
26
|
+
if name && (parts = name.split " ") && parts.size == 2
|
27
|
+
model = parts.first
|
28
|
+
operation = parts.last.downcase
|
29
|
+
metric_name = case operation
|
30
|
+
when 'load' then 'find'
|
31
|
+
when 'indexes', 'columns' then nil # not under developer control
|
32
|
+
when 'destroy', 'find', 'save', 'create', 'exists' then operation
|
33
|
+
when 'update' then 'save'
|
34
|
+
else
|
35
|
+
if model == 'Join'
|
36
|
+
operation
|
37
|
+
end
|
38
|
+
end
|
39
|
+
metric = "ActiveRecord/#{model}/#{metric_name}" if metric_name
|
40
|
+
metric = "ActiveRecord/SQL/other" if metric.nil?
|
41
|
+
else
|
42
|
+
metric = "ActiveRecord/SQL/Unknown"
|
43
|
+
end
|
44
|
+
metric
|
45
|
+
end
|
46
|
+
|
47
|
+
# Removes actual values from SQL. Used to both obfuscate the SQL and group
|
48
|
+
# similar queries in the UI.
|
49
|
+
def scout_sanitize_sql(sql)
|
50
|
+
return nil if sql.length > 1000 # safeguard - don't sanitize large SQL statements
|
51
|
+
sql = sql.dup
|
52
|
+
sql.gsub!(/\\"/, '') # removing escaping double quotes
|
53
|
+
sql.gsub!(/\\'/, '') # removing escaping single quotes
|
54
|
+
sql.gsub!(/'(?:[^']|'')*'/, '?') # removing strings (single quote)
|
55
|
+
sql.gsub!(/"(?:[^"]|"")*"/, '?') # removing strings (double quote)
|
56
|
+
sql.gsub!(/\b\d+\b/, '?') # removing integers
|
57
|
+
sql.gsub!(/\?(,\?)+/,'?') # replace multiple ? w/a single ?
|
58
|
+
sql
|
59
|
+
end
|
60
|
+
|
61
|
+
end # module ActiveRecordInstruments
|
62
|
+
end # module Instruments
|
63
|
+
|
64
|
+
def add_instruments
|
65
|
+
if defined?(ActiveRecord) && defined?(ActiveRecord::Base)
|
66
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
|
67
|
+
include ::ScoutApm::Instruments::ActiveRecordInstruments
|
68
|
+
include ::ScoutApm::Tracer
|
69
|
+
end
|
70
|
+
ActiveRecord::Base.class_eval do
|
71
|
+
include ::ScoutApm::Tracer
|
72
|
+
end
|
73
|
+
end
|
74
|
+
rescue
|
75
|
+
ScoutApm::Agent.instance.logger.warn "ActiveRecord instrumentation exception: #{$!.message}"
|
76
|
+
end
|
77
|
+
|
78
|
+
if defined?(::Rails) && ::Rails::VERSION::MAJOR.to_i == 3 && ::Rails.respond_to?(:configuration)
|
79
|
+
Rails.configuration.after_initialize do
|
80
|
+
ScoutApm::Agent.instance.logger.debug "Adding ActiveRecord instrumentation to a Rails 3 app"
|
81
|
+
add_instruments
|
82
|
+
end
|
83
|
+
else
|
84
|
+
add_instruments
|
85
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# Mongoid versions that use Moped should instrument Moped.
|
2
|
+
if defined?(::Mongoid) and !defined?(::Moped)
|
3
|
+
ScoutApm::Agent.instance.logger.debug "Instrumenting Mongoid"
|
4
|
+
Mongoid::Collection.class_eval do
|
5
|
+
include ScoutApm::Tracer
|
6
|
+
(Mongoid::Collections::Operations::ALL - [:<<, :[]]).each do |method|
|
7
|
+
instrument_method method, :metric_name => "MongoDB/\#{@klass}/#{method}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
if defined?(::Moped)
|
2
|
+
ScoutApm::Agent.instance.logger.debug "Instrumenting Moped"
|
3
|
+
Moped::Node.class_eval do
|
4
|
+
include ScoutApm::Tracer
|
5
|
+
def process_with_scout_instruments(operation, &callback)
|
6
|
+
if operation.respond_to?(:collection)
|
7
|
+
collection = operation.collection
|
8
|
+
self.class.instrument("MongoDB/Process/#{collection}/#{operation.class.to_s.split('::').last}", :desc => scout_sanitize_log(operation.log_inspect)) do
|
9
|
+
process_without_scout_instruments(operation, &callback)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
alias_method :process_without_scout_instruments, :process
|
14
|
+
alias_method :process, :process_with_scout_instruments
|
15
|
+
|
16
|
+
# replaces values w/ ?
|
17
|
+
def scout_sanitize_log(log)
|
18
|
+
return nil if log.length > 1000 # safeguard - don't sanitize large SQL statements
|
19
|
+
log.gsub(/(=>")((?:[^"]|"")*)"/) do
|
20
|
+
$1 + '?' + '"'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
if defined?(::Net) && defined?(Net::HTTP)
|
2
|
+
ScoutApm::Agent.instance.logger.debug "Instrumenting Net::HTTP"
|
3
|
+
Net::HTTP.class_eval do
|
4
|
+
include ScoutApm::Tracer
|
5
|
+
|
6
|
+
def request_with_scout_instruments(*args,&block)
|
7
|
+
self.class.instrument("HTTP/request", :desc => "#{(@address+args.first.path.split('?').first)[0..99]}") do
|
8
|
+
request_without_scout_instruments(*args,&block)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
alias request_without_scout_instruments request
|
12
|
+
alias request request_with_scout_instruments
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ScoutApm::Instruments
|
2
|
+
module Process
|
3
|
+
class ProcessCpu
|
4
|
+
def initialize(num_processors)
|
5
|
+
@num_processors = num_processors || 1
|
6
|
+
end
|
7
|
+
|
8
|
+
def run
|
9
|
+
res=nil
|
10
|
+
now = Time.now
|
11
|
+
t = ::Process.times
|
12
|
+
if @last_run
|
13
|
+
elapsed_time = now - @last_run
|
14
|
+
if elapsed_time >= 1
|
15
|
+
user_time_since_last_sample = t.utime - @last_utime
|
16
|
+
system_time_since_last_sample = t.stime - @last_stime
|
17
|
+
res = ((user_time_since_last_sample + system_time_since_last_sample)/(elapsed_time * @num_processors))*100
|
18
|
+
end
|
19
|
+
end
|
20
|
+
@last_utime = t.utime
|
21
|
+
@last_stime = t.stime
|
22
|
+
@last_run = now
|
23
|
+
return res
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ScoutApm::Instruments
|
2
|
+
module Process
|
3
|
+
class ProcessMemory
|
4
|
+
def run
|
5
|
+
res=nil
|
6
|
+
platform = RUBY_PLATFORM.downcase
|
7
|
+
|
8
|
+
if platform =~ /linux/
|
9
|
+
res = get_mem_from_procfile
|
10
|
+
elsif platform =~ /darwin9/ # 10.5
|
11
|
+
res = get_mem_from_shell("ps -o rsz")
|
12
|
+
elsif platform =~ /darwin1[01]/ # 10.6 & 10.7
|
13
|
+
res = get_mem_from_shell("ps -o rss")
|
14
|
+
end
|
15
|
+
return res
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def get_mem_from_procfile
|
21
|
+
res = nil
|
22
|
+
proc_status = File.open(procfile, "r") { |f| f.read_nonblock(4096).strip }
|
23
|
+
if proc_status =~ /RSS:\s*(\d+) kB/i
|
24
|
+
res= $1.to_f / 1024.0
|
25
|
+
end
|
26
|
+
res
|
27
|
+
end
|
28
|
+
|
29
|
+
def procfile
|
30
|
+
"/proc/#{$$}/status"
|
31
|
+
end
|
32
|
+
|
33
|
+
# memory in MB the current process is using
|
34
|
+
def get_mem_from_shell(command)
|
35
|
+
res = `#{command} #{$$}`.split("\n")[1].to_f / 1024.0 #rescue nil
|
36
|
+
res
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ScoutApm::Instruments
|
2
|
+
module ActionControllerInstruments
|
3
|
+
def self.included(instrumented_class)
|
4
|
+
ScoutApm::Agent.instance.logger.debug "Instrumenting #{instrumented_class.inspect}"
|
5
|
+
instrumented_class.class_eval do
|
6
|
+
unless instrumented_class.method_defined?(:perform_action_without_scout_instruments)
|
7
|
+
alias_method :perform_action_without_scout_instruments, :perform_action
|
8
|
+
alias_method :perform_action, :perform_action_with_scout_instruments
|
9
|
+
private :perform_action
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end # self.included
|
13
|
+
|
14
|
+
# In addition to instrumenting actions, this also sets the scope to the controller action name. The scope is later
|
15
|
+
# applied to metrics recorded during this transaction. This lets us associate ActiveRecord calls with
|
16
|
+
# specific controller actions.
|
17
|
+
def perform_action_with_scout_instruments(*args, &block)
|
18
|
+
scout_controller_action = "Controller/#{controller_path}/#{action_name}"
|
19
|
+
self.class.scout_apm_trace(scout_controller_action, :uri => request.request_uri, :ip => request.remote_ip) do
|
20
|
+
perform_action_without_scout_instruments(*args, &block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if defined?(ActionController) && defined?(ActionController::Base)
|
27
|
+
ActionController::Base.class_eval do
|
28
|
+
include ScoutApm::Tracer
|
29
|
+
include ::ScoutApm::Instruments::ActionControllerInstruments
|
30
|
+
|
31
|
+
def rescue_action_with_scout(exception)
|
32
|
+
ScoutApm::Agent.instance.store.track!("Errors/Request",1, :scope => nil)
|
33
|
+
ScoutApm::Agent.instance.store.ignore_transaction!
|
34
|
+
rescue_action_without_scout exception
|
35
|
+
end
|
36
|
+
|
37
|
+
alias_method :rescue_action_without_scout, :rescue_action
|
38
|
+
alias_method :rescue_action, :rescue_action_with_scout
|
39
|
+
protected :rescue_action
|
40
|
+
end
|
41
|
+
ScoutApm::Agent.instance.logger.debug "Instrumenting ActionView::Template"
|
42
|
+
ActionView::Template.class_eval do
|
43
|
+
include ::ScoutApm::Tracer
|
44
|
+
instrument_method :render, :metric_name => 'View/#{path[%r{^(/.*/)?(.*)$},2]}/Rendering', :scope => true
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# Rails 3/4
|
2
|
+
module ScoutApm::Instruments
|
3
|
+
module ActionControllerInstruments
|
4
|
+
# Instruments the action and tracks errors.
|
5
|
+
def process_action(*args)
|
6
|
+
scout_controller_action = "Controller/#{controller_path}/#{action_name}"
|
7
|
+
#ScoutApm::Agent.instance.logger.debug "Processing #{scout_controller_action}"
|
8
|
+
self.class.scout_apm_trace(scout_controller_action, :uri => request.fullpath, :ip => request.remote_ip) do
|
9
|
+
begin
|
10
|
+
super
|
11
|
+
rescue Exception => e
|
12
|
+
ScoutApm::Agent.instance.store.track!("Errors/Request",1, :scope => nil)
|
13
|
+
raise
|
14
|
+
ensure
|
15
|
+
Thread::current[:scout_apm_scope_name] = nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# ActionController::Base is a subclass of ActionController::Metal, so this instruments both
|
23
|
+
# standard Rails requests + Metal.
|
24
|
+
if defined?(ActionController) && defined?(ActionController::Metal)
|
25
|
+
ScoutApm::Agent.instance.logger.debug "Instrumenting ActionController::Metal"
|
26
|
+
ActionController::Metal.class_eval do
|
27
|
+
include ScoutApm::Tracer
|
28
|
+
include ::ScoutApm::Instruments::ActionControllerInstruments
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if defined?(ActionView) && defined?(ActionView::PartialRenderer)
|
33
|
+
ScoutApm::Agent.instance.logger.debug "Instrumenting ActionView::PartialRenderer"
|
34
|
+
ActionView::PartialRenderer.class_eval do
|
35
|
+
include ScoutApm::Tracer
|
36
|
+
instrument_method :render_partial, :metric_name => 'View/#{@template.virtual_path}/Rendering', :scope => true
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# Stores metrics in a file before sending them to the server. Two uses:
|
2
|
+
# 1. A centralized store for multiple Agent processes. This way, only 1 checkin is sent to Scout rather than 1 per-process.
|
3
|
+
# 2. Bundling up reports from multiple timeslices to make updates more efficent server-side.
|
4
|
+
#
|
5
|
+
# Data is stored in a Hash, where the keys are Time.to_i on the minute. The value is a Hash {:metrics => Hash, :slow_transactions => Array}.
|
6
|
+
# When depositing data, the new data is either merged with an existing time or placed in a new key.
|
7
|
+
class ScoutApm::Layaway
|
8
|
+
attr_accessor :file
|
9
|
+
def initialize
|
10
|
+
@file = ScoutApm::LayawayFile.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def deposit_and_deliver
|
14
|
+
new_metrics = ScoutApm::Agent.instance.store.metric_hash
|
15
|
+
log_deposited_metrics(new_metrics)
|
16
|
+
log_deposited_slow_transactions(ScoutApm::Agent.instance.store.slow_transactions)
|
17
|
+
to_deliver = {}
|
18
|
+
file.read_and_write do |old_data|
|
19
|
+
old_data ||= Hash.new
|
20
|
+
# merge data
|
21
|
+
# if (1) there's data in the file and (2) there isn't any data yet for the current minute, this means we've
|
22
|
+
# collected all metrics for the previous slots and we're ready to deliver.
|
23
|
+
#
|
24
|
+
# Example w/2 processes:
|
25
|
+
#
|
26
|
+
# 12:00:34 ---
|
27
|
+
# Process 1: old_data.any? => false, so deposits.
|
28
|
+
# Process 2: old_data_any? => true and old_data[12:00].nil? => false, so deposits.
|
29
|
+
#
|
30
|
+
# 12:01:34 ---
|
31
|
+
# Process 1: old_data.any? => true and old_data[12:01].nil? => true, so delivers metrics.
|
32
|
+
# Process 2: old_data.any? => true and old_data[12:01].nil? => false, so deposits.
|
33
|
+
if old_data.any? and old_data[slot].nil?
|
34
|
+
to_deliver = old_data
|
35
|
+
old_data = Hash.new
|
36
|
+
elsif old_data.any?
|
37
|
+
ScoutApm::Agent.instance.logger.debug "Not yet time to deliver payload for slot [#{Time.at(old_data.keys.sort.last).strftime("%m/%d/%y %H:%M:%S %z")}]"
|
38
|
+
else
|
39
|
+
ScoutApm::Agent.instance.logger.debug "There is no data in the layaway file to deliver."
|
40
|
+
end
|
41
|
+
old_data[slot]=ScoutApm::Agent.instance.store.merge_data_and_clear(old_data[slot] || {:metrics => {}, :slow_transactions => []})
|
42
|
+
log_saved_data(old_data,new_metrics)
|
43
|
+
old_data
|
44
|
+
end
|
45
|
+
to_deliver.any? ? validate_data(to_deliver) : {}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Ensures the data we're sending to the server isn't stale.
|
49
|
+
# This can occur if the agent is collecting data, and app server goes down w/data in the local storage.
|
50
|
+
# When it is restarted later data will remain in local storage but it won't be for the current reporting interval.
|
51
|
+
#
|
52
|
+
# If the data is stale, an empty Hash is returned. Otherwise, the data from the most recent slot is returned.
|
53
|
+
def validate_data(data)
|
54
|
+
data = data.to_a.sort
|
55
|
+
now = Time.now
|
56
|
+
if (most_recent = data.first.first) < now.to_i - 2*60
|
57
|
+
ScoutApm::Agent.instance.logger.debug "Local Storage is stale (#{Time.at(most_recent).strftime("%m/%d/%y %H:%M:%S %z")}). Not sending data."
|
58
|
+
{}
|
59
|
+
else
|
60
|
+
data.first.last
|
61
|
+
end
|
62
|
+
rescue
|
63
|
+
ScoutApm::Agent.instance.logger.debug $!.message
|
64
|
+
ScoutApm::Agent.instance.logger.debug $!.backtrace
|
65
|
+
end
|
66
|
+
|
67
|
+
# Data is stored under timestamp-keys (without the second).
|
68
|
+
def slot
|
69
|
+
t = Time.now
|
70
|
+
t -= t.sec
|
71
|
+
t.to_i
|
72
|
+
end
|
73
|
+
|
74
|
+
def log_deposited_metrics(new_metrics)
|
75
|
+
controller_count = 0
|
76
|
+
new_metrics.each do |meta,stats|
|
77
|
+
if meta.metric_name =~ /\AController/
|
78
|
+
controller_count += stats.call_count
|
79
|
+
end
|
80
|
+
end
|
81
|
+
ScoutApm::Agent.instance.logger.debug "Depositing #{controller_count} requests into #{Time.at(slot).strftime("%m/%d/%y %H:%M:%S %z")} slot."
|
82
|
+
end
|
83
|
+
|
84
|
+
def log_deposited_slow_transactions(new_slow_transactions)
|
85
|
+
ScoutApm::Agent.instance.logger.debug "Depositing #{new_slow_transactions.size} slow transactions into #{Time.at(slot).strftime("%m/%d/%y %H:%M:%S %z")} slot."
|
86
|
+
end
|
87
|
+
|
88
|
+
def log_saved_data(old_data,new_metrics)
|
89
|
+
ScoutApm::Agent.instance.logger.debug "Saving the following #{old_data.size} time slots locally:"
|
90
|
+
old_data.each do |k,v|
|
91
|
+
controller_count = 0
|
92
|
+
new_metrics.each do |meta,stats|
|
93
|
+
if meta.metric_name =~ /\AController/
|
94
|
+
controller_count += stats.call_count
|
95
|
+
end
|
96
|
+
end
|
97
|
+
ScoutApm::Agent.instance.logger.debug "#{Time.at(k).strftime("%m/%d/%y %H:%M:%S %z")} => #{controller_count} requests and #{v[:slow_transactions].size} slow transactions"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# Logic for the serialized file access
|
2
|
+
class ScoutApm::LayawayFile
|
3
|
+
def path
|
4
|
+
"#{ScoutApm::Agent.instance.log_path}/scout_apm.db"
|
5
|
+
end
|
6
|
+
|
7
|
+
def dump(object)
|
8
|
+
Marshal.dump(object)
|
9
|
+
end
|
10
|
+
|
11
|
+
def load(dump)
|
12
|
+
if dump.size == 0
|
13
|
+
ScoutApm::Agent.instance.logger.debug("No data in layaway file.")
|
14
|
+
return nil
|
15
|
+
end
|
16
|
+
Marshal.load(dump)
|
17
|
+
rescue ArgumentError, TypeError => e
|
18
|
+
ScoutApm::Agent.instance.logger.debug("Error loading data from layaway file: #{e.inspect}")
|
19
|
+
ScoutApm::Agent.instance.logger.debug(e.backtrace.inspect)
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def read_and_write
|
24
|
+
File.open(path, File::RDWR | File::CREAT) do |f|
|
25
|
+
f.flock(File::LOCK_EX)
|
26
|
+
begin
|
27
|
+
result = (yield get_data(f))
|
28
|
+
f.rewind
|
29
|
+
f.truncate(0)
|
30
|
+
if result
|
31
|
+
write(f, dump(result))
|
32
|
+
end
|
33
|
+
ensure
|
34
|
+
f.flock(File::LOCK_UN)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
rescue Errno::ENOENT, Exception => e
|
38
|
+
ScoutApm::Agent.instance.logger.error("Unable to access the layaway file [#{e.message}]. The user running the app must have read+write access.")
|
39
|
+
ScoutApm::Agent.instance.logger.debug(e.backtrace.split("\n"))
|
40
|
+
# ensure the in-memory metric hash is cleared so data doesn't continue to accumulate.
|
41
|
+
ScoutApm::Agent.instance.store.metric_hash = {}
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_data(f)
|
45
|
+
data = read_until_end(f)
|
46
|
+
result = load(data)
|
47
|
+
f.truncate(0)
|
48
|
+
result
|
49
|
+
end
|
50
|
+
|
51
|
+
def write(f, string)
|
52
|
+
result = 0
|
53
|
+
while (result < string.length)
|
54
|
+
result += f.write_nonblock(string)
|
55
|
+
end
|
56
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
57
|
+
IO.select(nil, [f])
|
58
|
+
retry
|
59
|
+
end
|
60
|
+
|
61
|
+
def read_until_end(f)
|
62
|
+
contents = ""
|
63
|
+
while true
|
64
|
+
contents << f.read_nonblock(10_000)
|
65
|
+
end
|
66
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
67
|
+
IO.select([f])
|
68
|
+
retry
|
69
|
+
rescue EOFError
|
70
|
+
contents
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Contains the meta information associated with a metric. Used to lookup Metrics in to Store's metric_hash.
|
2
|
+
class ScoutApm::MetricMeta
|
3
|
+
def initialize(metric_name, options = {})
|
4
|
+
@metric_name = metric_name
|
5
|
+
@metric_id = nil
|
6
|
+
@scope = Thread::current[:scout_apm_sub_scope] || Thread::current[:scout_apm_scope_name]
|
7
|
+
@desc = options[:desc]
|
8
|
+
@extra = {}
|
9
|
+
end
|
10
|
+
attr_accessor :metric_id, :metric_name
|
11
|
+
attr_accessor :scope
|
12
|
+
attr_accessor :client_id
|
13
|
+
attr_accessor :desc, :extra
|
14
|
+
|
15
|
+
# To avoid conflicts with different JSON libaries
|
16
|
+
def to_json(*a)
|
17
|
+
%Q[{"metric_id":#{metric_id || 'null'},"metric_name":#{metric_name.to_json},"scope":#{scope.to_json || 'null'}}]
|
18
|
+
end
|
19
|
+
|
20
|
+
def ==(o)
|
21
|
+
self.eql?(o)
|
22
|
+
end
|
23
|
+
|
24
|
+
def hash
|
25
|
+
h = metric_name.downcase.hash
|
26
|
+
h ^= scope.downcase.hash unless scope.nil?
|
27
|
+
h ^= desc.downcase.hash unless desc.nil?
|
28
|
+
h
|
29
|
+
end
|
30
|
+
|
31
|
+
def eql?(o)
|
32
|
+
self.class == o.class && metric_name.downcase.eql?(o.metric_name.downcase) && scope == o.scope && client_id == o.client_id && desc == o.desc
|
33
|
+
end
|
34
|
+
end # class MetricMeta
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Stats that are associated with each instrumented method.
|
2
|
+
class ScoutApm::MetricStats
|
3
|
+
attr_accessor :call_count
|
4
|
+
attr_accessor :min_call_time
|
5
|
+
attr_accessor :max_call_time
|
6
|
+
attr_accessor :total_call_time
|
7
|
+
attr_accessor :total_exclusive_time
|
8
|
+
attr_accessor :sum_of_squares
|
9
|
+
|
10
|
+
def initialize(scoped = false)
|
11
|
+
@scoped = scoped
|
12
|
+
self.call_count = 0
|
13
|
+
self.total_call_time = 0.0
|
14
|
+
self.total_exclusive_time = 0.0
|
15
|
+
self.min_call_time = 0.0
|
16
|
+
self.max_call_time = 0.0
|
17
|
+
self.sum_of_squares = 0.0
|
18
|
+
end
|
19
|
+
|
20
|
+
def update!(call_time,exclusive_time)
|
21
|
+
# If this metric is scoped inside another, use exclusive time for min/max and sum_of_squares. Non-scoped metrics
|
22
|
+
# (like controller actions) track the total call time.
|
23
|
+
t = (@scoped ? exclusive_time : call_time)
|
24
|
+
self.min_call_time = t if self.call_count == 0 or t < min_call_time
|
25
|
+
self.max_call_time = t if self.call_count == 0 or t > max_call_time
|
26
|
+
self.call_count +=1
|
27
|
+
self.total_call_time += call_time
|
28
|
+
self.total_exclusive_time += exclusive_time
|
29
|
+
self.sum_of_squares += (t * t)
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
# combines data from another MetricStats object
|
34
|
+
def combine!(other)
|
35
|
+
self.call_count += other.call_count
|
36
|
+
self.total_call_time += other.total_call_time
|
37
|
+
self.total_exclusive_time += other.total_exclusive_time
|
38
|
+
self.min_call_time = other.min_call_time if self.min_call_time.zero? or other.min_call_time < self.min_call_time
|
39
|
+
self.max_call_time = other.max_call_time if other.max_call_time > self.max_call_time
|
40
|
+
self.sum_of_squares += other.sum_of_squares
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# To avoid conflicts with different JSON libaries handle JSON ourselves.
|
45
|
+
# Time-based metrics are converted to milliseconds from seconds.
|
46
|
+
def to_json(*a)
|
47
|
+
%Q[{"total_exclusive_time":#{total_exclusive_time*1000},"min_call_time":#{min_call_time*1000},"call_count":#{call_count},"sum_of_squares":#{sum_of_squares*1000},"total_call_time":#{total_call_time*1000},"max_call_time":#{max_call_time*1000}}]
|
48
|
+
end
|
49
|
+
end # class MetricStats
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class ScoutApm::SlowTransaction
|
2
|
+
BACKTRACE_THRESHOLD = 0.5 # the minimum threshold to record the backtrace for a metric.
|
3
|
+
BACKTRACE_LIMIT = 5 # Max length of callers to display
|
4
|
+
MAX_SIZE = 100 # Limits the size of the metric hash to prevent a metric explosion.
|
5
|
+
attr_reader :metric_name, :total_call_time, :metrics, :meta, :uri, :context
|
6
|
+
|
7
|
+
# Given a call stack, generates a filtered backtrace that:
|
8
|
+
# * Limits to the app/models, app/controllers, or app/views directories
|
9
|
+
# * Limits to 5 total callers
|
10
|
+
# * Makes the app folder the top-level folder used in trace info
|
11
|
+
def self.backtrace_parser(backtrace)
|
12
|
+
stack = []
|
13
|
+
backtrace.each do |c|
|
14
|
+
if m=c.match(/(\/app\/(controllers|models|views)\/.+)/)
|
15
|
+
stack << m[1]
|
16
|
+
break if stack.size == BACKTRACE_LIMIT
|
17
|
+
end
|
18
|
+
end
|
19
|
+
stack
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(uri,metric_name,total_call_time,metrics,context)
|
23
|
+
@uri = uri
|
24
|
+
@metric_name = metric_name
|
25
|
+
@total_call_time = total_call_time
|
26
|
+
@metrics = metrics
|
27
|
+
@context = context
|
28
|
+
end
|
29
|
+
|
30
|
+
# Used to remove metrics when the payload will be too large.
|
31
|
+
def clear_metrics!
|
32
|
+
@metrics = nil
|
33
|
+
self
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class ScoutApm::StackItem
|
2
|
+
attr_accessor :children_time
|
3
|
+
attr_reader :metric_name, :start_time
|
4
|
+
|
5
|
+
def initialize(metric_name)
|
6
|
+
@metric_name = metric_name
|
7
|
+
@start_time = Time.now
|
8
|
+
@children_time = 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(o)
|
12
|
+
self.eql?(o)
|
13
|
+
end
|
14
|
+
|
15
|
+
def eql?(o)
|
16
|
+
self.class == o.class && metric_name.eql?(o.metric_name)
|
17
|
+
end
|
18
|
+
end # class StackItem
|