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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module ScoutRails
2
+ VERSION = "0.0.1"
3
+ end
@@ -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: []