scout_apm 5.8.0 → 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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +2 -0
  3. data/CHANGELOG.markdown +14 -2
  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 +15 -1
  12. data/lib/scout_apm/background_job_integrations/sidekiq.rb +89 -1
  13. data/lib/scout_apm/config.rb +32 -7
  14. data/lib/scout_apm/context.rb +3 -1
  15. data/lib/scout_apm/error_service/error_record.rb +1 -1
  16. data/lib/scout_apm/instrument_manager.rb +2 -0
  17. data/lib/scout_apm/instruments/http_client.rb +10 -0
  18. data/lib/scout_apm/instruments/httpx.rb +119 -0
  19. data/lib/scout_apm/instruments/opensearch.rb +131 -0
  20. data/lib/scout_apm/sampling.rb +25 -13
  21. data/lib/scout_apm/server_integrations/puma.rb +21 -4
  22. data/lib/scout_apm/version.rb +1 -1
  23. data/lib/scout_apm.rb +9 -3
  24. data/test/unit/auto_instrument/controller-ast.prism.txt +1015 -0
  25. data/test/unit/auto_instrument/controller-instrumented.rb +36 -11
  26. data/test/unit/auto_instrument/controller.rb +25 -0
  27. data/test/unit/auto_instrument/hash_shorthand_controller-instrumented.rb +28 -10
  28. data/test/unit/auto_instrument/hash_shorthand_controller.rb +19 -1
  29. data/test/unit/auto_instrument_test.rb +7 -1
  30. data/test/unit/background_job_integrations/sidekiq_test.rb +38 -0
  31. data/test/unit/config_test.rb +14 -0
  32. data/test/unit/error_service/error_buffer_test.rb +31 -0
  33. data/test/unit/error_test.rb +1 -1
  34. data/test/unit/ignored_uris_test.rb +7 -0
  35. data/test/unit/instruments/http_client_test.rb +0 -2
  36. data/test/unit/instruments/httpx_test.rb +78 -0
  37. data/test/unit/sampling_test.rb +10 -10
  38. metadata +8 -2
  39. /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
@@ -76,7 +76,21 @@ module ScoutApm
76
76
  # Abusing this key to pass job info
77
77
  params_key = 'action_dispatch.request.parameters'
78
78
  env = {}
79
- env[params_key] = job.payload_object.job_data
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
+
80
94
  env[:custom_controller] = name
81
95
  env[:custom_action] = queue
82
96
  context = ScoutApm::Agent.instance.context
@@ -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
@@ -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,7 +18,7 @@ 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
@@ -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
 
@@ -0,0 +1,119 @@
1
+ module ScoutApm
2
+ module Instruments
3
+ class HTTPX
4
+ attr_reader :context
5
+
6
+ def initialize(context)
7
+ @context = context
8
+ @installed = false
9
+ end
10
+
11
+ def logger
12
+ context.logger
13
+ end
14
+
15
+ def installed?
16
+ @installed
17
+ end
18
+
19
+ def install(prepend:)
20
+ if defined?(::HTTPX) && defined?(::HTTPX::Session)
21
+ @installed = true
22
+
23
+ logger.info "Instrumenting HTTPX"
24
+
25
+ ::HTTPX::Session.send(:prepend, HTTPXInstrumentationPrepend)
26
+ end
27
+ end
28
+
29
+ module HTTPXInstrumentationPrepend
30
+ def request(*args, **params)
31
+ verb, desc = determine_verb_and_desc(*args, **params)
32
+
33
+ layer = ScoutApm::Layer.new("HTTP", verb)
34
+ layer.desc = desc
35
+
36
+ req = ScoutApm::RequestManager.lookup
37
+ req.start_layer(layer)
38
+
39
+ begin
40
+ super(*args, **params)
41
+ ensure
42
+ req.stop_layer
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ # See the following for various argument patterns:
49
+ # https://gitlab.com/os85/httpx/-/blob/v1.6.3/lib/httpx/session.rb?ref_type=tags#L87
50
+ def determine_verb_and_desc(*args, **params)
51
+ # Pattern 1: session.request(req1) or session.request(req1, req2, ...)
52
+ if args.first.is_a?(::HTTPX::Request)
53
+ if args.length > 1
54
+ return args.first.verb.to_s.upcase, "#{args.length} requests"
55
+ else
56
+ return args.first.verb.to_s.upcase, scout_url_desc(args.first.uri)
57
+ end
58
+ end
59
+
60
+ # Pattern 2: session.request("GET", "https://server.org/a")
61
+ # Pattern 3: session.request("GET", ["https://server.org/a", "https://server.org/b"])
62
+ # Pattern 4: session.request("POST", ["https://server.org/a"], form: { ... })
63
+ # Pattern 5: session.request("GET", ["https://..."], headers: { ... })
64
+ if args.first.is_a?(String) || args.first.is_a?(Symbol)
65
+ verb = args.first.to_s.upcase
66
+
67
+ if args[1].is_a?(String)
68
+ return verb, scout_url_desc(args[1])
69
+ elsif args[1].is_a?(Array)
70
+ return verb, scout_url_desc(args[1][0]) unless args[1].length > 1
71
+ return verb, "#{args[1].length} requests"
72
+ else
73
+ return verb, ""
74
+ end
75
+ end
76
+
77
+ # Pattern 6: session.request(["GET", "https://..."], ["GET", "https://..."])
78
+ # Pattern 7: session.request(["POST", "https://...", form: {...}], ["GET", "https://..."])
79
+ if args.first.is_a?(Array)
80
+ if args.length > 1
81
+ verb = args.first[0].to_s.upcase rescue "REQUEST"
82
+ return verb, "#{args.length} requests"
83
+ elsif args.first.length >= 2
84
+ verb = args.first[0].to_s.upcase rescue "REQUEST"
85
+ url = args.first[1]
86
+ return verb, scout_url_desc(url)
87
+ end
88
+ end
89
+
90
+ return "REQUEST", ""
91
+ end
92
+
93
+ def scout_url_desc(uri)
94
+ max_length = ScoutApm::Agent.instance.context.config.value('instrument_http_url_length')
95
+ uri_str = uri.to_s
96
+
97
+ # URI object
98
+ if uri.respond_to?(:host) && uri.respond_to?(:path)
99
+ path = uri.path.to_s
100
+ path = "/" if path.empty?
101
+ result = "#{uri.host}#{path.split('?').first}"
102
+ # String URL
103
+ elsif uri_str =~ %r{^https?://([^/]+)(/[^?]*)?}
104
+ host = $1
105
+ path = $2 || "/"
106
+ result = "#{host}#{path}"
107
+ else
108
+ # Fallback
109
+ result = uri_str.split('?').first
110
+ end
111
+
112
+ result[0..(max_length - 1)]
113
+ rescue => e
114
+ ""
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end