scout_apm 1.1.0.pre1 → 1.2.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +6 -0
  3. data/lib/scout_apm/agent/reporting.rb +67 -77
  4. data/lib/scout_apm/agent.rb +56 -9
  5. data/lib/scout_apm/background_job_integrations/delayed_job.rb +19 -0
  6. data/lib/scout_apm/background_job_integrations/sidekiq.rb +60 -0
  7. data/lib/scout_apm/bucket_name_splitter.rb +2 -2
  8. data/lib/scout_apm/capacity.rb +2 -1
  9. data/lib/scout_apm/context.rb +1 -5
  10. data/lib/scout_apm/environment.rb +16 -1
  11. data/lib/scout_apm/instruments/action_controller_rails_2.rb +13 -3
  12. data/lib/scout_apm/instruments/action_controller_rails_3.rb +20 -20
  13. data/lib/scout_apm/instruments/active_record.rb +5 -8
  14. data/lib/scout_apm/instruments/delayed_job.rb +56 -0
  15. data/lib/scout_apm/instruments/middleware.rb +44 -0
  16. data/lib/scout_apm/instruments/mongoid.rb +1 -1
  17. data/lib/scout_apm/instruments/moped.rb +2 -2
  18. data/lib/scout_apm/instruments/net_http.rb +1 -2
  19. data/lib/scout_apm/instruments/process/process_cpu.rb +5 -1
  20. data/lib/scout_apm/instruments/process/process_memory.rb +5 -1
  21. data/lib/scout_apm/instruments/sinatra.rb +14 -2
  22. data/lib/scout_apm/layaway.rb +33 -79
  23. data/lib/scout_apm/layaway_file.rb +2 -1
  24. data/lib/scout_apm/layer.rb +115 -0
  25. data/lib/scout_apm/layer_converter.rb +196 -0
  26. data/lib/scout_apm/metric_meta.rb +24 -4
  27. data/lib/scout_apm/metric_stats.rb +14 -4
  28. data/lib/scout_apm/request_manager.rb +26 -0
  29. data/lib/scout_apm/request_queue_time.rb +54 -0
  30. data/lib/scout_apm/serializers/payload_serializer.rb +8 -1
  31. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +1 -0
  32. data/lib/scout_apm/slow_transaction.rb +3 -0
  33. data/lib/scout_apm/store.rb +122 -190
  34. data/lib/scout_apm/tracer.rb +54 -83
  35. data/lib/scout_apm/tracked_request.rb +168 -0
  36. data/lib/scout_apm/version.rb +1 -1
  37. data/lib/scout_apm.rb +18 -5
  38. metadata +11 -2
@@ -1,11 +1,14 @@
1
1
  # Stats that are associated with each instrumented method.
2
- class ScoutApm::MetricStats
2
+ module ScoutApm
3
+ class MetricStats
3
4
  attr_accessor :call_count
4
5
  attr_accessor :min_call_time
5
6
  attr_accessor :max_call_time
6
7
  attr_accessor :total_call_time
7
8
  attr_accessor :total_exclusive_time
8
9
  attr_accessor :sum_of_squares
10
+ attr_accessor :queue
11
+ attr_accessor :latency
9
12
 
10
13
  def initialize(scoped = false)
11
14
  @scoped = scoped
@@ -17,16 +20,22 @@ class ScoutApm::MetricStats
17
20
  self.sum_of_squares = 0.0
18
21
  end
19
22
 
20
- def update!(call_time,exclusive_time)
23
+ # Note, that you must include exclusive_time if you wish to set
24
+ # extra_metrics. A two argument use of this method won't do that.
25
+ def update!(call_time, exclusive_time=call_time, extra_metrics={})
21
26
  # If this metric is scoped inside another, use exclusive time for min/max and sum_of_squares. Non-scoped metrics
22
27
  # (like controller actions) track the total call time.
23
28
  t = (@scoped ? exclusive_time : call_time)
24
29
  self.min_call_time = t if self.call_count == 0 or t < min_call_time
25
30
  self.max_call_time = t if self.call_count == 0 or t > max_call_time
26
- self.call_count +=1
31
+ self.call_count += 1
27
32
  self.total_call_time += call_time
28
33
  self.total_exclusive_time += exclusive_time
29
34
  self.sum_of_squares += (t * t)
35
+ if extra_metrics
36
+ self.queue = extra_metrics[:queue] if extra_metrics[:queue]
37
+ self.latency = extra_metrics[:latency] if extra_metrics[:latency]
38
+ end
30
39
  self
31
40
  end
32
41
 
@@ -52,4 +61,5 @@ class ScoutApm::MetricStats
52
61
  # uri, context
53
62
  ScoutApm::AttributeArranger.call(self, json_attributes)
54
63
  end
55
- end # class MetricStats
64
+ end
65
+ end
@@ -0,0 +1,26 @@
1
+ # Request manager handles the threadlocal variable that holds the current
2
+ # request. If there isn't one, then create one
3
+
4
+ module ScoutApm
5
+ class RequestManager
6
+ def self.lookup
7
+ find || create
8
+ end
9
+
10
+ # Get the current Thread local, and detecting, and not returning a stale request
11
+ def self.find
12
+ req = Thread.current[:scout_request]
13
+
14
+ if req && req.recorded?
15
+ nil
16
+ else
17
+ req
18
+ end
19
+ end
20
+
21
+ # Create a new TrackedRequest object for this thread
22
+ def self.create
23
+ Thread.current[:scout_request] = TrackedRequest.new
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,54 @@
1
+ module ScoutApm
2
+ class RequestQueueTime < LayerConverterBase
3
+ HEADERS = %w(X-Queue-Start X-Request-Start X-QUEUE-START X-REQUEST-START x-queue-start x-request-start)
4
+
5
+ # Headers is a hash of request headers. In Rails, request.headers would be appropriate
6
+ def initialize(request)
7
+ super(request)
8
+ @headers = request.headers
9
+ end
10
+
11
+ def call
12
+ return {} unless headers
13
+
14
+ raw_start = locate_timestamp
15
+ return {} unless raw_start
16
+
17
+ parsed_start = parse(raw_start)
18
+ return {} unless parsed_start
19
+
20
+ request_start = root_layer.start_time
21
+ queue_time = (request_start - parsed_start).to_f
22
+
23
+ meta = MetricMeta.new("QueueTime/Request", {:scope => scope_layer.legacy_metric_name})
24
+ stat = MetricStats.new(true)
25
+ stat.update!(queue_time)
26
+
27
+ { meta => stat }
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :headers
33
+
34
+ # Looks through the possible headers with this data, and extracts the raw
35
+ # value of the header
36
+ # Returns nil if not found
37
+ def locate_timestamp
38
+ return nil unless headers
39
+
40
+ header = HEADERS.find { |candidate| headers[candidate] }
41
+ if header
42
+ data = headers[header]
43
+ data.to_s.gsub(/(t=|\.)/, '')
44
+ else
45
+ nil
46
+ end
47
+ end
48
+
49
+ # Returns a timestamp in fractional seconds since epoch
50
+ def parse(time_string)
51
+ Time.at("#{time_string[0,10]}.#{time_string[10,13]}".to_f)
52
+ end
53
+ end
54
+ end
@@ -6,7 +6,14 @@ module ScoutApm
6
6
  if ScoutApm::Agent.instance.config.value("report_format") == 'json'
7
7
  ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions)
8
8
  else
9
- Marshal.dump(:metadata => metadata, :metrics => metrics, :slow_transactions => slow_transactions)
9
+ metadata = metadata.dup
10
+ metadata.default_proc = nil
11
+
12
+ metrics = metrics.dup
13
+ metrics.default_proc = nil
14
+ Marshal.dump(:metadata => metadata,
15
+ :metrics => metrics,
16
+ :slow_transactions => slow_transactions)
10
17
  end
11
18
  end
12
19
 
@@ -5,6 +5,7 @@ module ScoutApm
5
5
  def serialize(metadata, metrics, slow_transactions)
6
6
  rearranged_metrics = rearrange_the_metrics(metrics)
7
7
  rearranged_slow_transactions = rearrange_the_slow_transactions(slow_transactions)
8
+ metadata.merge!({:payload_version => 2})
8
9
  jsonify_hash({:metadata => metadata, :metrics => rearranged_metrics, :slow_transactions => rearranged_slow_transactions})
9
10
  end
10
11
 
@@ -16,6 +16,9 @@ module ScoutApm
16
16
  attr_reader :prof
17
17
  attr_reader :raw_prof
18
18
 
19
+ # TODO: Move this out of SlowTransaction, it doesn't have much to do w/
20
+ # slow trans other than being a piece of data that ends up in it.
21
+ #
19
22
  # Given a call stack, generates a filtered backtrace that:
20
23
  # * Limits to the app/models, app/controllers, or app/views directories
21
24
  # * Limits to 5 total callers
@@ -1,220 +1,152 @@
1
-
2
- # The store encapsolutes the logic that (1) saves instrumented data by Metric name to memory and (2) maintains a stack (just an Array)
3
- # of instrumented methods that are being called. It's accessed via +ScoutApm::Agent.instance.store+.
1
+ # Stores one or more minute's worth of Metrics/SlowTransactions in local ram.
2
+ # When informed to by the background worker, it pushes the in-ram metrics off to
3
+ # the layaway file for cross-process aggregation.
4
4
  module ScoutApm
5
5
  class Store
6
+ # A hash of reporting periods. { StoreReportingPeriodTimestamp => StoreReportingPeriod }
7
+ attr_reader :reporting_periods
6
8
 
7
- # Limits the size of the metric hash to prevent a metric explosion.
8
- MAX_SIZE = 1000
9
+ def initialize
10
+ @reporting_periods = Hash.new { |h,k| h[k] = StoreReportingPeriod.new(k) }
11
+ end
9
12
 
10
- # 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.
11
- MAX_SLOW_TRANSACTIONS_TO_STORE_METRICS = 10
13
+ def current_timestamp
14
+ StoreReportingPeriodTimestamp.new
15
+ end
12
16
 
13
- attr_accessor :metric_hash
14
- attr_accessor :transaction_hash
15
- attr_accessor :stack
16
- attr_accessor :slow_transactions # array of slow transaction slow_transactions
17
- attr_reader :slow_transaction_lock
17
+ # Save newly collected metrics
18
+ def track!(metrics, options={})
19
+ reporting_periods[current_timestamp].merge_metrics!(metrics)
20
+ end
18
21
 
19
- def initialize
20
- @metric_hash = Hash.new
21
- # Stores aggregate metrics for the current transaction. When the transaction is finished, metrics
22
- # are merged with the +metric_hash+.
23
- @transaction_hash = Hash.new
24
- @stack = Array.new
25
- # ensure background thread doesn't manipulate transaction sample while the store is.
26
- @slow_transaction_lock = Mutex.new
27
- @slow_transactions = Array.new
22
+ def track_one!(type, name, value, options={})
23
+ meta = MetricMeta.new("#{type}/#{name}")
24
+ stat = MetricStats.new(false)
25
+ stat.update!(value)
26
+ track!({meta => stat}, options)
28
27
  end
29
28
 
30
- # Called when the last stack item completes for the current transaction to clear
31
- # for the next run.
32
- def reset_transaction!
33
- Thread::current[:scout_apm_ignore_transaction] = nil
34
- Thread::current[:scout_apm_scope_name] = nil
35
- @transaction_hash = Hash.new
36
- @stack = Array.new
29
+ # Save a new slow transaction
30
+ def track_slow_transaction!(slow_transaction)
31
+ reporting_periods[current_timestamp].merge_slow_transactions!(slow_transaction)
37
32
  end
38
33
 
39
- def ignore_transaction!
40
- Thread::current[:scout_apm_ignore_transaction] = true
34
+ # Take each completed reporting_period, and write it to the layaway passed
35
+ def write_to_layaway(layaway)
36
+ reporting_periods.select { |time, rp| time.timestamp < current_timestamp.timestamp}.
37
+ each { |time, reporting_period|
38
+ layaway.add_reporting_period(time, reporting_period)
39
+ reporting_periods.delete(time)
40
+ }
41
41
  end
42
+ end
43
+
44
+ # A timestamp, normalized to the beginning of a minute. Used as a hash key to
45
+ # bucket metrics into per-minute groups
46
+ class StoreReportingPeriodTimestamp
47
+ attr_reader :timestamp
42
48
 
43
- # Called at the start of Tracer#instrument:
44
- # (1) Either finds an existing MetricStats object in the metric_hash or
45
- # initialize a new one. An existing MetricStats object is present if this +metric_name+ has already been instrumented.
46
- # (2) Adds a StackItem to the stack. This StackItem is returned and later used to validate the item popped off the stack
47
- # when an instrumented code block completes.
48
- def record(metric_name)
49
- item = ScoutApm::StackItem.new(metric_name)
50
- stack << item
51
- item
49
+ def initialize(time=Time.now)
50
+ @raw_time = time.utc # The actual time passed in. Store it so we can to_s it without reparsing a timestamp
51
+ @timestamp = @raw_time.to_i - @raw_time.sec # The normalized time (integer) to compare by
52
52
  end
53
53
 
54
- # Options:
55
- # * :scope - If specified, sets the sub-scope for the metric. We allow additional scope level. This is used
56
- # * uri - the request uri
57
- def stop_recording(sanity_check_item, options={})
58
- item = stack.pop
59
- stack_empty = stack.empty?
54
+ def to_s
55
+ @raw_time.iso8601
56
+ end
60
57
 
61
- # if ignoring the transaction, the item is popped but nothing happens.
62
- if Thread::current[:scout_apm_ignore_transaction]
63
- return
64
- end
58
+ def eql?(o)
59
+ timestamp.eql?(o.timestamp)
60
+ end
65
61
 
66
- # unbalanced stack check - unreproducable cases have seen this occur. when it does, sets a Thread variable
67
- # so we ignore further recordings. +Store#reset_transaction!+ resets this.
68
- if item != sanity_check_item
69
- ScoutApm::Agent.instance.logger.warn "Scope [#{Thread::current[:scout_apm_scope_name]}] Popped off stack: #{item.inspect} Expected: #{sanity_check_item.inspect}. Aborting."
70
- ignore_transaction!
71
- return
72
- end
62
+ def hash
63
+ timestamp.hash
64
+ end
73
65
 
74
- duration = Time.now - item.start_time
75
- if last = stack.last
76
- last.children_time += duration
77
- end
66
+ def age_in_seconds
67
+ Time.now.to_i - timestamp
68
+ end
69
+ end
78
70
 
79
- meta = ScoutApm::MetricMeta.new(item.metric_name, :desc => options[:desc])
80
- meta.scope = nil if stack_empty
71
+ # One period of Storage. Typically 1 minute
72
+ class StoreReportingPeriod
73
+ # A hash of { MetricMeta => MetricStat }
74
+ # This holds metrics for specific parts of the application.
75
+ # "Controller/user/index", "ActiveRecord/SQL/users/find", "View/users/_gravatar" and similar.
76
+ #
77
+ # If over the course of a minute a metric is called more than once (very likely), it will be
78
+ # combined with the others of the same type, and summed/calculated. The merging logic is in
79
+ # MetricStats
80
+ #
81
+ # Use the accessor function `metrics_payload` for most uses. It includes the calculated aggregate values
82
+ attr_reader :metrics
81
83
 
82
- # add backtrace for slow calls ... how is exclusive time handled?
83
- if duration > ScoutApm::SlowTransaction::BACKTRACE_THRESHOLD and !stack_empty
84
- meta.extra = {:backtrace => ScoutApm::SlowTransaction.backtrace_parser(caller)}
85
- end
84
+ # An array of SlowTransaction objects
85
+ attr_reader :slow_transactions
86
86
 
87
- stat = transaction_hash[meta] || ScoutApm::MetricStats.new(!stack_empty)
88
- stat.update!(duration,duration-item.children_time)
89
- transaction_hash[meta] = stat if store_metric?(stack_empty)
90
-
91
- # Uses controllers as the entry point for a transaction. Otherwise, stats are ignored.
92
- if stack_empty and meta.metric_name.match(/\AController\//)
93
- aggs = aggregate_calls(transaction_hash.dup,meta)
94
- store_slow(options[:uri], transaction_hash.dup.merge(aggs), meta, stat)
95
- # deep duplicate
96
- duplicate = aggs.dup
97
- duplicate.each_pair do |k,v|
98
- duplicate[k.dup] = v.dup
99
- end
100
- merge_metrics(duplicate.merge({meta.dup => stat.dup})) # aggregrates + controller
101
- end
87
+ # A StoreReportingPeriodTimestamp representing the time that this
88
+ # collection of metrics is for
89
+ attr_reader :timestamp
90
+
91
+ def initialize(timestamp)
92
+ @metrics = Hash.new
93
+ @slow_transactions = Array.new
94
+ @timestamp = timestamp
102
95
  end
103
96
 
104
- # TODO - Move more logic to SlowTransaction
105
- #
106
- # Limits the size of the transaction hash to prevent a large transactions. The final item on the stack
107
- # is allowed to be stored regardless of hash size to wrapup the transaction sample w/the parent metric.
108
- def store_metric?(stack_empty)
109
- transaction_hash.size < ScoutApm::SlowTransaction::MAX_SIZE or stack_empty
110
- end
111
-
112
- # Returns the top-level category names used in the +metrics+ hash.
113
- def categories(metrics)
114
- cats = Set.new
115
- metrics.keys.each do |meta|
116
- next if meta.scope.nil? # ignore controller
117
- if match=meta.metric_name.match(/\A([\w]+)\//)
118
- cats << match[1]
119
- end
120
- end # metrics.each
121
- cats
122
- end
123
-
124
- # Takes a metric_hash of calls and generates aggregates for ActiveRecord and View calls.
125
- def aggregate_calls(metrics,parent_meta)
126
- categories = categories(metrics)
127
- aggregates = {}
128
- categories.each do |cat|
129
- agg_meta=ScoutApm::MetricMeta.new("#{cat}/all")
130
- agg_meta.scope = parent_meta.metric_name
131
- agg_stats = ScoutApm::MetricStats.new
132
- metrics.each do |meta,stats|
133
- if meta.metric_name =~ /\A#{cat}\//
134
- agg_stats.combine!(stats)
135
- end
136
- end # metrics.each
137
- aggregates[agg_meta] = agg_stats unless agg_stats.call_count.zero?
138
- end # categories.each
139
- aggregates
140
- end
141
-
142
- SLOW_TRANSACTION_THRESHOLD = 2
143
-
144
- # Stores slow transactions. This will be sent to the server.
145
- def store_slow(uri, transaction_hash, parent_meta, parent_stat, options = {})
146
- @slow_transaction_lock.synchronize do
147
- if parent_stat.total_call_time >= SLOW_TRANSACTION_THRESHOLD
148
- slow_transaction = ScoutApm::SlowTransaction.new(uri,
149
- parent_meta.metric_name,
150
- parent_stat.total_call_time,
151
- transaction_hash.dup,
152
- ScoutApm::Context.current,
153
- Time.now,
154
- Thread::current[:scout_apm_prof])
155
- @slow_transactions.push(slow_transaction)
156
- ScoutApm::Agent.instance.logger.debug "Slow transaction sample added. [URI: #{uri}] [Context: #{ScoutApm::Context.current.to_hash}] Array Size: #{@slow_transactions.size}"
157
- end
158
- end
97
+ #################################
98
+ # Add metrics as they are recorded
99
+ #################################
100
+ def merge_metrics!(metrics)
101
+ @metrics.merge!(metrics) { |key, old_stat, new_stat| old_stat.combine!(new_stat) }
102
+ self
159
103
  end
160
104
 
161
- # Finds or creates the metric w/the given name in the metric_hash, and updates the time. Primarily used to
162
- # record sampled metrics. For instrumented methods, #record and #stop_recording are used.
163
- #
164
- # Options:
165
- # :scope => If provided, overrides the default scope.
166
- # :exclusive_time => Sets the exclusive time for the method. If not provided, uses +call_time+.
167
- def track!(metric_name, call_time, options = {})
168
- meta = ScoutApm::MetricMeta.new(metric_name)
169
- meta.scope = options[:scope] if options.has_key?(:scope)
170
- stat = metric_hash[meta] || ScoutApm::MetricStats.new
171
- stat.update!(call_time,options[:exclusive_time] || call_time)
172
- metric_hash[meta] = stat
173
- end
174
-
175
- # Combines old and current data
176
- def merge_data(old_data)
177
- {
178
- :metrics => merge_metrics(old_data[:metrics]),
179
- :slow_transactions => merge_slow_transactions(old_data[:slow_transactions])
180
- }
181
- end
182
-
183
- # Merges old and current data, clears the current in-memory metric hash, and returns
184
- # the merged data
185
- def merge_data_and_clear(old_data)
186
- merged = merge_data(old_data)
187
- self.metric_hash = {}
188
- # TODO - is this lock needed?
189
- @slow_transaction_lock.synchronize do
190
- self.slow_transactions = []
191
- end
192
- merged
105
+ def merge_slow_transactions!(slow_transactions)
106
+ @slow_transactions += Array(slow_transactions)
107
+ self
193
108
  end
194
109
 
195
- def merge_metrics(old_metrics)
196
- old_metrics.each do |old_meta,old_stats|
197
- if stats = metric_hash[old_meta]
198
- metric_hash[old_meta] = stats.combine!(old_stats)
199
- elsif metric_hash.size < MAX_SIZE
200
- metric_hash[old_meta] = old_stats
201
- end
202
- end
203
- metric_hash
204
- end
205
-
206
- # Merges slow_transactions together, removing transaction sample metrics from slow_transactions if the > MAX_SLOW_TRANSACTIONS_TO_STORE_METRICS
207
- def merge_slow_transactions(old_slow_transactions)
208
- # need transaction lock here?
209
- self.slow_transactions += old_slow_transactions
210
- if trim_slow_transactions = self.slow_transactions[MAX_SLOW_TRANSACTIONS_TO_STORE_METRICS..-1]
211
- ScoutApm::Agent.instance.logger.debug "Trimming metrics from #{trim_slow_transactions.size} slow_transactions."
212
- i = MAX_SLOW_TRANSACTIONS_TO_STORE_METRICS
213
- trim_slow_transactions.each do |sample|
214
- self.slow_transactions[i] = sample.clear_metrics!
110
+ #################################
111
+ # Retrieve Metrics for reporting
112
+ #################################
113
+ def metrics_payload
114
+ aggregate_metrics
115
+ end
116
+
117
+ def slow_transactions_payload
118
+ @slow_transactions
119
+ end
120
+
121
+ private
122
+
123
+ # We can't aggregate CPU, Memory, Capacity, or Controller, so pass through these metrics directly
124
+ # TODO: Figure out a way to not have this duplicate what's in Samplers, and also on server's ingest
125
+ PASSTHROUGH_METRICS = ["CPU", "Memory", "Instance", "Controller"]
126
+
127
+ # Calculate any aggregate metrics necessary.
128
+ #
129
+ # A hash of { MetricMeta => MetricStat }
130
+ # This represents the aggregate metrics over the course of the minute.
131
+ # "ActiveRecord/all", "View/all", "HTTP/all" and similar
132
+ def aggregate_metrics
133
+ hsh = Hash.new {|h,k| h[k] = MetricStats.new }
134
+
135
+ @metrics.inject(hsh) do |result, (meta, stat)|
136
+ if PASSTHROUGH_METRICS.include?(meta.type) # Leave as-is, don't attempt to combine
137
+ hsh[meta] = stat
138
+ elsif meta.type == "Errors" # Sadly special cased, we want both raw and aggregate values
139
+ hsh[meta] = stat
140
+ agg_meta = MetricMeta.new("Errors/Request", :scope => meta.scope)
141
+ hsh[agg_meta].combine!(stat)
142
+ else # Combine down to a single /all key
143
+ agg_meta = MetricMeta.new("#{meta.type}/all", :scope => meta.scope)
144
+ hsh[agg_meta].combine!(stat)
215
145
  end
146
+
147
+ hsh
216
148
  end
217
- self.slow_transactions
218
149
  end
219
- end # class Store
150
+ end
220
151
  end
152
+