scout_rails 0.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.
- data/.DS_Store +0 -0
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.markdown +29 -0
- data/Rakefile +1 -0
- data/lib/scout_rails.rb +31 -0
- data/lib/scout_rails/agent.rb +316 -0
- data/lib/scout_rails/config.rb +34 -0
- data/lib/scout_rails/environment.rb +86 -0
- data/lib/scout_rails/instruments/active_record_instruments.rb +88 -0
- data/lib/scout_rails/instruments/process/process_cpu.rb +27 -0
- data/lib/scout_rails/instruments/process/process_memory.rb +40 -0
- data/lib/scout_rails/instruments/rails/action_controller_instruments.rb +47 -0
- data/lib/scout_rails/instruments/rails3/action_controller_instruments.rb +35 -0
- data/lib/scout_rails/instruments/sinatra_instruments.rb +34 -0
- data/lib/scout_rails/layaway.rb +77 -0
- data/lib/scout_rails/layaway_file.rb +70 -0
- data/lib/scout_rails/metric_meta.rb +36 -0
- data/lib/scout_rails/metric_stats.rb +45 -0
- data/lib/scout_rails/stack_item.rb +18 -0
- data/lib/scout_rails/store.rb +78 -0
- data/lib/scout_rails/tracer.rb +90 -0
- data/lib/scout_rails/version.rb +3 -0
- data/scout_rails.gemspec +24 -0
- metadata +71 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Stats that are associated with each instrumented method.
|
|
2
|
+
class ScoutRails::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
|
|
11
|
+
self.call_count = 0
|
|
12
|
+
self.total_call_time = 0.0
|
|
13
|
+
self.total_exclusive_time = 0.0
|
|
14
|
+
self.min_call_time = 0.0
|
|
15
|
+
self.max_call_time = 0.0
|
|
16
|
+
self.sum_of_squares = 0.0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def update!(call_time,exclusive_time)
|
|
20
|
+
self.min_call_time = call_time if self.call_count == 0 or call_time < min_call_time
|
|
21
|
+
self.max_call_time = call_time if self.call_count == 0 or call_time > max_call_time
|
|
22
|
+
self.call_count +=1
|
|
23
|
+
self.total_call_time += call_time
|
|
24
|
+
self.total_exclusive_time += exclusive_time
|
|
25
|
+
self.sum_of_squares += (call_time * call_time)
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# combines data from another MetricStats object
|
|
30
|
+
def combine!(other)
|
|
31
|
+
self.call_count += other.call_count
|
|
32
|
+
self.total_call_time += other.total_call_time
|
|
33
|
+
self.total_exclusive_time += other.total_exclusive_time
|
|
34
|
+
self.min_call_time = other.min_call_time if other.min_call_time < self.min_call_time
|
|
35
|
+
self.max_call_time = other.max_call_time if other.max_call_time > self.max_call_time
|
|
36
|
+
self.sum_of_squares += other.sum_of_squares
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# To avoid conflicts with different JSON libaries handle JSON ourselves.
|
|
41
|
+
# Time-based metrics are converted to milliseconds from seconds.
|
|
42
|
+
def to_json(*a)
|
|
43
|
+
%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}}]
|
|
44
|
+
end
|
|
45
|
+
end # class MetricStats
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class ScoutRails::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,78 @@
|
|
|
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 +ScoutRails::Agent.instance.store+.
|
|
3
|
+
class ScoutRails::Store
|
|
4
|
+
attr_accessor :metric_hash
|
|
5
|
+
attr_accessor :stack
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@metric_hash = Hash.new
|
|
9
|
+
@stack = Array.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Called at the start of Tracer#instrument:
|
|
13
|
+
# (1) Either finds an existing MetricStats object in the metric_hash or
|
|
14
|
+
# initialize a new one. An existing MetricStats object is present if this +metric_name+ has already been instrumented.
|
|
15
|
+
# (2) Adds a StackItem to the stack. This StackItem is returned and later used to validate the item popped off the stack
|
|
16
|
+
# when an instrumented code block completes.
|
|
17
|
+
def record(metric_name)
|
|
18
|
+
#ScoutRails::Agent.instance.logger.debug "recording #{metric_name}"
|
|
19
|
+
item = ScoutRails::StackItem.new(metric_name)
|
|
20
|
+
stack << item
|
|
21
|
+
item
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stop_recording(sanity_check_item)
|
|
25
|
+
item = stack.pop
|
|
26
|
+
raise "items not equal: #{item.inspect} / #{sanity_check_item.inspect}" if item != sanity_check_item
|
|
27
|
+
duration = Time.now - item.start_time
|
|
28
|
+
if last=stack.last
|
|
29
|
+
#ScoutRails::Agent.instance.logger.debug "found an element on stack [#{last.inspect}]. adding duration #{duration} to children time [#{last.children_time}]"
|
|
30
|
+
last.children_time += duration
|
|
31
|
+
end
|
|
32
|
+
#ScoutRails::Agent.instance.logger.debug "popped #{item.inspect} off stack. duration: #{duration}s"
|
|
33
|
+
if stack.empty? # this is the last item on the stack. it shouldn't have a scope.
|
|
34
|
+
Thread::current[:scout_scope_name] = nil
|
|
35
|
+
end
|
|
36
|
+
meta = ScoutRails::MetricMeta.new(item.metric_name)
|
|
37
|
+
#ScoutRails::Agent.instance.logger.debug "meta: #{meta.inspect}"
|
|
38
|
+
stat = metric_hash[meta] || ScoutRails::MetricStats.new
|
|
39
|
+
#ScoutRails::Agent.instance.logger.debug "found existing stat w/ky: #{meta}" if !stat.call_count.zero?
|
|
40
|
+
stat.update!(duration,duration-item.children_time)
|
|
41
|
+
metric_hash[meta] = stat
|
|
42
|
+
#ScoutRails::Agent.instance.logger.debug "metric hash has #{metric_hash.size} items"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Finds or creates the metric w/the given name in the metric_hash, and updates the time. Primarily used to
|
|
46
|
+
# record sampled metrics. For instrumented methods, #record and #stop_recording are used.
|
|
47
|
+
#
|
|
48
|
+
# Options:
|
|
49
|
+
# :scope => If provided, overrides the default scope.
|
|
50
|
+
# :exclusive_time => Sets the exclusive time for the method. If not provided, uses +call_time+.
|
|
51
|
+
def track!(metric_name,call_time,options = {})
|
|
52
|
+
meta = ScoutRails::MetricMeta.new(metric_name)
|
|
53
|
+
meta.scope = options[:scope] if options.has_key?(:scope)
|
|
54
|
+
stat = metric_hash[meta] || ScoutRails::MetricStats.new
|
|
55
|
+
stat.update!(call_time,options[:exclusive_time] || call_time)
|
|
56
|
+
metric_hash[meta] = stat
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Combines old and current data
|
|
60
|
+
def merge_data(old_data)
|
|
61
|
+
old_data.each do |old_meta,old_stats|
|
|
62
|
+
if stats = metric_hash[old_meta]
|
|
63
|
+
metric_hash[old_meta] = stats.combine!(old_stats)
|
|
64
|
+
else
|
|
65
|
+
metric_hash[old_meta] = old_stats
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
metric_hash
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Merges old and current data, clears the current in-memory metric hash, and returns
|
|
72
|
+
# the merged data
|
|
73
|
+
def merge_data_and_clear(old_data)
|
|
74
|
+
merged = merge_data(old_data)
|
|
75
|
+
self.metric_hash = {}
|
|
76
|
+
merged
|
|
77
|
+
end
|
|
78
|
+
end # class Store
|
|
@@ -0,0 +1,90 @@
|
|
|
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 ScoutRails::Tracer
|
|
10
|
+
def self.included(klass)
|
|
11
|
+
klass.extend ClassMethods
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module ClassMethods
|
|
15
|
+
# An easier reference to the agent's associated store.
|
|
16
|
+
def store
|
|
17
|
+
ScoutRails::Agent.instance.store
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def instrument(metric_name, &block)
|
|
21
|
+
#ScoutRails::Agent.instance.logger.debug "Instrumenting: #{metric_name} w/scope: #{Thread::current[:scout_scope_name]}"
|
|
22
|
+
stack_item = store.record(metric_name)
|
|
23
|
+
#ScoutRails::Agent.instance.logger.debug "Stack contains: #{store.stack.size} methods"
|
|
24
|
+
begin
|
|
25
|
+
yield
|
|
26
|
+
ensure
|
|
27
|
+
store.stop_recording(stack_item)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def instrument_method(method,metric_name = nil)
|
|
32
|
+
#ScoutRails::Agent.instance.logger.debug "Instrumenting method name: #{name} w/metric name: #{metric_name}"
|
|
33
|
+
metric_name = metric_name || default_metric_name(method)
|
|
34
|
+
return if !instrumentable?(method) or instrumented?(method,metric_name)
|
|
35
|
+
class_eval instrumented_method_string(method, metric_name), __FILE__, __LINE__
|
|
36
|
+
|
|
37
|
+
alias_method _uninstrumented_method_name(method, metric_name), method
|
|
38
|
+
alias_method method, _instrumented_method_name(method, metric_name)
|
|
39
|
+
#ScoutRails::Agent.instance.logger.debug "Instrumented #{self.name}##{method} w/metric name: #{metric_name}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def instrumented_method_string(method, metric_name)
|
|
45
|
+
klass = (self === Module) ? "self" : "self.class"
|
|
46
|
+
"def #{_instrumented_method_name(method, metric_name)}(*args, &block)
|
|
47
|
+
result = #{klass}.instrument(\"#{metric_name}\") do
|
|
48
|
+
#{_uninstrumented_method_name(method, metric_name)}(*args, &block)
|
|
49
|
+
end
|
|
50
|
+
result
|
|
51
|
+
end"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The method must exist to be instrumented.
|
|
55
|
+
def instrumentable?(method)
|
|
56
|
+
exists = method_defined?(method) || private_method_defined?(method)
|
|
57
|
+
ScoutRails::Agent.instance.logger.warn "The method [#{self.name}##{method}] does not exist and will not be instrumented" unless exists
|
|
58
|
+
exists
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# +True+ if the method is already instrumented.
|
|
62
|
+
def instrumented?(method,metric_name)
|
|
63
|
+
instrumented = method_defined?(_instrumented_method_name(method, metric_name))
|
|
64
|
+
ScoutRails::Agent.instance.logger.warn "The method [#{self.name}##{method}] has already been instrumented" if instrumented
|
|
65
|
+
instrumented
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def default_metric_name(method)
|
|
69
|
+
"Custom/#{self.name}/#{method.to_s}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# given a method and a metric, this method returns the
|
|
73
|
+
# untraced alias of the method name
|
|
74
|
+
def _uninstrumented_method_name(method, metric_name)
|
|
75
|
+
"#{_sanitize_name(method)}_without_scout_instrument_#{_sanitize_name(metric_name)}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# given a method and a metric, this method returns the traced
|
|
79
|
+
# alias of the method name
|
|
80
|
+
def _instrumented_method_name(method, metric_name)
|
|
81
|
+
name = "#{_sanitize_name(method)}_with_scout_instrument_#{_sanitize_name(metric_name)}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Method names like +any?+ or +replace!+ contain a trailing character that would break when
|
|
85
|
+
# eval'd as ? and ! aren't allowed inside method names.
|
|
86
|
+
def _sanitize_name(name)
|
|
87
|
+
name.to_s.tr_s('^a-zA-Z0-9', '_')
|
|
88
|
+
end
|
|
89
|
+
end # ClassMethods
|
|
90
|
+
end # module Tracer
|
data/scout_rails.gemspec
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "scout_rails/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "scout_rails"
|
|
7
|
+
s.version = ScoutRails::VERSION
|
|
8
|
+
s.authors = ["Derek Haynes",'Andre Lewis']
|
|
9
|
+
s.email = ["support@scoutapp.com"]
|
|
10
|
+
s.homepage = "http://scoutapp.com"
|
|
11
|
+
s.summary = "Rails application performance monitoring"
|
|
12
|
+
s.description = "Monitors a Ruby on Rails application and reports detailed metrics on performance to Scout, a hosted monitoring service."
|
|
13
|
+
|
|
14
|
+
s.rubyforge_project = "scout_rails"
|
|
15
|
+
|
|
16
|
+
s.files = `git ls-files`.split("\n")
|
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
19
|
+
s.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
# specify any dependencies here; for example:
|
|
22
|
+
# s.add_development_dependency "rspec"
|
|
23
|
+
# s.add_runtime_dependency "rest-client"
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: scout_rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Derek Haynes
|
|
9
|
+
- Andre Lewis
|
|
10
|
+
autorequire:
|
|
11
|
+
bindir: bin
|
|
12
|
+
cert_chain: []
|
|
13
|
+
date: 2012-04-27 00:00:00.000000000 Z
|
|
14
|
+
dependencies: []
|
|
15
|
+
description: Monitors a Ruby on Rails application and reports detailed metrics on
|
|
16
|
+
performance to Scout, a hosted monitoring service.
|
|
17
|
+
email:
|
|
18
|
+
- support@scoutapp.com
|
|
19
|
+
executables: []
|
|
20
|
+
extensions: []
|
|
21
|
+
extra_rdoc_files: []
|
|
22
|
+
files:
|
|
23
|
+
- .DS_Store
|
|
24
|
+
- .gitignore
|
|
25
|
+
- Gemfile
|
|
26
|
+
- README.markdown
|
|
27
|
+
- Rakefile
|
|
28
|
+
- lib/scout_rails.rb
|
|
29
|
+
- lib/scout_rails/agent.rb
|
|
30
|
+
- lib/scout_rails/config.rb
|
|
31
|
+
- lib/scout_rails/environment.rb
|
|
32
|
+
- lib/scout_rails/instruments/active_record_instruments.rb
|
|
33
|
+
- lib/scout_rails/instruments/process/process_cpu.rb
|
|
34
|
+
- lib/scout_rails/instruments/process/process_memory.rb
|
|
35
|
+
- lib/scout_rails/instruments/rails/action_controller_instruments.rb
|
|
36
|
+
- lib/scout_rails/instruments/rails3/action_controller_instruments.rb
|
|
37
|
+
- lib/scout_rails/instruments/sinatra_instruments.rb
|
|
38
|
+
- lib/scout_rails/layaway.rb
|
|
39
|
+
- lib/scout_rails/layaway_file.rb
|
|
40
|
+
- lib/scout_rails/metric_meta.rb
|
|
41
|
+
- lib/scout_rails/metric_stats.rb
|
|
42
|
+
- lib/scout_rails/stack_item.rb
|
|
43
|
+
- lib/scout_rails/store.rb
|
|
44
|
+
- lib/scout_rails/tracer.rb
|
|
45
|
+
- lib/scout_rails/version.rb
|
|
46
|
+
- scout_rails.gemspec
|
|
47
|
+
homepage: http://scoutapp.com
|
|
48
|
+
licenses: []
|
|
49
|
+
post_install_message:
|
|
50
|
+
rdoc_options: []
|
|
51
|
+
require_paths:
|
|
52
|
+
- lib
|
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
54
|
+
none: false
|
|
55
|
+
requirements:
|
|
56
|
+
- - ! '>='
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
|
+
none: false
|
|
61
|
+
requirements:
|
|
62
|
+
- - ! '>='
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '0'
|
|
65
|
+
requirements: []
|
|
66
|
+
rubyforge_project: scout_rails
|
|
67
|
+
rubygems_version: 1.8.10
|
|
68
|
+
signing_key:
|
|
69
|
+
specification_version: 3
|
|
70
|
+
summary: Rails application performance monitoring
|
|
71
|
+
test_files: []
|