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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +2 -0
- data/CHANGELOG.markdown +21 -1
- data/README.markdown +20 -8
- data/gems/instruments.gemfile +1 -0
- data/lib/scout_apm/auto_instrument/instruction_sequence.rb +2 -1
- data/lib/scout_apm/auto_instrument/parser.rb +150 -2
- data/lib/scout_apm/auto_instrument/prism.rb +357 -0
- data/lib/scout_apm/auto_instrument/rails.rb +9 -155
- data/lib/scout_apm/auto_instrument/requirements.rb +11 -0
- data/lib/scout_apm/background_job_integrations/delayed_job.rb +25 -1
- data/lib/scout_apm/background_job_integrations/faktory.rb +7 -1
- data/lib/scout_apm/background_job_integrations/good_job.rb +7 -1
- data/lib/scout_apm/background_job_integrations/legacy_sneakers.rb +7 -1
- data/lib/scout_apm/background_job_integrations/que.rb +7 -1
- data/lib/scout_apm/background_job_integrations/shoryuken.rb +7 -1
- data/lib/scout_apm/background_job_integrations/sidekiq.rb +89 -1
- data/lib/scout_apm/background_job_integrations/sneakers.rb +7 -1
- data/lib/scout_apm/background_job_integrations/solid_queue.rb +19 -1
- data/lib/scout_apm/config.rb +32 -7
- data/lib/scout_apm/context.rb +3 -1
- data/lib/scout_apm/error_service/error_record.rb +5 -1
- data/lib/scout_apm/instrument_manager.rb +2 -0
- data/lib/scout_apm/instruments/http_client.rb +10 -0
- data/lib/scout_apm/instruments/httpx.rb +119 -0
- data/lib/scout_apm/instruments/opensearch.rb +131 -0
- data/lib/scout_apm/limited_layer.rb +5 -2
- data/lib/scout_apm/logger.rb +1 -1
- data/lib/scout_apm/sampling.rb +25 -13
- data/lib/scout_apm/server_integrations/puma.rb +21 -4
- data/lib/scout_apm/version.rb +1 -1
- data/lib/scout_apm.rb +9 -4
- data/test/unit/auto_instrument/controller-ast.prism.txt +1015 -0
- data/test/unit/auto_instrument/controller-instrumented.rb +36 -11
- data/test/unit/auto_instrument/controller.rb +25 -0
- data/test/unit/auto_instrument/hash_shorthand_controller-instrumented.rb +28 -10
- data/test/unit/auto_instrument/hash_shorthand_controller.rb +19 -1
- data/test/unit/auto_instrument_test.rb +7 -1
- data/test/unit/background_job_integrations/faktory_test.rb +109 -0
- data/test/unit/background_job_integrations/shoryuken_test.rb +81 -0
- data/test/unit/background_job_integrations/sidekiq_test.rb +38 -0
- data/test/unit/config_test.rb +14 -0
- data/test/unit/error_service/error_buffer_test.rb +32 -0
- data/test/unit/error_test.rb +3 -3
- data/test/unit/ignored_uris_test.rb +7 -0
- data/test/unit/instruments/http_client_test.rb +0 -2
- data/test/unit/instruments/httpx_test.rb +78 -0
- data/test/unit/limited_layer_test.rb +4 -4
- data/test/unit/sampling_test.rb +10 -10
- metadata +10 -3
- data/lib/scout_apm/utils/time.rb +0 -12
- /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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
@@ -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 =>
|
|
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 =>
|
|
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',
|
|
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 =>
|
|
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
|
data/lib/scout_apm/config.rb
CHANGED
|
@@ -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.
|
|
48
|
-
# sample_endpoints - An array of endpoints to sample. These are matched as regular expressions with individual sample rate of 0 to
|
|
49
|
-
# sample_jobs - An array of job names with individual sample rate of 0 to
|
|
50
|
-
# endpoint_sample_rate - Rate to sample all endpoints.
|
|
51
|
-
# job_sample_rate - Rate to sample all jobs.
|
|
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
|
-
'
|
|
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' =>
|
|
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,
|
data/lib/scout_apm/context.rb
CHANGED
|
@@ -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?(
|
|
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.
|
|
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
|
|