scout_rails 0.0.3.pre → 0.0.4.pre

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/CHANGELOG.markdown CHANGED
@@ -1,7 +1,13 @@
1
+ # 0.0.4.pre
2
+
3
+ * Transaction Sampling
4
+
1
5
  # 0.0.3.pre
2
6
 
3
7
  * Removed dynamic ActiveRecord caller instrumentation
4
8
  * Fixed issue that prevents the app from loading if ActiveRecord isn't used.
9
+ * Using a metric hash for each request, then merging when complete. Ensures data associated w/requests that overlap a
10
+ minute boundary are correctly associated.
5
11
 
6
12
  # 0.0.2
7
13
 
@@ -117,7 +117,7 @@ module ScoutRails
117
117
  end
118
118
 
119
119
  def log_version_pid
120
- logger.debug "Scout Agent [#{ScoutRails::VERSION}] Initialized"
120
+ logger.info "Scout Agent [#{ScoutRails::VERSION}] Initialized"
121
121
  end
122
122
 
123
123
  def log_path
@@ -216,19 +216,22 @@ module ScoutRails
216
216
  controller_count += stats.call_count
217
217
  end
218
218
  end
219
- logger.debug "#{config.settings['name']} Delivering metrics for #{controller_count} requests."
219
+ logger.debug "#{config.settings['name']} Delivering metrics for #{controller_count} requests."
220
220
  response = post( checkin_uri,
221
- Marshal.dump(metrics),
221
+ Marshal.dump(:metrics => metrics, :sample => store.sample),
222
222
  "Content-Type" => "application/json" )
223
223
  if response and response.is_a?(Net::HTTPSuccess)
224
224
  directives = Marshal.load(response.body)
225
225
  self.metric_lookup.merge!(directives[:metric_lookup])
226
+ store.transaction_sample_lock.synchronize do
227
+ store.sample = nil
228
+ end
226
229
  logger.debug "Metric Cache Size: #{metric_lookup.size}"
227
230
  end
228
231
  end
229
232
  rescue
230
- logger.debug "Error on checkin to #{checkin_uri.to_s}"
231
- logger.debug $!.message
233
+ logger.info "Error on checkin to #{checkin_uri.to_s}"
234
+ logger.info $!.message
232
235
  logger.debug $!.backtrace
233
236
  end
234
237
 
@@ -15,7 +15,7 @@ module ScoutRails::Instruments
15
15
 
16
16
  def log_with_scout_instruments(*args, &block)
17
17
  sql, name = args
18
- self.class.instrument(scout_ar_metric_name(sql,name)) do
18
+ self.class.instrument(scout_ar_metric_name(sql,name), :desc => scout_sanitize_sql(sql)) do
19
19
  log_without_scout_instruments(sql, name, &block)
20
20
  end
21
21
  end
@@ -35,13 +35,27 @@ module ScoutRails::Instruments
35
35
  end
36
36
  end
37
37
  metric = "ActiveRecord/#{model}/#{metric_name}" if metric_name
38
- metric = "Database/SQL/other" if metric.nil?
38
+ metric = "ActiveRecord/SQL/other" if metric.nil?
39
39
  else
40
- metric = "Database/SQL/Unknown"
40
+ metric = "ActiveRecord/SQL/Unknown"
41
41
  end
42
42
  metric
43
43
  end
44
44
 
45
+ # Removes actual values from SQL. Used to both obfuscate the SQL and group
46
+ # similar queries in the UI.
47
+ def scout_sanitize_sql(sql)
48
+ return nil if sql.length > 1000 # safeguard - don't sanitize large SQL statements
49
+ sql = sql.dup
50
+ sql.gsub!(/\\"/, '') # removing escaping double quotes
51
+ sql.gsub!(/\\'/, '') # removing escaping single quotes
52
+ sql.gsub!(/'(?:[^']|'')*'/, '?') # removing strings (single quote)
53
+ sql.gsub!(/"(?:[^"]|"")*"/, '?') # removing strings (double quote)
54
+ sql.gsub!(/\b\d+\b/, '?') # removing integers
55
+ sql.gsub!(/\?(,\?)+/,'?') # replace multiple ? w/a single ?
56
+ sql
57
+ end
58
+
45
59
  end # module ActiveRecordInstruments
46
60
  end # module Instruments
47
61
 
@@ -55,6 +69,8 @@ def add_instruments
55
69
  include ::ScoutRails::Tracer
56
70
  end
57
71
  end
72
+ rescue
73
+ ScoutRails::Agent.instance.logger.warn "ActiveRecord instrumentation exception: #{$!.message}"
58
74
  end
59
75
 
60
76
  if defined?(::Rails) && ::Rails::VERSION::MAJOR.to_i == 3
@@ -16,10 +16,8 @@ module ScoutRails::Instruments
16
16
  # specific controller actions.
17
17
  def perform_action_with_scout_instruments(*args, &block)
18
18
  scout_controller_action = "Controller/#{controller_path}/#{action_name}"
19
- self.class.instrument(scout_controller_action) do
20
- Thread::current[:scout_scope_name] = scout_controller_action
19
+ self.class.trace(scout_controller_action, :uri => request.request_uri) do
21
20
  perform_action_without_scout_instruments(*args, &block)
22
- Thread::current[:scout_scope_name] = nil
23
21
  end
24
22
  end
25
23
  end
@@ -42,6 +40,6 @@ if defined?(ActionController) && defined?(ActionController::Base)
42
40
  ScoutRails::Agent.instance.logger.debug "Instrumenting ActionView::Template"
43
41
  ActionView::Template.class_eval do
44
42
  include ::ScoutRails::Tracer
45
- instrument_method :render, 'View/#{path[%r{^(/.*/)?(.*)$},2]}/Rendering'
43
+ instrument_method :render, :metric_name => 'View/#{path[%r{^(/.*/)?(.*)$},2]}/Rendering', :scope => true
46
44
  end
47
45
  end
@@ -1,10 +1,10 @@
1
+ # Rails 3
1
2
  module ScoutRails::Instruments
2
3
  module ActionControllerInstruments
3
4
  # Instruments the action and tracks errors.
4
5
  def process_action(*args)
5
6
  scout_controller_action = "Controller/#{controller_path}/#{action_name}"
6
- self.class.instrument(scout_controller_action) do
7
- Thread::current[:scout_scope_name] = scout_controller_action
7
+ self.class.trace(scout_controller_action, :uri => request.fullpath) do
8
8
  begin
9
9
  super
10
10
  rescue Exception => e
@@ -30,6 +30,6 @@ if defined?(ActionView) && defined?(ActionView::PartialRenderer)
30
30
  ScoutRails::Agent.instance.logger.debug "Instrumenting ActionView::PartialRenderer"
31
31
  ActionView::PartialRenderer.class_eval do
32
32
  include ScoutRails::Tracer
33
- instrument_method :render_partial, 'View/#{@template.virtual_path}/Rendering'
33
+ instrument_method :render_partial, :metric_name => 'View/#{@template.virtual_path}/Rendering', :scope => true
34
34
  end
35
35
  end
@@ -15,8 +15,7 @@ module ScoutRails::Instruments
15
15
  name = 'root' if name.empty?
16
16
  name = @request.request_method + ' ' + name if @request && @request.respond_to?(:request_method)
17
17
  scout_controller_action = "Controller/Sinatra/#{name}"
18
- self.class.instrument(scout_controller_action) do
19
- Thread::current[:scout_scope_name] = scout_controller_action
18
+ self.class.trace(scout_controller_action, :uri => @request.path_info) do
20
19
  route_eval_without_scout_instruments(&blockarg)
21
20
  end
22
21
  end # route_eval_with_scout_instrumentss
@@ -61,7 +61,6 @@ class ScoutRails::Layaway
61
61
  ScoutRails::Agent.instance.logger.debug "Local Storage is stale (#{Time.at(most_recent).strftime("%m/%d/%y %H:%M:%S %z")}). Not sending data."
62
62
  {}
63
63
  else
64
- #ScoutRails::Agent.instance.logger.debug "Validated time slot: #{Time.at(most_recent).strftime("%m/%d/%y %H:%M:%S %z")}"
65
64
  data.first.last
66
65
  end
67
66
  rescue
@@ -1,13 +1,16 @@
1
1
  # Contains the meta information associated with a metric. Used to lookup Metrics in to Store's metric_hash.
2
2
  class ScoutRails::MetricMeta
3
- def initialize(metric_name)
3
+ def initialize(metric_name, options = {})
4
4
  @metric_name = metric_name
5
5
  @metric_id = nil
6
- @scope = Thread::current[:scout_scope_name]
6
+ @scope = Thread::current[:scout_sub_scope] || Thread::current[:scout_scope_name]
7
+ @desc = options[:desc]
8
+ @extra = {}
7
9
  end
8
10
  attr_accessor :metric_id, :metric_name
9
11
  attr_accessor :scope
10
12
  attr_accessor :client_id
13
+ attr_accessor :desc, :extra
11
14
 
12
15
  # To avoid conflicts with different JSON libaries
13
16
  def to_json(*a)
@@ -21,16 +24,19 @@ class ScoutRails::MetricMeta
21
24
  def hash
22
25
  h = metric_name.hash
23
26
  h ^= scope.hash unless scope.nil?
27
+ h ^= desc.hash unless desc.nil?
24
28
  h
25
29
  end
26
30
 
27
31
  def <=>(o)
28
32
  namecmp = self.name <=> o.name
29
33
  return namecmp if namecmp != 0
30
- return (self.scope || '') <=> (o.scope || '')
34
+ scopecmp = (self.scope || '') <=> (o.scope || '')
35
+ return scopecmp if scopecmp != 0
36
+ (self.desc || '') <=> (o.desc || '')
31
37
  end
32
38
 
33
39
  def eql?(o)
34
- self.class == o.class && metric_name.eql?(o.metric_name) && scope == o.scope && client_id == o.client_id
40
+ self.class == o.class && metric_name.eql?(o.metric_name) && scope == o.scope && client_id == o.client_id && desc == o.desc
35
41
  end
36
42
  end # class MetricMeta
@@ -7,7 +7,8 @@ class ScoutRails::MetricStats
7
7
  attr_accessor :total_exclusive_time
8
8
  attr_accessor :sum_of_squares
9
9
 
10
- def initialize
10
+ def initialize(scoped = false)
11
+ @scoped = scoped
11
12
  self.call_count = 0
12
13
  self.total_call_time = 0.0
13
14
  self.total_exclusive_time = 0.0
@@ -17,12 +18,15 @@ class ScoutRails::MetricStats
17
18
  end
18
19
 
19
20
  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
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
22
26
  self.call_count +=1
23
27
  self.total_call_time += call_time
24
28
  self.total_exclusive_time += exclusive_time
25
- self.sum_of_squares += (call_time * call_time)
29
+ self.sum_of_squares += (t * t)
26
30
  self
27
31
  end
28
32
 
@@ -31,7 +35,7 @@ class ScoutRails::MetricStats
31
35
  self.call_count += other.call_count
32
36
  self.total_call_time += other.total_call_time
33
37
  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
38
+ self.min_call_time = other.min_call_time if self.min_call_time.zero? or other.min_call_time < self.min_call_time
35
39
  self.max_call_time = other.max_call_time if other.max_call_time > self.max_call_time
36
40
  self.sum_of_squares += other.sum_of_squares
37
41
  self
@@ -2,11 +2,26 @@
2
2
  # of instrumented methods that are being called. It's accessed via +ScoutRails::Agent.instance.store+.
3
3
  class ScoutRails::Store
4
4
  attr_accessor :metric_hash
5
+ attr_accessor :transaction_hash
5
6
  attr_accessor :stack
7
+ attr_accessor :sample
8
+ attr_reader :transaction_sample_lock
6
9
 
7
10
  def initialize
8
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
9
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[:scout_scope_name] = nil
24
+ @transaction_hash = Hash.new
10
25
  end
11
26
 
12
27
  # Called at the start of Tracer#instrument:
@@ -15,31 +30,68 @@ class ScoutRails::Store
15
30
  # (2) Adds a StackItem to the stack. This StackItem is returned and later used to validate the item popped off the stack
16
31
  # when an instrumented code block completes.
17
32
  def record(metric_name)
18
- #ScoutRails::Agent.instance.logger.debug "recording #{metric_name}"
19
33
  item = ScoutRails::StackItem.new(metric_name)
20
34
  stack << item
21
35
  item
22
36
  end
23
37
 
24
- def stop_recording(sanity_check_item)
38
+ def stop_recording(sanity_check_item, options={})
25
39
  item = stack.pop
40
+ stack_empty = stack.empty?
26
41
  raise "items not equal: #{item.inspect} / #{sanity_check_item.inspect}" if item != sanity_check_item
27
42
  duration = Time.now - item.start_time
28
43
  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
44
  last.children_time += duration
31
45
  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
46
+ meta = ScoutRails::MetricMeta.new(item.metric_name, :desc => options[:desc])
47
+ meta.scope = nil if stack_empty
48
+
49
+ # add backtrace for slow calls ... how is exclusive time handled?
50
+ if duration > 0.5 and !stack_empty
51
+ meta.extra = {:backtrace => caller.find_all { |c| c =~ /\/app\//}}
35
52
  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?
53
+ stat = transaction_hash[meta] || ScoutRails::MetricStats.new(!stack_empty)
54
+
40
55
  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"
56
+ transaction_hash[meta] = stat
57
+
58
+ if stack_empty
59
+ aggs=aggregate_calls(transaction_hash.dup,meta)
60
+ store_sample(options[:uri],transaction_hash.dup.merge(aggs),meta,stat)
61
+ # ugly attempt to see if deep dup is the issue
62
+ duplicate = aggs.dup
63
+ duplicate.each_pair do |k,v|
64
+ duplicate[k.dup] = v.dup
65
+ end
66
+ merge_data(duplicate.merge({meta.dup => stat.dup})) # aggregrates + controller
67
+ end
68
+ end
69
+
70
+ # Takes a metric_hash of calls and generates aggregates for ActiveRecord and View calls.
71
+ def aggregate_calls(metrics,parent_meta)
72
+ categories = %w(ActiveRecord View)
73
+ aggregates = {}
74
+ categories.each do |cat|
75
+ agg_meta=ScoutRails::MetricMeta.new("#{cat}/all")
76
+ agg_meta.scope = parent_meta.metric_name
77
+ agg_stats = ScoutRails::MetricStats.new
78
+ metrics.each do |meta,stats|
79
+ if meta.metric_name =~ /\A#{cat}\//
80
+ agg_stats.combine!(stats)
81
+ end
82
+ end # metrics.each
83
+ aggregates[agg_meta] = agg_stats unless agg_stats.call_count.zero?
84
+ end # categories.each
85
+ aggregates
86
+ end
87
+
88
+ # Stores the slowest transaction. This will be sent to the server.
89
+ def store_sample(uri,transaction_hash,parent_meta,parent_stat,options = {})
90
+ @transaction_sample_lock.synchronize do
91
+ if parent_stat.total_call_time > 1 and (@sample.nil? or (@sample and parent_stat.total_call_time > @sample.total_call_time))
92
+ @sample = ScoutRails::TransactionSample.new(uri,parent_meta.metric_name,parent_stat.total_call_time,transaction_hash.dup)
93
+ end
94
+ end
43
95
  end
44
96
 
45
97
  # Finds or creates the metric w/the given name in the metric_hash, and updates the time. Primarily used to
@@ -17,35 +17,45 @@ module ScoutRails::Tracer
17
17
  ScoutRails::Agent.instance.store
18
18
  end
19
19
 
20
- def instrument(metric_name, &block)
21
- #ScoutRails::Agent.instance.logger.debug "Instrumenting: #{metric_name} w/scope: #{Thread::current[:scout_scope_name]}"
20
+ # Use to trace a method call, possibly reporting slow transaction traces to Scout.
21
+ def trace(metric_name, options = {}, &block)
22
+ ScoutRails::Agent.instance.store.reset_transaction!
23
+ instrument(metric_name, options) do
24
+ Thread::current[:scout_scope_name] = metric_name
25
+ yield
26
+ Thread::current[:scout_scope_name] = nil
27
+ end
28
+ end
29
+
30
+ def instrument(metric_name, options={}, &block)
31
+ if options.delete(:scope)
32
+ Thread::current[:scout_sub_scope] = metric_name
33
+ end
22
34
  stack_item = store.record(metric_name)
23
- #ScoutRails::Agent.instance.logger.debug "Stack contains: #{store.stack.size} methods"
24
35
  begin
25
36
  yield
26
37
  ensure
27
- store.stop_recording(stack_item)
38
+ Thread::current[:scout_sub_scope] = nil if Thread::current[:scout_sub_scope] == metric_name
39
+ store.stop_recording(stack_item,options)
28
40
  end
29
41
  end
30
42
 
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)
43
+ def instrument_method(method,options = {})
44
+ metric_name = options[:metric_name] || default_metric_name(method)
34
45
  return if !instrumentable?(method) or instrumented?(method,metric_name)
35
- class_eval instrumented_method_string(method, metric_name), __FILE__, __LINE__
46
+ class_eval instrumented_method_string(method, {:metric_name => metric_name, :scope => options[:scope]}), __FILE__, __LINE__
36
47
 
37
48
  alias_method _uninstrumented_method_name(method, metric_name), method
38
49
  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
50
  end
41
51
 
42
52
  private
43
53
 
44
- def instrumented_method_string(method, metric_name)
54
+ def instrumented_method_string(method, options)
45
55
  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)
56
+ "def #{_instrumented_method_name(method, options[:metric_name])}(*args, &block)
57
+ result = #{klass}.instrument(\"#{options[:metric_name]}\",{:scope => #{options[:scope] || false}}) do
58
+ #{_uninstrumented_method_name(method, options[:metric_name])}(*args, &block)
49
59
  end
50
60
  result
51
61
  end"
@@ -0,0 +1,10 @@
1
+ class ScoutRails::TransactionSample
2
+ attr_reader :metric_name, :total_call_time, :metrics, :meta, :uri
3
+
4
+ def initialize(uri,metric_name,total_call_time,metrics)
5
+ @uri = uri
6
+ @metric_name = metric_name
7
+ @total_call_time = total_call_time
8
+ @metrics = metrics
9
+ end
10
+ end
@@ -1,3 +1,3 @@
1
1
  module ScoutRails
2
- VERSION = "0.0.3.pre"
2
+ VERSION = "0.0.4.pre"
3
3
  end
data/lib/scout_rails.rb CHANGED
@@ -14,6 +14,7 @@ require File.expand_path('../scout_rails/metric_stats.rb', __FILE__)
14
14
  require File.expand_path('../scout_rails/stack_item.rb', __FILE__)
15
15
  require File.expand_path('../scout_rails/store.rb', __FILE__)
16
16
  require File.expand_path('../scout_rails/tracer.rb', __FILE__)
17
+ require File.expand_path('../scout_rails/transaction_sample.rb', __FILE__)
17
18
  require File.expand_path('../scout_rails/instruments/process/process_cpu.rb', __FILE__)
18
19
  require File.expand_path('../scout_rails/instruments/process/process_memory.rb', __FILE__)
19
20
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3.pre
4
+ version: 0.0.4.pre
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-05-16 00:00:00.000000000 Z
13
+ date: 2012-06-12 00:00:00.000000000 Z
14
14
  dependencies: []
15
15
  description: Monitors a Ruby on Rails application and reports detailed metrics on
16
16
  performance to Scout, a hosted monitoring service.
@@ -43,6 +43,7 @@ files:
43
43
  - lib/scout_rails/stack_item.rb
44
44
  - lib/scout_rails/store.rb
45
45
  - lib/scout_rails/tracer.rb
46
+ - lib/scout_rails/transaction_sample.rb
46
47
  - lib/scout_rails/version.rb
47
48
  - scout_rails.gemspec
48
49
  homepage: https://github.com/scoutapp/scout_rails