Class: ScoutRails::Store

Inherits:
Object
  • Object
show all
Defined in:
lib/scout_rails/store.rb

Overview

The store encapsolutes the logic that (1) saves instrumented data by Metric name to memory and (2) maintains a stack (just an Array) of instrumented methods that are being called. It’s accessed via ScoutRails::Agent.instance.store.

Constant Summary collapse

MAX_SIZE =

Limits the size of the metric hash to prevent a metric explosion.

1000

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeStore

Returns a new instance of Store.



14
15
16
17
18
19
20
21
22
# File 'lib/scout_rails/store.rb', line 14

def initialize
  @metric_hash = Hash.new
  # Stores aggregate metrics for the current transaction. When the transaction is finished, metrics
  # are merged with the +metric_hash+.
  @transaction_hash = Hash.new
  @stack = Array.new
  # ensure background thread doesn't manipulate transaction sample while the store is.
  @transaction_sample_lock = Mutex.new
end

Instance Attribute Details

#metric_hashObject

Returns the value of attribute metric_hash.



8
9
10
# File 'lib/scout_rails/store.rb', line 8

def metric_hash
  @metric_hash
end

#sampleObject

Returns the value of attribute sample.



11
12
13
# File 'lib/scout_rails/store.rb', line 11

def sample
  @sample
end

#stackObject

Returns the value of attribute stack.



10
11
12
# File 'lib/scout_rails/store.rb', line 10

def stack
  @stack
end

#transaction_hashObject

Returns the value of attribute transaction_hash.



9
10
11
# File 'lib/scout_rails/store.rb', line 9

def transaction_hash
  @transaction_hash
end

#transaction_sample_lockObject (readonly)

Returns the value of attribute transaction_sample_lock.



12
13
14
# File 'lib/scout_rails/store.rb', line 12

def transaction_sample_lock
  @transaction_sample_lock
end

Instance Method Details

#aggregate_calls(metrics, parent_meta) ⇒ Object

Takes a metric_hash of calls and generates aggregates for ActiveRecord and View calls.



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/scout_rails/store.rb', line 111

def aggregate_calls(metrics,parent_meta)
  categories = categories(metrics)
  aggregates = {}
  categories.each do |cat|
    agg_meta=ScoutRails::MetricMeta.new("#{cat}/all")
    agg_meta.scope = parent_meta.metric_name
    agg_stats = ScoutRails::MetricStats.new
    metrics.each do |meta,stats|
      if meta.metric_name =~ /\A#{cat}\//
        agg_stats.combine!(stats) 
      end
    end # metrics.each
    aggregates[agg_meta] = agg_stats unless agg_stats.call_count.zero?
  end # categories.each    
  aggregates
end

#categories(metrics) ⇒ Object

Returns the top-level category names used in the metrics hash.



99
100
101
102
103
104
105
106
107
108
# File 'lib/scout_rails/store.rb', line 99

def categories(metrics)
  cats = Set.new
  metrics.keys.each do |meta|
    next if meta.scope.nil? # ignore controller
    if match=meta.metric_name.match(/\A([\w|\d]+)\//)
      cats << match[1]
    end
  end # metrics.each
  cats
end

#ignore_transaction!Object



33
34
35
# File 'lib/scout_rails/store.rb', line 33

def ignore_transaction!
  Thread::current[:ignore_transaction] = true
end

#merge_data(old_data) ⇒ Object

Combines old and current data



152
153
154
155
156
157
158
159
160
161
# File 'lib/scout_rails/store.rb', line 152

def merge_data(old_data)
  old_data.each do |old_meta,old_stats|
    if stats = metric_hash[old_meta]
      metric_hash[old_meta] = stats.combine!(old_stats)
    elsif metric_hash.size < MAX_SIZE
      metric_hash[old_meta] = old_stats
    end
  end
  metric_hash
end

#merge_data_and_clear(old_data) ⇒ Object

Merges old and current data, clears the current in-memory metric hash, and returns the merged data



165
166
167
168
169
# File 'lib/scout_rails/store.rb', line 165

def merge_data_and_clear(old_data)
  merged = merge_data(old_data)
  self.metric_hash =  {}
  merged
end

#record(metric_name) ⇒ Object

Called at the start of Tracer#instrument: (1) Either finds an existing MetricStats object in the metric_hash or initialize a new one. An existing MetricStats object is present if this metric_name has already been instrumented. (2) Adds a StackItem to the stack. This StackItem is returned and later used to validate the item popped off the stack when an instrumented code block completes.



42
43
44
45
46
# File 'lib/scout_rails/store.rb', line 42

def record(metric_name)
  item = ScoutRails::StackItem.new(metric_name)
  stack << item
  item
end

#reset_transaction!Object

Called when the last stack item completes for the current transaction to clear for the next run.



26
27
28
29
30
31
# File 'lib/scout_rails/store.rb', line 26

def reset_transaction!
  Thread::current[:ignore_transaction] = nil
  Thread::current[:scout_scope_name] = nil
  @transaction_hash = Hash.new
  @stack = Array.new
end

#stop_recording(sanity_check_item, options = {}) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/scout_rails/store.rb', line 48

def stop_recording(sanity_check_item, options={})
  item = stack.pop
  stack_empty = stack.empty?
  # if ignoring the transaction, the item is popped but nothing happens. 
  if Thread::current[:ignore_transaction]
    return
  end
  # unbalanced stack check - unreproducable cases have seen this occur. when it does, sets a Thread variable 
  # so we ignore further recordings. +Store#reset_transaction!+ resets this. 
  if item != sanity_check_item
    ScoutRails::Agent.instance.logger.warn "Scope [#{Thread::current[:scout_scope_name]}] Popped off stack: #{item.inspect} Expected: #{sanity_check_item.inspect}. Aborting."
    ignore_transaction!
    return
  end
  duration = Time.now - item.start_time
  if last=stack.last
    last.children_time += duration
  end
  meta = ScoutRails::MetricMeta.new(item.metric_name, :desc => options[:desc])
  meta.scope = nil if stack_empty
  
  # add backtrace for slow calls ... how is exclusive time handled?
  if duration > ScoutRails::TransactionSample::BACKTRACE_THRESHOLD and !stack_empty
    meta.extra = {:backtrace => ScoutRails::TransactionSample.backtrace_parser(caller)}
  end
  stat = transaction_hash[meta] || ScoutRails::MetricStats.new(!stack_empty)
  stat.update!(duration,duration-item.children_time)
  transaction_hash[meta] = stat if store_metric?(stack_empty)
  
  # Uses controllers as the entry point for a transaction. Otherwise, stats are ignored.
  if stack_empty and meta.metric_name.match(/\AController\//)
    aggs=aggregate_calls(transaction_hash.dup,meta)
    store_sample(options[:uri],transaction_hash.dup.merge(aggs),meta,stat)  
    # deep duplicate  
    duplicate = aggs.dup
    duplicate.each_pair do |k,v|
      duplicate[k.dup] = v.dup
    end  
    merge_data(duplicate.merge({meta.dup => stat.dup})) # aggregrates + controller 
  end
end

#store_metric?(stack_empty) ⇒ Boolean

TODO - Move more logic to TransactionSample

Limits the size of the transaction hash to prevent a large transactions. The final item on the stack is allowed to be stored regardless of hash size to wrapup the transaction sample w/the parent metric.

Returns:

  • (Boolean)


94
95
96
# File 'lib/scout_rails/store.rb', line 94

def store_metric?(stack_empty)
  transaction_hash.size < ScoutRails::TransactionSample::MAX_SIZE or stack_empty
end

#store_sample(uri, transaction_hash, parent_meta, parent_stat, options = {}) ⇒ Object

Stores the slowest transaction. This will be sent to the server.



129
130
131
132
133
134
135
# File 'lib/scout_rails/store.rb', line 129

def store_sample(uri,transaction_hash,parent_meta,parent_stat,options = {})    
  @transaction_sample_lock.synchronize do
    if parent_stat.total_call_time >= 2 and (@sample.nil? or (@sample and parent_stat.total_call_time > @sample.total_call_time))
      @sample = ScoutRails::TransactionSample.new(uri,parent_meta.metric_name,parent_stat.total_call_time,transaction_hash.dup)
    end
  end
end

#track!(metric_name, call_time, options = {}) ⇒ Object

Finds or creates the metric w/the given name in the metric_hash, and updates the time. Primarily used to record sampled metrics. For instrumented methods, #record and #stop_recording are used.

Options: :scope => If provided, overrides the default scope. :exclusive_time => Sets the exclusive time for the method. If not provided, uses call_time.



143
144
145
146
147
148
149
# File 'lib/scout_rails/store.rb', line 143

def track!(metric_name,call_time,options = {})
   meta = ScoutRails::MetricMeta.new(metric_name)
   meta.scope = options[:scope] if options.has_key?(:scope)
   stat = metric_hash[meta] || ScoutRails::MetricStats.new
   stat.update!(call_time,options[:exclusive_time] || call_time)
   metric_hash[meta] = stat
end