Class: ScoutApm::Store

Inherits:
Object
  • Object
show all
Defined in:
lib/scout_apm/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 ScoutApm::Agent.instance.store.

Constant Summary collapse

MAX_SIZE =

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

1000
MAX_SLOW_TRANSACTIONS_TO_STORE_METRICS =

Limit the number of slow transactions that we store metrics with to prevent writing too much data to the layaway file if there are are many processes and many slow slow_transactions.

10

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeStore

Returns a new instance of Store.



19
20
21
22
23
24
25
26
27
28
# File 'lib/scout_apm/store.rb', line 19

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.
  @slow_transaction_lock = Mutex.new
  @slow_transactions = Array.new
end

Instance Attribute Details

#metric_hashObject

Returns the value of attribute metric_hash.



13
14
15
# File 'lib/scout_apm/store.rb', line 13

def metric_hash
  @metric_hash
end

#slow_transaction_lockObject (readonly)

Returns the value of attribute slow_transaction_lock.



17
18
19
# File 'lib/scout_apm/store.rb', line 17

def slow_transaction_lock
  @slow_transaction_lock
end

#slow_transactionsObject

array of slow transaction slow_transactions



16
17
18
# File 'lib/scout_apm/store.rb', line 16

def slow_transactions
  @slow_transactions
end

#stackObject

Returns the value of attribute stack.



15
16
17
# File 'lib/scout_apm/store.rb', line 15

def stack
  @stack
end

#transaction_hashObject

Returns the value of attribute transaction_hash.



14
15
16
# File 'lib/scout_apm/store.rb', line 14

def transaction_hash
  @transaction_hash
end

Instance Method Details

#aggregate_calls(metrics, parent_meta) ⇒ Object

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



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/scout_apm/store.rb', line 120

def aggregate_calls(metrics,parent_meta)
  categories = categories(metrics)
  aggregates = {}
  categories.each do |cat|
    agg_meta=ScoutApm::MetricMeta.new("#{cat}/all")
    agg_meta.scope = parent_meta.metric_name
    agg_stats = ScoutApm::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.



108
109
110
111
112
113
114
115
116
117
# File 'lib/scout_apm/store.rb', line 108

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]+)\//)
      cats << match[1]
    end
  end # metrics.each
  cats
end

#ignore_transaction!Object



39
40
41
# File 'lib/scout_apm/store.rb', line 39

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

#merge_data(old_data) ⇒ Object

Combines old and current data



163
164
165
# File 'lib/scout_apm/store.rb', line 163

def merge_data(old_data)
  {:metrics => merge_metrics(old_data[:metrics]), :slow_transactions => merge_slow_transactions(old_data[:slow_transactions])}
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



169
170
171
172
173
174
175
176
177
# File 'lib/scout_apm/store.rb', line 169

def merge_data_and_clear(old_data)
  merged = merge_data(old_data)
  self.metric_hash =  {}
  # TODO - is this lock needed?
  @slow_transaction_lock.synchronize do
    self.slow_transactions = []
  end
  merged
end

#merge_metrics(old_metrics) ⇒ Object



179
180
181
182
183
184
185
186
187
188
# File 'lib/scout_apm/store.rb', line 179

def merge_metrics(old_metrics)
  old_metrics.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_slow_transactions(old_slow_transactions) ⇒ Object

Merges slow_transactions together, removing transaction sample metrics from slow_transactions if the > MAX_SLOW_TRANSACTIONS_TO_STORE_METRICS



191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/scout_apm/store.rb', line 191

def merge_slow_transactions(old_slow_transactions)
  # need transaction lock here?
  self.slow_transactions += old_slow_transactions
  if trim_slow_transactions = self.slow_transactions[MAX_SLOW_TRANSACTIONS_TO_STORE_METRICS..-1]
    ScoutApm::Agent.instance.logger.debug "Trimming metrics from #{trim_slow_transactions.size} slow_transactions."
    i = MAX_SLOW_TRANSACTIONS_TO_STORE_METRICS
    trim_slow_transactions.each do |sample|
      self.slow_transactions[i] = sample.clear_metrics!
    end
  end
  self.slow_transactions
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.



48
49
50
51
52
# File 'lib/scout_apm/store.rb', line 48

def record(metric_name)
  item = ScoutApm::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.



32
33
34
35
36
37
# File 'lib/scout_apm/store.rb', line 32

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

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

Options:

  • :scope - If specified, sets the sub-scope for the metric. We allow additional scope level. This is used

  • uri - the request uri



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
89
90
91
92
93
94
95
96
97
# File 'lib/scout_apm/store.rb', line 57

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[:scout_apm_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
    ScoutApm::Agent.instance.logger.warn "Scope [#{Thread::current[:scout_apm_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 = ScoutApm::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 > ScoutApm::SlowTransaction::BACKTRACE_THRESHOLD and !stack_empty
    meta.extra = {:backtrace => ScoutApm::SlowTransaction.backtrace_parser(caller)}
  end
  stat = transaction_hash[meta] || ScoutApm::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_slow(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_metrics(duplicate.merge({meta.dup => stat.dup})) # aggregrates + controller
  end
end

#store_metric?(stack_empty) ⇒ Boolean

TODO - Move more logic to SlowTransaction

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)


103
104
105
# File 'lib/scout_apm/store.rb', line 103

def store_metric?(stack_empty)
  transaction_hash.size < ScoutApm::SlowTransaction::MAX_SIZE or stack_empty
end

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

Stores slow transactions. This will be sent to the server.



138
139
140
141
142
143
144
145
146
# File 'lib/scout_apm/store.rb', line 138

def store_slow(uri,transaction_hash,parent_meta,parent_stat,options = {})
  @slow_transaction_lock.synchronize do
    # tree map of all slow transactions
    if parent_stat.total_call_time >= 2
      @slow_transactions.push(ScoutApm::SlowTransaction.new(uri,parent_meta.metric_name,parent_stat.total_call_time,transaction_hash.dup,ScoutApm::Context.current,Thread::current[:scout_apm_trace_time]))
      ScoutApm::Agent.instance.logger.debug "Slow transaction sample added. [URI: #{uri}] [Context: #{ScoutApm::Context.current.to_hash}] Array Size: #{@slow_transactions.size}"
    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.



154
155
156
157
158
159
160
# File 'lib/scout_apm/store.rb', line 154

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