scout_apm 5.7.1 → 6.0.0

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +2 -0
  3. data/CHANGELOG.markdown +21 -1
  4. data/README.markdown +20 -8
  5. data/gems/instruments.gemfile +1 -0
  6. data/lib/scout_apm/auto_instrument/instruction_sequence.rb +2 -1
  7. data/lib/scout_apm/auto_instrument/parser.rb +150 -2
  8. data/lib/scout_apm/auto_instrument/prism.rb +357 -0
  9. data/lib/scout_apm/auto_instrument/rails.rb +9 -155
  10. data/lib/scout_apm/auto_instrument/requirements.rb +11 -0
  11. data/lib/scout_apm/background_job_integrations/delayed_job.rb +25 -1
  12. data/lib/scout_apm/background_job_integrations/faktory.rb +7 -1
  13. data/lib/scout_apm/background_job_integrations/good_job.rb +7 -1
  14. data/lib/scout_apm/background_job_integrations/legacy_sneakers.rb +7 -1
  15. data/lib/scout_apm/background_job_integrations/que.rb +7 -1
  16. data/lib/scout_apm/background_job_integrations/shoryuken.rb +7 -1
  17. data/lib/scout_apm/background_job_integrations/sidekiq.rb +89 -1
  18. data/lib/scout_apm/background_job_integrations/sneakers.rb +7 -1
  19. data/lib/scout_apm/background_job_integrations/solid_queue.rb +19 -1
  20. data/lib/scout_apm/config.rb +32 -7
  21. data/lib/scout_apm/context.rb +3 -1
  22. data/lib/scout_apm/error_service/error_record.rb +5 -1
  23. data/lib/scout_apm/instrument_manager.rb +2 -0
  24. data/lib/scout_apm/instruments/http_client.rb +10 -0
  25. data/lib/scout_apm/instruments/httpx.rb +119 -0
  26. data/lib/scout_apm/instruments/opensearch.rb +131 -0
  27. data/lib/scout_apm/limited_layer.rb +5 -2
  28. data/lib/scout_apm/logger.rb +1 -1
  29. data/lib/scout_apm/sampling.rb +25 -13
  30. data/lib/scout_apm/server_integrations/puma.rb +21 -4
  31. data/lib/scout_apm/version.rb +1 -1
  32. data/lib/scout_apm.rb +9 -4
  33. data/test/unit/auto_instrument/controller-ast.prism.txt +1015 -0
  34. data/test/unit/auto_instrument/controller-instrumented.rb +36 -11
  35. data/test/unit/auto_instrument/controller.rb +25 -0
  36. data/test/unit/auto_instrument/hash_shorthand_controller-instrumented.rb +28 -10
  37. data/test/unit/auto_instrument/hash_shorthand_controller.rb +19 -1
  38. data/test/unit/auto_instrument_test.rb +7 -1
  39. data/test/unit/background_job_integrations/faktory_test.rb +109 -0
  40. data/test/unit/background_job_integrations/shoryuken_test.rb +81 -0
  41. data/test/unit/background_job_integrations/sidekiq_test.rb +38 -0
  42. data/test/unit/config_test.rb +14 -0
  43. data/test/unit/error_service/error_buffer_test.rb +32 -0
  44. data/test/unit/error_test.rb +3 -3
  45. data/test/unit/ignored_uris_test.rb +7 -0
  46. data/test/unit/instruments/http_client_test.rb +0 -2
  47. data/test/unit/instruments/httpx_test.rb +78 -0
  48. data/test/unit/limited_layer_test.rb +4 -4
  49. data/test/unit/sampling_test.rb +10 -10
  50. metadata +10 -3
  51. data/lib/scout_apm/utils/time.rb +0 -12
  52. /data/test/unit/auto_instrument/{controller-ast.txt → controller-ast.parser.txt} +0 -0
@@ -1,6 +1,10 @@
1
1
 
2
2
  require 'scout_apm/auto_instrument/layer'
3
- require 'scout_apm/auto_instrument/parser'
3
+ if defined?(Prism)
4
+ require 'scout_apm/auto_instrument/prism'
5
+ else
6
+ require 'scout_apm/auto_instrument/parser'
7
+ end
4
8
 
5
9
  module ScoutApm
6
10
  module AutoInstrument
@@ -31,160 +35,10 @@ module ScoutApm
31
35
  end
32
36
 
33
37
  def self.rewrite(path, code = nil)
34
- code ||= File.read(path)
35
-
36
- ast = ::Parser::CurrentRuby.parse(code)
37
-
38
- # pp ast
39
-
40
- buffer = ::Parser::Source::Buffer.new(path)
41
- buffer.source = code
42
-
43
- rewriter = Rewriter.new
44
-
45
- # Rewrite the AST, returns a String with the new form.
46
- rewriter.rewrite(buffer, ast)
47
- end
48
-
49
- class Rewriter < ::Parser::TreeRewriter
50
- def initialize
51
- super
52
-
53
- # Keeps track of the parent - child relationship between nodes:
54
- @nesting = []
55
-
56
- # The stack of method nodes (type :def):
57
- @method = []
58
-
59
- # The stack of class nodes:
60
- @scope = []
61
-
62
- @cache = Cache.new
63
- end
64
-
65
- def instrument(source, file_name, line)
66
- # Don't log huge chunks of code... just the first line:
67
- if lines = source.lines and lines.count > 1
68
- source = lines.first.chomp + "..."
69
- end
70
-
71
- method_name = @method.last.children[0]
72
- bt = ["#{file_name}:#{line}:in `#{method_name}'"]
73
-
74
- return [
75
- "::ScoutApm::AutoInstrument("+ source.dump + ",#{bt}){",
76
- "}"
77
- ]
78
- end
79
-
80
- # Look up 1 or more nodes to check if the parent exists and matches the given type.
81
- # @param type [Symbol] the symbol type to match.
82
- # @param up [Integer] how far up to look.
83
- def parent_type?(type, up = 1)
84
- parent = @nesting[@nesting.size - up - 1] and parent.type == type
85
- end
86
-
87
- def on_block(node)
88
- # If we are not in a method, don't do any instrumentation:
89
- return if @method.empty?
90
-
91
- line = node.location.line || 'line?'
92
- column = node.location.column || 'column?' # not used
93
- method_name = node.children[0].children[1] || '*unknown*' # not used
94
- file_name = @source_rewriter.source_buffer.name
95
-
96
- wrap(node.location.expression, *instrument(node.location.expression.source, file_name, line))
97
- end
98
-
99
- def on_mlhs(node)
100
- # Ignore / don't instrument multiple assignment (LHS).
101
- return
102
- end
103
-
104
- def on_op_asgn(node)
105
- process(node.children[2])
106
- end
107
-
108
- def on_or_asgn(node)
109
- process(node.children[1])
110
- end
111
-
112
- def on_and_asgn(node)
113
- process(node.children[1])
114
- end
115
-
116
- # Handle the method call AST node. If this method doesn't call `super`, no futher rewriting is applied to children.
117
- def on_send(node)
118
- # We aren't interested in top level function calls:
119
- return if @method.empty?
120
-
121
- if @cache.local_assignments?(node)
122
- return super
123
- end
124
-
125
- # This ignores both initial block method invocation `*x*{}`, and subsequent nested invocations `x{*y*}`:
126
- return if parent_type?(:block)
127
-
128
- # Extract useful metadata for instrumentation:
129
- line = node.location.line || 'line?'
130
- column = node.location.column || 'column?' # not used
131
- method_name = node.children[1] || '*unknown*' # not used
132
- file_name = @source_rewriter.source_buffer.name
133
-
134
- # Wrap the expression with instrumentation:
135
- wrap(node.location.expression, *instrument(node.location.expression.source, file_name, line))
136
- end
137
-
138
- def on_hash(node)
139
- node.children.each do |pair|
140
- # Skip `pair` if we're sure it's not using the hash shorthand syntax
141
- next if pair.type != :pair
142
- key_node, value_node = pair.children
143
- next unless key_node.type == :sym && value_node.type == :send
144
- key = key_node.children[0]
145
- next unless value_node.children.size == 2 && value_node.children[0].nil? && key == value_node.children[1]
146
-
147
- # Extract useful metadata for instrumentation:
148
- line = pair.location.line || 'line?'
149
- # column = pair.location.column || 'column?' # not used
150
- # method_name = key || '*unknown*' # not used
151
- file_name = @source_rewriter.source_buffer.name
152
-
153
- instrument_before, instrument_after = instrument(pair.location.expression.source, file_name, line)
154
- replace(pair.loc.expression, "#{key}: #{instrument_before}#{key}#{instrument_after}")
155
- end
156
- super
157
- end
158
-
159
- # def on_class(node)
160
- # class_name = node.children[1]
161
- #
162
- # Kernel.const_get(class_name).ancestors.include? ActionController::Controller
163
- #
164
- # if class_name =~ /.../
165
- # super # continue processing
166
- # end
167
- # end
168
-
169
- # Invoked for every AST node as it is processed top to bottom.
170
- def process(node)
171
- # We are nesting inside this node:
172
- @nesting.push(node)
173
-
174
- if node and node.type == :def
175
- # If the node is a method, push it on the method stack as well:
176
- @method.push(node)
177
- super
178
- @method.pop
179
- elsif node and node.type == :class
180
- @scope.push(node.children[0])
181
- super
182
- @scope.pop
183
- else
184
- super
185
- end
186
-
187
- @nesting.pop
38
+ if defined?(Prism)
39
+ PrismImplementation.rewrite(path, code)
40
+ else
41
+ ParserImplementation.rewrite(path, code)
188
42
  end
189
43
  end
190
44
  end
@@ -0,0 +1,11 @@
1
+ begin
2
+ require 'prism'
3
+ rescue LoadError
4
+ end
5
+
6
+ unless defined?(Prism)
7
+ begin
8
+ require 'parser'
9
+ rescue LoadError
10
+ end
11
+ end
@@ -4,6 +4,7 @@ module ScoutApm
4
4
  ACTIVE_JOB_KLASS = 'ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper'.freeze
5
5
  DJ_PERFORMABLE_METHOD = 'Delayed::PerformableMethod'.freeze
6
6
 
7
+
7
8
  attr_reader :logger
8
9
 
9
10
  def name
@@ -69,8 +70,31 @@ module ScoutApm
69
70
 
70
71
  # Call the job itself.
71
72
  block.call(job, *args)
72
- rescue
73
+ rescue Exception => exception
74
+ # Capture the error for further processing and shipping
73
75
  req.error!
76
+ # Abusing this key to pass job info
77
+ params_key = 'action_dispatch.request.parameters'
78
+ env = {}
79
+
80
+ # Get job data safely - check for job_data first (ActiveJob), then fall back to args (PerformableMethod)
81
+ env[params_key] = if job.payload_object.respond_to?(:job_data)
82
+ job.payload_object.job_data
83
+ elsif job.payload_object.respond_to?(:args)
84
+ # For PerformableMethod, create a hash with relevant info
85
+ {
86
+ 'args' => job.payload_object.args,
87
+ 'method_name' => job.payload_object.method_name,
88
+ 'object' => job.payload_object.object.class.to_s
89
+ }
90
+ else
91
+ {}
92
+ end
93
+
94
+ env[:custom_controller] = name
95
+ env[:custom_action] = queue
96
+ context = ScoutApm::Agent.instance.context
97
+ context.error_buffer.capture(exception, env)
74
98
  raise
75
99
  ensure
76
100
  req.stop_layer if started_job
@@ -60,8 +60,14 @@ module ScoutApm
60
60
  started_job = true
61
61
 
62
62
  yield
63
- rescue
63
+ rescue Exception => exception
64
64
  req.error!
65
+ env = {
66
+ :custom_controller => job_class(job),
67
+ :custom_action => queue
68
+ }
69
+ context = ScoutApm::Agent.instance.context
70
+ context.error_buffer.capture(exception, env)
65
71
  raise
66
72
  ensure
67
73
  req.stop_layer if started_job
@@ -34,8 +34,14 @@ module ScoutApm
34
34
  started_job = true # Following Convention
35
35
 
36
36
  block.call
37
- rescue
37
+ rescue Exception => exception
38
38
  req.error!
39
+ env = {
40
+ :custom_controller => job.class.name,
41
+ :custom_action => job.queue_name.presence || UNKNOWN_QUEUE_PLACEHOLDER
42
+ }
43
+ context = ScoutApm::Agent.instance.context
44
+ context.error_buffer.capture(exception, env)
39
45
  raise
40
46
  ensure
41
47
  req.stop_layer if started_job
@@ -42,8 +42,14 @@ module ScoutApm
42
42
  else
43
43
  super
44
44
  end
45
- rescue Exception
45
+ rescue Exception => exception
46
46
  req.error!
47
+ env = {
48
+ :custom_controller => job_class,
49
+ :custom_action => queue
50
+ }
51
+ context = ScoutApm::Agent.instance.context
52
+ context.error_buffer.capture(exception, env)
47
53
  raise
48
54
  ensure
49
55
  req.stop_layer if started_job
@@ -115,8 +115,14 @@ module ScoutApm
115
115
  started_job = true
116
116
 
117
117
  _run_without_scout(*args)
118
- rescue Exception => e
118
+ rescue Exception => exception
119
119
  req.error!
120
+ env = {
121
+ :custom_controller => job_class,
122
+ :custom_action => queue
123
+ }
124
+ context = ScoutApm::Agent.instance.context
125
+ context.error_buffer.capture(exception, env)
120
126
  raise
121
127
  ensure
122
128
  req.stop_layer if started_job
@@ -79,8 +79,14 @@ module ScoutApm
79
79
  started_job = true
80
80
 
81
81
  yield
82
- rescue Exception => e
82
+ rescue Exception => exception
83
83
  req.error!
84
+ env = {
85
+ :custom_controller => job_class,
86
+ :custom_action => queue
87
+ }
88
+ context = ScoutApm::Agent.instance.context
89
+ context.error_buffer.capture(exception, env)
84
90
  raise
85
91
  ensure
86
92
  req.stop_layer if started_job
@@ -52,11 +52,14 @@ module ScoutApm
52
52
  def call(_worker, msg, queue)
53
53
  req = ScoutApm::RequestManager.lookup
54
54
  req.annotate_request(:queue_latency => latency(msg))
55
+ class_name = job_class(msg)
56
+
57
+ add_context!(msg, class_name) if capture_job_args?
55
58
 
56
59
  begin
57
60
  req.start_layer(ScoutApm::Layer.new('Queue', queue))
58
61
  started_queue = true
59
- req.start_layer(ScoutApm::Layer.new('Job', job_class(msg)))
62
+ req.start_layer(ScoutApm::Layer.new('Job', class_name))
60
63
  started_job = true
61
64
 
62
65
  yield
@@ -129,6 +132,41 @@ module ScoutApm
129
132
  UNKNOWN_CLASS_PLACEHOLDER
130
133
  end
131
134
 
135
+ def capture_job_args?
136
+ ScoutApm::Agent.instance.context.config.value("job_params_capture")
137
+ end
138
+
139
+ def add_context!(msg, class_name)
140
+ return if class_name == UNKNOWN_CLASS_PLACEHOLDER
141
+
142
+ klass = class_name.constantize rescue nil
143
+ return if klass.nil?
144
+
145
+ # Only allow required and optional parameters, as others aren't fully supported by Sidekiq by default.
146
+ # This also keeps it easy in terms of the canonical signature of parameters.
147
+ allowed_parameter_types = [:req, :opt]
148
+
149
+ known_parameters =
150
+ klass.instance_method(:perform).parameters.each_with_object([]) do |(type, name), acc|
151
+ acc << name if allowed_parameter_types.include?(type)
152
+ end
153
+
154
+ return if known_parameters.empty?
155
+
156
+ arguments = msg.fetch('args', [])
157
+
158
+ # Don't think this can actually happen. With perform_all_later,
159
+ # it appears we go through this middleware individually (even with multiples of the same job type).
160
+ return if arguments.length > 1
161
+
162
+ job_args = arguments.first.fetch('arguments', [])
163
+
164
+ # Reduce known parameters to just the ones that are present in the job arguments (excluding non altered optional params)
165
+ known_parameters = known_parameters[0...job_args.length]
166
+
167
+ ScoutApm::Context.add(filter_params(known_parameters.zip(job_args).to_h))
168
+ end
169
+
132
170
  def latency(msg, time = Time.now.to_f)
133
171
  created_at = msg['enqueued_at'] || msg['created_at']
134
172
  if created_at
@@ -146,6 +184,56 @@ module ScoutApm
146
184
  rescue
147
185
  0
148
186
  end
187
+
188
+ ###################
189
+ # Filtering Params
190
+ ###################
191
+
192
+ # Replaces parameter values with a string / set in config file
193
+ def filter_params(params)
194
+ return params unless filtered_params_config
195
+
196
+ params.each do |k, v|
197
+ if filter_key?(k)
198
+ params[k] = "[FILTERED]"
199
+ next
200
+ end
201
+
202
+ if filter_value?(v)
203
+ params[k] = "[UNSUPPORTED TYPE]"
204
+ end
205
+ end
206
+
207
+ params
208
+ end
209
+
210
+ def filter_value?(value)
211
+ !ScoutApm::Context::VALID_TYPES.any? { |klass| value.is_a?(klass) }
212
+ end
213
+
214
+ # Check, if a key should be filtered
215
+ def filter_key?(key)
216
+ params_to_filter.any? do |filter|
217
+ key.to_s == filter.to_s # key.to_s.include?(filter.to_s)
218
+ end
219
+ end
220
+
221
+ def params_to_filter
222
+ @params_to_filter ||= filtered_params_config + rails_filtered_params
223
+ end
224
+
225
+ # TODO: Flip this over to use a new class like filtered exceptions? Some shared logic between
226
+ # this and the error service.
227
+ def filtered_params_config
228
+ ScoutApm::Agent.instance.context.config.value("job_filtered_params")
229
+ end
230
+
231
+ def rails_filtered_params
232
+ return [] unless defined?(Rails)
233
+ Rails.configuration.filter_parameters
234
+ rescue
235
+ []
236
+ end
149
237
  end
150
238
  end
151
239
  end
@@ -65,8 +65,14 @@ module ScoutApm
65
65
  started_job = true
66
66
 
67
67
  process_work_without_scout(*args)
68
- rescue Exception => e
68
+ rescue Exception => exception
69
69
  req.error!
70
+ env = {
71
+ :custom_controller => job_class,
72
+ :custom_action => queue
73
+ }
74
+ context = ScoutApm::Agent.instance.context
75
+ context.error_buffer.capture(exception, env)
70
76
  raise
71
77
  ensure
72
78
  req.stop_layer if started_job
@@ -32,8 +32,26 @@ module ScoutApm
32
32
  started_job = true # Following Convention
33
33
 
34
34
  block.call
35
- rescue
35
+ rescue Exception => exception
36
36
  req.error!
37
+ # Extract job parameters like DelayedJob does
38
+ params_key = 'action_dispatch.request.parameters'
39
+ job_args = begin
40
+ {
41
+ arguments: job.arguments,
42
+ job_id: job.job_id,
43
+ }
44
+ rescue => e
45
+ { error_extracting_params: e.message }
46
+ end
47
+
48
+ env = {
49
+ params_key => job_args,
50
+ :custom_controller => job.class.name,
51
+ :custom_action => job.queue_name.presence || UNKNOWN_QUEUE_PLACEHOLDER
52
+ }
53
+ context = ScoutApm::Agent.instance.context
54
+ context.error_buffer.capture(exception, env)
37
55
  raise
38
56
  ensure
39
57
  req.stop_layer if started_job
@@ -29,6 +29,8 @@ require 'scout_apm/environment'
29
29
  # report_format - 'json' or 'marshal'. Marshal is legacy and will be removed.
30
30
  # scm_subdirectory - if the app root lives in source management in a subdirectory. E.g. #{SCM_ROOT}/src
31
31
  # uri_reporting - 'path' or 'full_path' default is 'full_path', which reports URL params as well as the path.
32
+ # job_params_capture - true/false to enable capturing of job args in the context.
33
+ # job_filtered_params - An array of job argument names to filter/redact out of job reports.
32
34
  # record_queue_time - true/false to enable recording of queuetime.
33
35
  # remote_agent_host - Internal: What host to bind to, and also send messages to for remote. Default: 127.0.0.1.
34
36
  # remote_agent_port - What port to bind the remote webserver to
@@ -44,11 +46,11 @@ require 'scout_apm/environment'
44
46
  # instruments listed in this array. Default: []
45
47
  # ignore_endpoints - An array of endpoints to ignore. These are matched as regular expressions. (supercedes 'ignore')
46
48
  # ignore_jobs - An array of job names to ignore.
47
- # sample_rate - Rate to sample entire application. An integer between 0 and 100. 0 means no traces are sent, 100 means all traces are sent.
48
- # sample_endpoints - An array of endpoints to sample. These are matched as regular expressions with individual sample rate of 0 to 100.
49
- # sample_jobs - An array of job names with individual sample rate of 0 to 100.
50
- # endpoint_sample_rate - Rate to sample all endpoints. An integer between 0 and 100. 0 means no traces are sent, 100 means all traces are sent. (supercedes 'sample_rate')
51
- # job_sample_rate - Rate to sample all jobs. An integer between 0 and 100. 0 means no traces are sent, 100 means all traces are sent. (supercedes 'sample_rate')
49
+ # sample_rate - Rate to sample entire application. A float between 0 and 1. 0 means no requests are tracked, 1 means all are, .05 means 5% are.
50
+ # sample_endpoints - An array of endpoints to sample. These are matched as regular expressions with individual sample rate of 0 to 1.
51
+ # sample_jobs - An array of job names with individual sample rate of 0 to 1.
52
+ # endpoint_sample_rate - Rate to sample all endpoints. A float between 0 and 1. 0 means no requests are tracked, 1 means all. (supercedes 'sample_rate')
53
+ # job_sample_rate - Rate to sample all jobs. A float between 0 and 1. 0 means no requests are tracked, 1 means all. (supercedes 'sample_rate')
52
54
  #
53
55
  #
54
56
  # Errors Service Configuration
@@ -96,6 +98,8 @@ module ScoutApm
96
98
  'profile',
97
99
  'proxy',
98
100
  'record_queue_time',
101
+ 'job_params_capture',
102
+ 'job_filtered_params',
99
103
  'remote_agent_host',
100
104
  'remote_agent_port',
101
105
  'report_format',
@@ -207,6 +211,23 @@ module ScoutApm
207
211
  end
208
212
  end
209
213
 
214
+ class SampleRateCoercion
215
+ def coerce(val)
216
+ v = val.to_f
217
+ # Anything above 1 is assumed a percentage for backwards compat, so convert to a decimal
218
+ if v > 1
219
+ v = v / 100
220
+ end
221
+ if v < 0 || v > 1
222
+ v = v.clamp(0, 1)
223
+ end
224
+ v
225
+ end
226
+ end
227
+
228
+ # Map of config keys to coercions. Any key not listed here will be passed
229
+ # through without modification.
230
+
210
231
 
211
232
  SETTING_COERCIONS = {
212
233
  'async_recording' => BooleanCoercion.new,
@@ -226,7 +247,9 @@ module ScoutApm
226
247
  'external_service_metric_report_limit' => IntegerCoercion.new,
227
248
  'instrument_http_url_length' => IntegerCoercion.new,
228
249
  'record_queue_time' => BooleanCoercion.new,
229
- 'sample_rate' => IntegerCoercion.new,
250
+ 'job_params_capture' => BooleanCoercion.new,
251
+ 'job_filtered_params' => JsonCoercion.new,
252
+ 'sample_rate' => SampleRateCoercion.new,
230
253
  'sample_endpoints' => JsonCoercion.new,
231
254
  'sample_jobs' => JsonCoercion.new,
232
255
  'endpoint_sample_rate' => NullableIntegerCoercion.new,
@@ -357,13 +380,15 @@ module ScoutApm
357
380
  'external_service_metric_limit' => 5000, # The hard limit on external service metrics
358
381
  'external_service_metric_report_limit' => 1000,
359
382
  'instrument_http_url_length' => 300,
360
- 'sample_rate' => 100,
383
+ 'sample_rate' => 1,
361
384
  'sample_endpoints' => [],
362
385
  'sample_jobs' => [],
363
386
  'endpoint_sample_rate' => nil,
364
387
  'job_sample_rate' => nil,
365
388
  'start_resque_server_instrument' => true, # still only starts if Resque is detected
366
389
  'collect_remote_ip' => true,
390
+ 'job_params_capture' => false,
391
+ 'job_filtered_params' => [],
367
392
  'record_queue_time' => true,
368
393
  'timeline_traces' => true,
369
394
  'auto_instruments' => false,
@@ -7,6 +7,8 @@ module ScoutApm
7
7
  class Context
8
8
  attr_reader :context
9
9
 
10
+ VALID_TYPES = [String, Symbol, Numeric, Time, Date, TrueClass, FalseClass]
11
+
10
12
  def initialize(context)
11
13
  @context = context
12
14
  @extra = {}
@@ -93,7 +95,7 @@ module ScoutApm
93
95
  value = key_value.values.last
94
96
  if value.nil?
95
97
  false # don't log this ... easy to happen
96
- elsif !valid_type?([String, Symbol, Numeric, Time, Date, TrueClass, FalseClass],value)
98
+ elsif !valid_type?(VALID_TYPES, value)
97
99
  logger.info "The value for [#{key_value.keys.first}] is not a valid type [#{value.class}]."
98
100
  false
99
101
  else
@@ -18,10 +18,12 @@ module ScoutApm
18
18
  @agent_context = agent_context
19
19
 
20
20
  @context = if context
21
- context.to_hash
21
+ context.to_flat_hash
22
22
  else
23
23
  {}
24
24
  end
25
+ # Add the transaction_id, as it won't be added to the context normally until the request has been recorded.
26
+ @context[:transaction_id] ||= RequestManager.lookup.transaction_id
25
27
 
26
28
  @exception_class = LengthLimit.new(exception.class.name).to_s
27
29
  @message = LengthLimit.new(exception.message, 100).to_s
@@ -46,6 +48,7 @@ module ScoutApm
46
48
  # For background workers like sidekiq
47
49
  # TODO: extract data creation for background jobs
48
50
  components[:controller] ||= env[:custom_controller]
51
+ components[:action] ||= env[:custom_action]
49
52
 
50
53
  components
51
54
  end
@@ -92,6 +95,7 @@ module ScoutApm
92
95
 
93
96
  # Capture params from env
94
97
  KEYS_TO_KEEP = [
98
+ "REQUEST_METHOD",
95
99
  "HTTP_USER_AGENT",
96
100
  "HTTP_REFERER",
97
101
  "HTTP_ACCEPT_ENCODING",
@@ -33,11 +33,13 @@ module ScoutApm
33
33
  install_instrument(ScoutApm::Instruments::Typhoeus)
34
34
  install_instrument(ScoutApm::Instruments::HttpClient)
35
35
  install_instrument(ScoutApm::Instruments::HTTP)
36
+ install_instrument(ScoutApm::Instruments::HTTPX)
36
37
  install_instrument(ScoutApm::Instruments::Memcached)
37
38
  install_instrument(ScoutApm::Instruments::Redis)
38
39
  install_instrument(ScoutApm::Instruments::Redis5)
39
40
  install_instrument(ScoutApm::Instruments::InfluxDB)
40
41
  install_instrument(ScoutApm::Instruments::Elasticsearch)
42
+ install_instrument(ScoutApm::Instruments::OpenSearch)
41
43
  install_instrument(ScoutApm::Instruments::Grape)
42
44
  rescue
43
45
  logger.warn "Exception loading instruments:"
@@ -15,8 +15,18 @@ module ScoutApm
15
15
  def installed?
16
16
  @installed
17
17
  end
18
+
19
+ def require_library
20
+ unless defined?(::HTTPClient)
21
+ begin
22
+ require 'httpclient'
23
+ rescue LoadError
24
+ end
25
+ end
26
+ end
18
27
 
19
28
  def install(prepend:)
29
+ require_library
20
30
  if defined?(::HTTPClient)
21
31
  @installed = true
22
32