scout_rails_proxy_proxy 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.DS_Store +0 -0
- data/.gitignore +5 -0
- data/CHANGELOG.markdown +50 -0
- data/Gemfile +4 -0
- data/README.markdown +44 -0
- data/Rakefile +1 -0
- data/lib/scout_rails_proxy.rb +32 -0
- data/lib/scout_rails_proxy/agent.rb +319 -0
- data/lib/scout_rails_proxy/config.rb +34 -0
- data/lib/scout_rails_proxy/environment.rb +122 -0
- data/lib/scout_rails_proxy/instruments/active_record_instruments.rb +83 -0
- data/lib/scout_rails_proxy/instruments/net_http.rb +14 -0
- data/lib/scout_rails_proxy/instruments/process/process_cpu.rb +27 -0
- data/lib/scout_rails_proxy/instruments/process/process_memory.rb +40 -0
- data/lib/scout_rails_proxy/instruments/rails/action_controller_instruments.rb +46 -0
- data/lib/scout_rails_proxy/instruments/rails3/action_controller_instruments.rb +38 -0
- data/lib/scout_rails_proxy/instruments/sinatra_instruments.rb +33 -0
- data/lib/scout_rails_proxy/layaway.rb +76 -0
- data/lib/scout_rails_proxy/layaway_file.rb +70 -0
- data/lib/scout_rails_proxy/metric_meta.rb +34 -0
- data/lib/scout_rails_proxy/metric_stats.rb +49 -0
- data/lib/scout_rails_proxy/stack_item.rb +18 -0
- data/lib/scout_rails_proxy/store.rb +159 -0
- data/lib/scout_rails_proxy/tracer.rb +105 -0
- data/lib/scout_rails_proxy/transaction_sample.rb +10 -0
- data/lib/scout_rails_proxy/version.rb +3 -0
- data/scout_rails_proxy.gemspec +24 -0
- metadata +75 -0
@@ -0,0 +1,76 @@
|
|
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
|
+
# Metrics are stored in a Hash, where the keys are Time.to_i on the minute. When depositing data,
|
6
|
+
# metrics are either merged with an existing time or placed in a new key.
|
7
|
+
class ScoutRailsProxy::Layaway
|
8
|
+
attr_accessor :file
|
9
|
+
def initialize
|
10
|
+
@file = ScoutRailsProxy::LayawayFile.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def deposit_and_deliver
|
14
|
+
new_data = ScoutRailsProxy::Agent.instance.store.metric_hash
|
15
|
+
controller_count = 0
|
16
|
+
new_data.each do |meta,stats|
|
17
|
+
if meta.metric_name =~ /\AController/
|
18
|
+
controller_count += stats.call_count
|
19
|
+
end
|
20
|
+
end
|
21
|
+
ScoutRailsProxy::Agent.instance.logger.debug "Depositing #{controller_count} requests into #{Time.at(slot).strftime("%m/%d/%y %H:%M:%S %z")} slot."
|
22
|
+
|
23
|
+
to_deliver = {}
|
24
|
+
file.read_and_write do |old_data|
|
25
|
+
old_data ||= Hash.new
|
26
|
+
# merge data
|
27
|
+
# if the previous minute has ended, its time to send those metrics
|
28
|
+
if old_data.any? and old_data[slot].nil?
|
29
|
+
to_deliver = old_data
|
30
|
+
old_data = Hash.new
|
31
|
+
elsif old_data.any?
|
32
|
+
ScoutRailsProxy::Agent.instance.logger.debug "Not yet time to deliver metrics for slot [#{Time.at(old_data.keys.sort.last).strftime("%m/%d/%y %H:%M:%S %z")}]"
|
33
|
+
else
|
34
|
+
ScoutRailsProxy::Agent.instance.logger.debug "There is no data in the layaway file to deliver."
|
35
|
+
end
|
36
|
+
old_data[slot]=ScoutRailsProxy::Agent.instance.store.merge_data_and_clear(old_data[slot] || Hash.new)
|
37
|
+
ScoutRailsProxy::Agent.instance.logger.debug "Saving the following #{old_data.size} time slots locally:"
|
38
|
+
old_data.each do |k,v|
|
39
|
+
controller_count = 0
|
40
|
+
new_data.each do |meta,stats|
|
41
|
+
if meta.metric_name =~ /\AController/
|
42
|
+
controller_count += stats.call_count
|
43
|
+
end
|
44
|
+
end
|
45
|
+
ScoutRailsProxy::Agent.instance.logger.debug "#{Time.at(k).strftime("%m/%d/%y %H:%M:%S %z")} => #{controller_count} requests"
|
46
|
+
end
|
47
|
+
old_data
|
48
|
+
end
|
49
|
+
to_deliver.any? ? validate_data(to_deliver) : {}
|
50
|
+
end
|
51
|
+
|
52
|
+
# Ensures the data we're sending to the server isn't stale.
|
53
|
+
# This can occur if the agent is collecting data, and app server goes down w/data in the local storage.
|
54
|
+
# When it is restarted later data will remain in local storage but it won't be for the current reporting interval.
|
55
|
+
#
|
56
|
+
# If the data is stale, an empty Hash is returned. Otherwise, the data from the most recent slot is returned.
|
57
|
+
def validate_data(data)
|
58
|
+
data = data.to_a.sort
|
59
|
+
now = Time.now
|
60
|
+
if (most_recent = data.first.first) < now.to_i - 2*60
|
61
|
+
ScoutRailsProxy::Agent.instance.logger.debug "Local Storage is stale (#{Time.at(most_recent).strftime("%m/%d/%y %H:%M:%S %z")}). Not sending data."
|
62
|
+
{}
|
63
|
+
else
|
64
|
+
data.first.last
|
65
|
+
end
|
66
|
+
rescue
|
67
|
+
ScoutRailsProxy::Agent.instance.logger.debug $!.message
|
68
|
+
ScoutRailsProxy::Agent.instance.logger.debug $!.backtrace
|
69
|
+
end
|
70
|
+
|
71
|
+
def slot
|
72
|
+
t = Time.now
|
73
|
+
t -= t.sec
|
74
|
+
t.to_i
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# Logic for the serialized file access
|
2
|
+
class ScoutRailsProxy::LayawayFile
|
3
|
+
def path
|
4
|
+
"#{ScoutRailsProxy::Agent.instance.log_path}/scout_rails_proxy.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
|
+
ScoutRailsProxy::Agent.instance.logger.debug("No data in layaway file.")
|
14
|
+
return nil
|
15
|
+
end
|
16
|
+
Marshal.load(dump)
|
17
|
+
rescue ArgumentError, TypeError => e
|
18
|
+
ScoutRailsProxy::Agent.instance.logger.debug("Error loading data from layaway file: #{e.inspect}")
|
19
|
+
ScoutRailsProxy::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
|
+
ScoutRailsProxy::Agent.instance.logger.error(e.message)
|
39
|
+
ScoutRailsProxy::Agent.instance.logger.debug(e.backtrace.split("\n"))
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_data(f)
|
43
|
+
data = read_until_end(f)
|
44
|
+
result = load(data)
|
45
|
+
f.truncate(0)
|
46
|
+
result
|
47
|
+
end
|
48
|
+
|
49
|
+
def write(f, string)
|
50
|
+
result = 0
|
51
|
+
while (result < string.length)
|
52
|
+
result += f.write_nonblock(string)
|
53
|
+
end
|
54
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
55
|
+
IO.select(nil, [f])
|
56
|
+
retry
|
57
|
+
end
|
58
|
+
|
59
|
+
def read_until_end(f)
|
60
|
+
contents = ""
|
61
|
+
while true
|
62
|
+
contents << f.read_nonblock(10_000)
|
63
|
+
end
|
64
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
65
|
+
IO.select([f])
|
66
|
+
retry
|
67
|
+
rescue EOFError
|
68
|
+
contents
|
69
|
+
end
|
70
|
+
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 ScoutRailsProxy::MetricMeta
|
3
|
+
def initialize(metric_name, options = {})
|
4
|
+
@metric_name = metric_name
|
5
|
+
@metric_id = nil
|
6
|
+
@scope = Thread::current[:scout_sub_scope] || Thread::current[:scout_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 ScoutRailsProxy::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,18 @@
|
|
1
|
+
class ScoutRailsProxy::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
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# The store encapsolutes the logic that (1) saves instrumented data by Metric name to memory and (2) maintains a stack (just an Array)
|
2
|
+
# of instrumented methods that are being called. It's accessed via +ScoutRailsProxy::Agent.instance.store+.
|
3
|
+
class ScoutRailsProxy::Store
|
4
|
+
attr_accessor :metric_hash
|
5
|
+
attr_accessor :transaction_hash
|
6
|
+
attr_accessor :stack
|
7
|
+
attr_accessor :sample
|
8
|
+
attr_reader :transaction_sample_lock
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@metric_hash = Hash.new
|
12
|
+
# Stores aggregate metrics for the current transaction. When the transaction is finished, metrics
|
13
|
+
# are merged with the +metric_hash+.
|
14
|
+
@transaction_hash = Hash.new
|
15
|
+
@stack = Array.new
|
16
|
+
# ensure background thread doesn't manipulate transaction sample while the store is.
|
17
|
+
@transaction_sample_lock = Mutex.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Called when the last stack item completes for the current transaction to clear
|
21
|
+
# for the next run.
|
22
|
+
def reset_transaction!
|
23
|
+
Thread::current[:ignore_transaction] = nil
|
24
|
+
Thread::current[:scout_scope_name] = nil
|
25
|
+
@transaction_hash = Hash.new
|
26
|
+
@stack = Array.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def ignore_transaction!
|
30
|
+
Thread::current[:ignore_transaction] = true
|
31
|
+
end
|
32
|
+
|
33
|
+
# Called at the start of Tracer#instrument:
|
34
|
+
# (1) Either finds an existing MetricStats object in the metric_hash or
|
35
|
+
# initialize a new one. An existing MetricStats object is present if this +metric_name+ has already been instrumented.
|
36
|
+
# (2) Adds a StackItem to the stack. This StackItem is returned and later used to validate the item popped off the stack
|
37
|
+
# when an instrumented code block completes.
|
38
|
+
def record(metric_name)
|
39
|
+
item = ScoutRailsProxy::StackItem.new(metric_name)
|
40
|
+
stack << item
|
41
|
+
item
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop_recording(sanity_check_item, options={})
|
45
|
+
item = stack.pop
|
46
|
+
stack_empty = stack.empty?
|
47
|
+
# if ignoring the transaction, the item is popped but nothing happens.
|
48
|
+
if Thread::current[:ignore_transaction]
|
49
|
+
return
|
50
|
+
end
|
51
|
+
# unbalanced stack check - unreproducable cases have seen this occur. when it does, sets a Thread variable
|
52
|
+
# so we ignore further recordings. +Store#reset_transaction!+ resets this.
|
53
|
+
if item != sanity_check_item
|
54
|
+
ScoutRailsProxy::Agent.instance.logger.warn "Scope [#{Thread::current[:scout_scope_name]}] Popped off stack: #{item.inspect} Expected: #{sanity_check_item.inspect}. Aborting."
|
55
|
+
ignore_transaction!
|
56
|
+
return
|
57
|
+
end
|
58
|
+
duration = Time.now - item.start_time
|
59
|
+
if last=stack.last
|
60
|
+
last.children_time += duration
|
61
|
+
end
|
62
|
+
meta = ScoutRailsProxy::MetricMeta.new(item.metric_name, :desc => options[:desc])
|
63
|
+
meta.scope = nil if stack_empty
|
64
|
+
|
65
|
+
# add backtrace for slow calls ... how is exclusive time handled?
|
66
|
+
if duration > 0.5 and !stack_empty
|
67
|
+
meta.extra = {:backtrace => caller.find_all { |c| c =~ /\/app\//}}
|
68
|
+
end
|
69
|
+
stat = transaction_hash[meta] || ScoutRailsProxy::MetricStats.new(!stack_empty)
|
70
|
+
|
71
|
+
stat.update!(duration,duration-item.children_time)
|
72
|
+
transaction_hash[meta] = stat
|
73
|
+
|
74
|
+
# Uses controllers as the entry point for a transaction. Otherwise, stats are ignored.
|
75
|
+
if stack_empty and meta.metric_name.match(/\AController\//)
|
76
|
+
aggs=aggregate_calls(transaction_hash.dup,meta)
|
77
|
+
store_sample(options[:uri],transaction_hash.dup.merge(aggs),meta,stat)
|
78
|
+
# deep duplicate
|
79
|
+
duplicate = aggs.dup
|
80
|
+
duplicate.each_pair do |k,v|
|
81
|
+
duplicate[k.dup] = v.dup
|
82
|
+
end
|
83
|
+
merge_data(duplicate.merge({meta.dup => stat.dup})) # aggregrates + controller
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns the top-level category names used in the +metrics+ hash.
|
88
|
+
def categories(metrics)
|
89
|
+
cats = Set.new
|
90
|
+
metrics.keys.each do |meta|
|
91
|
+
next if meta.scope.nil? # ignore controller
|
92
|
+
if match=meta.metric_name.match(/\A([\w|\d]+)\//)
|
93
|
+
cats << match[1]
|
94
|
+
end
|
95
|
+
end # metrics.each
|
96
|
+
cats
|
97
|
+
end
|
98
|
+
|
99
|
+
# Takes a metric_hash of calls and generates aggregates for ActiveRecord and View calls.
|
100
|
+
def aggregate_calls(metrics,parent_meta)
|
101
|
+
categories = categories(metrics)
|
102
|
+
aggregates = {}
|
103
|
+
categories.each do |cat|
|
104
|
+
agg_meta=ScoutRailsProxy::MetricMeta.new("#{cat}/all")
|
105
|
+
agg_meta.scope = parent_meta.metric_name
|
106
|
+
agg_stats = ScoutRailsProxy::MetricStats.new
|
107
|
+
metrics.each do |meta,stats|
|
108
|
+
if meta.metric_name =~ /\A#{cat}\//
|
109
|
+
agg_stats.combine!(stats)
|
110
|
+
end
|
111
|
+
end # metrics.each
|
112
|
+
aggregates[agg_meta] = agg_stats unless agg_stats.call_count.zero?
|
113
|
+
end # categories.each
|
114
|
+
aggregates
|
115
|
+
end
|
116
|
+
|
117
|
+
# Stores the slowest transaction. This will be sent to the server.
|
118
|
+
def store_sample(uri,transaction_hash,parent_meta,parent_stat,options = {})
|
119
|
+
@transaction_sample_lock.synchronize do
|
120
|
+
if parent_stat.total_call_time >= 2 and (@sample.nil? or (@sample and parent_stat.total_call_time > @sample.total_call_time))
|
121
|
+
@sample = ScoutRailsProxy::TransactionSample.new(uri,parent_meta.metric_name,parent_stat.total_call_time,transaction_hash.dup)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Finds or creates the metric w/the given name in the metric_hash, and updates the time. Primarily used to
|
127
|
+
# record sampled metrics. For instrumented methods, #record and #stop_recording are used.
|
128
|
+
#
|
129
|
+
# Options:
|
130
|
+
# :scope => If provided, overrides the default scope.
|
131
|
+
# :exclusive_time => Sets the exclusive time for the method. If not provided, uses +call_time+.
|
132
|
+
def track!(metric_name,call_time,options = {})
|
133
|
+
meta = ScoutRailsProxy::MetricMeta.new(metric_name)
|
134
|
+
meta.scope = options[:scope] if options.has_key?(:scope)
|
135
|
+
stat = metric_hash[meta] || ScoutRailsProxy::MetricStats.new
|
136
|
+
stat.update!(call_time,options[:exclusive_time] || call_time)
|
137
|
+
metric_hash[meta] = stat
|
138
|
+
end
|
139
|
+
|
140
|
+
# Combines old and current data
|
141
|
+
def merge_data(old_data)
|
142
|
+
old_data.each do |old_meta,old_stats|
|
143
|
+
if stats = metric_hash[old_meta]
|
144
|
+
metric_hash[old_meta] = stats.combine!(old_stats)
|
145
|
+
else
|
146
|
+
metric_hash[old_meta] = old_stats
|
147
|
+
end
|
148
|
+
end
|
149
|
+
metric_hash
|
150
|
+
end
|
151
|
+
|
152
|
+
# Merges old and current data, clears the current in-memory metric hash, and returns
|
153
|
+
# the merged data
|
154
|
+
def merge_data_and_clear(old_data)
|
155
|
+
merged = merge_data(old_data)
|
156
|
+
self.metric_hash = {}
|
157
|
+
merged
|
158
|
+
end
|
159
|
+
end # class Store
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# Contains the methods that instrument blocks of code.
|
2
|
+
#
|
3
|
+
# When a code block is wrapped inside #instrument(metric_name):
|
4
|
+
# * The #instrument method pushes a StackItem onto Store#stack
|
5
|
+
# * When a code block is finished, #instrument pops the last item off the stack and verifies it's the StackItem
|
6
|
+
# we created earlier.
|
7
|
+
# * Once verified, the metrics for the recording session are merged into the in-memory Store#metric_hash. The current scope
|
8
|
+
# is also set for the metric (if Thread::current[:scout_scope_name] isn't nil).
|
9
|
+
module ScoutRailsProxy::Tracer
|
10
|
+
def self.included(klass)
|
11
|
+
klass.extend ClassMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
# Use to trace a method call, possibly reporting slow transaction traces to Scout.
|
17
|
+
def trace(metric_name, options = {}, &block)
|
18
|
+
ScoutRailsProxy::Agent.instance.store.reset_transaction!
|
19
|
+
instrument(metric_name, options) do
|
20
|
+
Thread::current[:scout_scope_name] = metric_name
|
21
|
+
yield
|
22
|
+
Thread::current[:scout_scope_name] = nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Options:
|
27
|
+
# - :scope => If specified, sets the sub-scope for the metric. We allow additional scope level. This is used
|
28
|
+
# when rendering the transaction tree in the UI.
|
29
|
+
def instrument(metric_name, options={}, &block)
|
30
|
+
# don't instrument if (1) NOT inside a transaction and (2) NOT a Controller metric.
|
31
|
+
if !Thread::current[:scout_scope_name] and metric_name !~ /\AController\//
|
32
|
+
ScoutRailsProxy::Agent.instance.logger.debug "Not instrumenting [#{metric_name}] - no scope."
|
33
|
+
return yield
|
34
|
+
end
|
35
|
+
if options.delete(:scope)
|
36
|
+
Thread::current[:scout_sub_scope] = metric_name
|
37
|
+
end
|
38
|
+
stack_item = ScoutRailsProxy::Agent.instance.store.record(metric_name)
|
39
|
+
begin
|
40
|
+
yield
|
41
|
+
ensure
|
42
|
+
Thread::current[:scout_sub_scope] = nil if Thread::current[:scout_sub_scope] == metric_name
|
43
|
+
ScoutRailsProxy::Agent.instance.store.stop_recording(stack_item,options)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def instrument_method(method,options = {})
|
48
|
+
ScoutRailsProxy::Agent.instance.logger.info "Instrumenting #{method}"
|
49
|
+
metric_name = options[:metric_name] || default_metric_name(method)
|
50
|
+
return if !instrumentable?(method) or instrumented?(method,metric_name)
|
51
|
+
class_eval instrumented_method_string(method, {:metric_name => metric_name, :scope => options[:scope]}), __FILE__, __LINE__
|
52
|
+
|
53
|
+
alias_method _uninstrumented_method_name(method, metric_name), method
|
54
|
+
alias_method method, _instrumented_method_name(method, metric_name)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def instrumented_method_string(method, options)
|
60
|
+
klass = (self === Module) ? "self" : "self.class"
|
61
|
+
"def #{_instrumented_method_name(method, options[:metric_name])}(*args, &block)
|
62
|
+
result = #{klass}.instrument(\"#{options[:metric_name]}\",{:scope => #{options[:scope] || false}}) do
|
63
|
+
#{_uninstrumented_method_name(method, options[:metric_name])}(*args, &block)
|
64
|
+
end
|
65
|
+
result
|
66
|
+
end"
|
67
|
+
end
|
68
|
+
|
69
|
+
# The method must exist to be instrumented.
|
70
|
+
def instrumentable?(method)
|
71
|
+
exists = method_defined?(method) || private_method_defined?(method)
|
72
|
+
ScoutRailsProxy::Agent.instance.logger.warn "The method [#{self.name}##{method}] does not exist and will not be instrumented" unless exists
|
73
|
+
exists
|
74
|
+
end
|
75
|
+
|
76
|
+
# +True+ if the method is already instrumented.
|
77
|
+
def instrumented?(method,metric_name)
|
78
|
+
instrumented = method_defined?(_instrumented_method_name(method, metric_name))
|
79
|
+
ScoutRailsProxy::Agent.instance.logger.warn "The method [#{self.name}##{method}] has already been instrumented" if instrumented
|
80
|
+
instrumented
|
81
|
+
end
|
82
|
+
|
83
|
+
def default_metric_name(method)
|
84
|
+
"Custom/#{self.name}/#{method.to_s}"
|
85
|
+
end
|
86
|
+
|
87
|
+
# given a method and a metric, this method returns the
|
88
|
+
# untraced alias of the method name
|
89
|
+
def _uninstrumented_method_name(method, metric_name)
|
90
|
+
"#{_sanitize_name(method)}_without_scout_instrument_#{_sanitize_name(metric_name)}"
|
91
|
+
end
|
92
|
+
|
93
|
+
# given a method and a metric, this method returns the traced
|
94
|
+
# alias of the method name
|
95
|
+
def _instrumented_method_name(method, metric_name)
|
96
|
+
name = "#{_sanitize_name(method)}_with_scout_instrument_#{_sanitize_name(metric_name)}"
|
97
|
+
end
|
98
|
+
|
99
|
+
# Method names like +any?+ or +replace!+ contain a trailing character that would break when
|
100
|
+
# eval'd as ? and ! aren't allowed inside method names.
|
101
|
+
def _sanitize_name(name)
|
102
|
+
name.to_s.tr_s('^a-zA-Z0-9', '_')
|
103
|
+
end
|
104
|
+
end # ClassMethods
|
105
|
+
end # module Tracer
|