Class: ScoutApm::TrackedRequest

Inherits:
Object
  • Object
show all
Defined in:
lib/scout_apm/tracked_request.rb

Constant Summary collapse

BACKTRACE_BLACKLIST =
["Controller", "Job"]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(store) ⇒ TrackedRequest

Returns a new instance of TrackedRequest.



45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/scout_apm/tracked_request.rb', line 45

def initialize(store)
  @store = store #this is passed in so we can use a real store (normal operation) or fake store (instant mode only)
  @layers = []
  @call_set = Hash.new { |h, k| h[k] = CallSet.new }
  @annotations = {}
  @ignoring_children = 0
  @context = Context.new
  @root_layer = nil
  @error = false
  @instant_key = nil
  @mem_start = mem_usage
  @dev_trace =  ScoutApm::Agent.instance.config.value('dev_trace') && Rails.env.development?
end

Instance Attribute Details

#annotationsObject (readonly)

As we go through a request, instrumentation can mark more general data into the Request Known Keys:

:uri - the full URI requested by the user
:queue_latency - how long a background Job spent in the queue before starting processing


23
24
25
# File 'lib/scout_apm/tracked_request.rb', line 23

def annotations
  @annotations
end

#call_countsObject

This maintains a lookup hash of Layer names and call counts. It’s used to trigger fetching a backtrace on n+1 calls. Note that layer names might not be Strings - can alse be Utils::ActiveRecordMetricName. Also, this would fail for layers with same names across multiple types.



36
37
38
# File 'lib/scout_apm/tracked_request.rb', line 36

def call_counts
  @call_counts
end

#contextObject (readonly)

Context is application defined extra information. (ie, which user, what is their email/ip, what plan are they on, what locale are they using, etc) See documentation for examples on how to set this from a before_filter



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

def context
  @context
end

#dev_traceObject

Whereas the instant_key gets set per-request in reponse to a URL param, dev_trace is set in the config file



43
44
45
# File 'lib/scout_apm/tracked_request.rb', line 43

def dev_trace
  @dev_trace
end

#headersObject (readonly)

Headers as recorded by rails Can be nil if we never reach a Rails Controller



27
28
29
# File 'lib/scout_apm/tracked_request.rb', line 27

def headers
  @headers
end

#instant_keyObject

if there’s an instant_key, pass the transaction trace on for immediate reporting (in addition to the usual background aggregation) this is set in the controller instumentation (ActionControllerRails3Rails4 according)



40
41
42
# File 'lib/scout_apm/tracked_request.rb', line 40

def instant_key
  @instant_key
end

#request_typeObject (readonly)

What kind of request is this? A trace of a web request, or a background job? Use job! and web! to set, and job? and web? to query



31
32
33
# File 'lib/scout_apm/tracked_request.rb', line 31

def request_type
  @request_type
end

#root_layerObject (readonly)

The first layer registered with this request. All other layers will be children of this layer.



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

def root_layer
  @root_layer
end

Instance Method Details

#acknowledge_children!Object



322
323
324
325
326
# File 'lib/scout_apm/tracked_request.rb', line 322

def acknowledge_children!
  if @ignoring_children > 0
    @ignoring_children -= 1
  end
end

#annotate_request(hsh) ⇒ Object

As we learn things about this request, we can add data here. For instance, when we know where Rails routed this request to, we can store that scope info. Or as soon as we know which URI it was directed at, we can store that.

This data is internal to ScoutApm, to add custom information, use the Context api.



179
180
181
# File 'lib/scout_apm/tracked_request.rb', line 179

def annotate_request(hsh)
  @annotations.merge!(hsh)
end

#backtrace_thresholdObject

Grab backtraces more aggressively when running in dev trace mode



333
334
335
# File 'lib/scout_apm/tracked_request.rb', line 333

def backtrace_threshold
  dev_trace ? 0.05 : 0.5 # the minimum threshold in seconds to record the backtrace for a metric.
end

#capture_backtrace?(layer) ⇒ Boolean

Returns:

  • (Boolean)


112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/scout_apm/tracked_request.rb', line 112

def capture_backtrace?(layer)
  # Never capture backtraces for this kind of layer. The backtrace will
  # always be 100% framework code.
  return false if BACKTRACE_BLACKLIST.include?(layer.type)

  # Only capture backtraces if we're in a real "request". Otherwise we
  # can spend lot of time capturing backtraces from the internals of
  # Sidekiq, only to throw them away immediately.
  return false unless (web? || job?)

  # Capture any individually slow layer.
  return true if layer.total_exclusive_time > backtrace_threshold

  # Capture any layer that we've seen many times. Captures n+1 problems
  return true if @call_set[layer.name].capture_backtrace?

  # Don't capture otherwise
  false
end

#capture_mem_delta!Object



142
143
144
# File 'lib/scout_apm/tracked_request.rb', line 142

def capture_mem_delta!
  @mem_delta = mem_usage - @mem_start
end

#current_layerObject

Grab the currently running layer. Useful for adding additional data as we learn it. This is useful in ActiveRecord instruments, where we start the instrumentation early, and gradually learn more about the request that actually happened as we go (for instance, the # of records found, or the actual SQL generated).

Returns nil in the case there is no current layer. That would be normal for a completed TrackedRequest



107
108
109
# File 'lib/scout_apm/tracked_request.rb', line 107

def current_layer
  @layers.last
end

#error!Object

This request had an exception. Mark it down as an error



184
185
186
# File 'lib/scout_apm/tracked_request.rb', line 184

def error!
  @error = true
end

#error?Boolean

Returns:

  • (Boolean)


188
189
190
# File 'lib/scout_apm/tracked_request.rb', line 188

def error?
  @error
end

#finalized?Boolean

Are we finished with this request? We’re done if we have no layers left after popping one off

Returns:

  • (Boolean)


152
153
154
# File 'lib/scout_apm/tracked_request.rb', line 152

def finalized?
  @layers.none?
end

#ignore_children!Object

Enable this when you would otherwise double track something interesting. This came up when we implemented InfluxDB instrumentation, which is more specific, and useful than the fact that InfluxDB happens to use Net::HTTP internally

When enabled, new layers won’t be added to the current Request, and calls to stop_layer will be ignored.

Do not forget to turn if off when leaving a layer, it is the instrumentation’s task to do that.

When you use this in code, be sure to use it in this order:

start_layer ignore_children

-> call

acknowledge_children stop_layer

If you don’t call it in this order, it’s possible to get out of sync, and have an ignored start and an actually-executed stop, causing layers to get out of sync



318
319
320
# File 'lib/scout_apm/tracked_request.rb', line 318

def ignore_children!
  @ignoring_children += 1
end

#ignoring_children?Boolean

Returns:

  • (Boolean)


328
329
330
# File 'lib/scout_apm/tracked_request.rb', line 328

def ignoring_children?
  @ignoring_children > 0
end

#instant?Boolean

Returns:

  • (Boolean)


212
213
214
# File 'lib/scout_apm/tracked_request.rb', line 212

def instant?
  instant_key
end

#job!Object



196
197
198
# File 'lib/scout_apm/tracked_request.rb', line 196

def job!
  @request_type = "job"
end

#job?Boolean

Returns:

  • (Boolean)


200
201
202
# File 'lib/scout_apm/tracked_request.rb', line 200

def job?
  request_type == "job"
end

#mem_usageObject

This may be in bytes or KB based on the OSX. We store this as-is here and only do conversion to MB in Layer Converters.



138
139
140
# File 'lib/scout_apm/tracked_request.rb', line 138

def mem_usage
  ScoutApm::Instruments::Process::ProcessMemory.rss
end

#record!Object

Convert this request to the appropriate structure, then report it into the peristent Store object



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/scout_apm/tracked_request.rb', line 222

def record!
  @recorded = true

  # Bail out early if the user asked us to ignore this uri
  return if ScoutApm::Agent.instance.ignored_uris.ignore?(annotations[:uri])

  # Update immediate and long-term histograms for both job and web requests
  if unique_name != :unknown
    ScoutApm::Agent.instance.request_histograms.add(unique_name, root_layer.total_call_time)
    ScoutApm::Agent.instance.request_histograms_by_time[@store.current_timestamp].
      add(unique_name, root_layer.total_call_time)
  end

  metrics = LayerConverters::MetricConverter.new(self).call
  @store.track!(metrics)

  error_metrics = LayerConverters::ErrorConverter.new(self).call
  @store.track!(error_metrics)

  allocation_metrics = LayerConverters::AllocationMetricConverter.new(self).call
  @store.track!(allocation_metrics)

  if web?
    # Don't #call this - that's the job of the ScoredItemSet later.
    slow_converter = LayerConverters::SlowRequestConverter.new(self)
    @store.track_slow_transaction!(slow_converter)

    queue_time_metrics = LayerConverters::RequestQueueTimeConverter.new(self).call
    @store.track!(queue_time_metrics)

    # If there's an instant_key, it means we need to report this right away
    if instant?
      trace = slow_converter.call
      ScoutApm::InstantReporting.new(trace, instant_key).call()
    end
  end

  if job?
    job_metrics = LayerConverters::JobConverter.new(self).call
    @store.track_job!(job_metrics)

    job_converter = LayerConverters::SlowJobConverter.new(self)
    @store.track_slow_job!(job_converter)
  end

  allocation_metrics = LayerConverters::AllocationMetricConverter.new(self).call
  @store.track!(allocation_metrics)

end

#recorded?Boolean

Have we already persisted this request? Used to know when we should just create a new one (don’t attempt to add data to an already-recorded request). See RequestManager

Returns:

  • (Boolean)


287
288
289
# File 'lib/scout_apm/tracked_request.rb', line 287

def recorded?
  @recorded
end

#set_headers(headers) ⇒ Object



192
193
194
# File 'lib/scout_apm/tracked_request.rb', line 192

def set_headers(headers)
  @headers = headers
end

#start_layer(layer) ⇒ Object



59
60
61
62
63
64
65
66
67
# File 'lib/scout_apm/tracked_request.rb', line 59

def start_layer(layer)
  if ignoring_children?
    return
  end

  start_request(layer) unless @root_layer
  @layers[-1].add_child(layer) if @layers.any?
  @layers.push(layer)
end

#start_request(layer) ⇒ Object

Run at the beginning of the whole request

  • Capture the first layer as the root_layer



159
160
161
# File 'lib/scout_apm/tracked_request.rb', line 159

def start_request(layer)
  @root_layer = layer unless @root_layer # capture root layer
end

#stop_layerObject



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/tracked_request.rb', line 69

def stop_layer
  return if ignoring_children?

  layer = @layers.pop

  # Safeguard against a mismatch in the layer tracking in an instrument.
  # This class works under the assumption that start & stop layers are
  # lined up correctly. If stop_layer gets called twice, when it should
  # only have been called once you'll end up with this error.
  if layer.nil?
    ScoutApm::Agent.instance.logger.warn("Error stopping layer, was nil. Root Layer: #{@root_layer.inspect}")
    stop_request
    return
  end

  layer.record_stop_time!
  layer.record_allocations!

  # This must be called before checking if a backtrace should be collected as the call count influences our capture logic.
  # We call `#update_call_counts in stop layer to ensure the layer has a final desc. Layer#desc is updated during the AR instrumentation flow.
  update_call_counts!(layer)
  if capture_backtrace?(layer)
    layer.capture_backtrace!
  end

  if finalized?
    stop_request
  end
end

#stop_requestObject

Run at the end of the whole request

  • Send the request off to be stored



166
167
168
# File 'lib/scout_apm/tracked_request.rb', line 166

def stop_request
  record!
end

#unique_nameObject

Only call this after the request is complete



273
274
275
276
277
278
279
280
281
282
# File 'lib/scout_apm/tracked_request.rb', line 273

def unique_name
  @unique_name ||= begin
                     scope_layer = LayerConverters::ConverterBase.new(self).scope_layer
                     if scope_layer
                       scope_layer.legacy_metric_name
                     else
                       :unknown
                     end
                   end
end

#update_call_counts!(layer) ⇒ Object

Maintains a lookup Hash of call counts by layer name. Used to determine if we should capture a backtrace.



133
134
135
# File 'lib/scout_apm/tracked_request.rb', line 133

def update_call_counts!(layer)
  @call_set[layer.name].update!(layer.desc)
end

#web!Object



204
205
206
# File 'lib/scout_apm/tracked_request.rb', line 204

def web!
  @request_type = "web"
end

#web?Boolean

Returns:

  • (Boolean)


208
209
210
# File 'lib/scout_apm/tracked_request.rb', line 208

def web?
  request_type == "web"
end