scout_apm 2.6.6 → 4.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +49 -0
- data/.rubocop.yml +2 -5
- data/.travis.yml +3 -7
- data/CHANGELOG.markdown +51 -0
- data/Gemfile +1 -8
- data/gems/rails6.gemfile +1 -1
- data/lib/scout_apm.rb +23 -1
- data/lib/scout_apm/agent.rb +22 -0
- data/lib/scout_apm/agent_context.rb +14 -2
- data/lib/scout_apm/background_job_integrations/delayed_job.rb +1 -1
- data/lib/scout_apm/background_job_integrations/faktory.rb +103 -0
- data/lib/scout_apm/background_job_integrations/sidekiq.rb +2 -2
- data/lib/scout_apm/config.rb +17 -2
- data/lib/scout_apm/detailed_trace.rb +2 -1
- data/lib/scout_apm/environment.rb +17 -1
- data/lib/scout_apm/error.rb +27 -0
- data/lib/scout_apm/error_service.rb +32 -0
- data/lib/scout_apm/error_service/error_buffer.rb +39 -0
- data/lib/scout_apm/error_service/error_record.rb +211 -0
- data/lib/scout_apm/error_service/ignored_exceptions.rb +66 -0
- data/lib/scout_apm/error_service/middleware.rb +32 -0
- data/lib/scout_apm/error_service/notifier.rb +33 -0
- data/lib/scout_apm/error_service/payload.rb +47 -0
- data/lib/scout_apm/error_service/periodic_work.rb +17 -0
- data/lib/scout_apm/error_service/railtie.rb +11 -0
- data/lib/scout_apm/error_service/sidekiq.rb +80 -0
- data/lib/scout_apm/extensions/transaction_callback_payload.rb +1 -1
- data/lib/scout_apm/instrument_manager.rb +1 -0
- data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +47 -26
- data/lib/scout_apm/instruments/action_view.rb +21 -8
- data/lib/scout_apm/instruments/active_record.rb +17 -28
- data/lib/scout_apm/instruments/typhoeus.rb +88 -0
- data/lib/scout_apm/layer.rb +1 -1
- data/lib/scout_apm/middleware.rb +1 -1
- data/lib/scout_apm/remote/server.rb +13 -1
- data/lib/scout_apm/reporter.rb +8 -3
- data/lib/scout_apm/serializers/payload_serializer_to_json.rb +6 -2
- data/lib/scout_apm/slow_policy/age_policy.rb +33 -0
- data/lib/scout_apm/slow_policy/percent_policy.rb +22 -0
- data/lib/scout_apm/slow_policy/percentile_policy.rb +24 -0
- data/lib/scout_apm/slow_policy/policy.rb +21 -0
- data/lib/scout_apm/slow_policy/speed_policy.rb +16 -0
- data/lib/scout_apm/slow_request_policy.rb +18 -77
- data/lib/scout_apm/tracer.rb +2 -2
- data/lib/scout_apm/utils/sql_sanitizer.rb +1 -0
- data/lib/scout_apm/utils/sql_sanitizer_regex.rb +3 -3
- data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +1 -0
- data/lib/scout_apm/version.rb +1 -1
- data/scout_apm.gemspec +6 -6
- data/test/unit/agent_context_test.rb +29 -0
- data/test/unit/environment_test.rb +2 -2
- data/test/unit/error_service/error_buffer_test.rb +25 -0
- data/test/unit/error_service/ignored_exceptions_test.rb +49 -0
- data/test/unit/serializers/payload_serializer_test.rb +36 -0
- data/test/unit/slow_request_policy_test.rb +41 -13
- data/test/unit/sql_sanitizer_test.rb +38 -0
- data/test/unit/tracer_test.rb +25 -0
- metadata +27 -61
- data/lib/scout_apm/slow_job_policy.rb +0 -111
- data/test/unit/slow_job_policy_test.rb +0 -6
@@ -30,6 +30,7 @@ module ScoutApm
|
|
30
30
|
ScoutApm::BackgroundJobIntegrations::Sneakers.new,
|
31
31
|
ScoutApm::BackgroundJobIntegrations::DelayedJob.new,
|
32
32
|
ScoutApm::BackgroundJobIntegrations::Que.new,
|
33
|
+
ScoutApm::BackgroundJobIntegrations::Faktory.new,
|
33
34
|
]
|
34
35
|
|
35
36
|
FRAMEWORK_INTEGRATIONS = [
|
@@ -182,9 +183,24 @@ module ScoutApm
|
|
182
183
|
@ruby_2 = defined?(RUBY_VERSION) && RUBY_VERSION.match(/^2/)
|
183
184
|
end
|
184
185
|
|
186
|
+
def ruby_3?
|
187
|
+
return @ruby_3 if defined?(@ruby_3)
|
188
|
+
@ruby_3 = defined?(RUBY_VERSION) && RUBY_VERSION.match(/^3/)
|
189
|
+
end
|
190
|
+
|
191
|
+
def ruby_minor
|
192
|
+
return @ruby_minor if defined?(@ruby_minor)
|
193
|
+
@ruby_minor = defined?(RUBY_VERSION) && RUBY_VERSION.split(".")[1].to_i
|
194
|
+
end
|
195
|
+
|
185
196
|
# Returns true if this Ruby version supports Module#prepend.
|
186
197
|
def supports_module_prepend?
|
187
|
-
ruby_2?
|
198
|
+
ruby_2? || ruby_3?
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns true if this Ruby version makes positional and keyword arguments incompatible
|
202
|
+
def supports_kwarg_delegation?
|
203
|
+
ruby_3? || (ruby_2? && ruby_minor >= 7)
|
188
204
|
end
|
189
205
|
|
190
206
|
# Returns a string representation of the OS (ex: darwin, linux)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Public API for the Scout Error Monitoring service
|
2
|
+
#
|
3
|
+
# See-Also ScoutApm::Transaction and ScoutApm::Tracing for APM related APIs
|
4
|
+
module ScoutApm
|
5
|
+
module Error
|
6
|
+
# Capture an exception, optionally with an environment hash. This may be a
|
7
|
+
# Rack environment, but is not required.
|
8
|
+
def self.capture(exception, env={})
|
9
|
+
context = ScoutApm::Agent.instance.context
|
10
|
+
|
11
|
+
# Skip if error monitoring isn't enabled at all
|
12
|
+
if ! context.config.value("errors_enabled")
|
13
|
+
return false
|
14
|
+
end
|
15
|
+
|
16
|
+
# Skip if this one error is ignored
|
17
|
+
if context.ignored_exceptions.ignored?(exception)
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
21
|
+
# Capture the error for further processing and shipping
|
22
|
+
context.error_buffer.capture(exception, env)
|
23
|
+
|
24
|
+
return true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "net/https"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module ScoutApm
|
6
|
+
module ErrorService
|
7
|
+
API_VERSION = "1"
|
8
|
+
|
9
|
+
HEADERS = {
|
10
|
+
"Content-type" => "application/json",
|
11
|
+
"Accept" => "application/json"
|
12
|
+
}
|
13
|
+
|
14
|
+
# Public API to force a given exception to be captured.
|
15
|
+
# Still obeys the ignore list
|
16
|
+
# Used internally by SidekiqException
|
17
|
+
def self.capture(exception, params = {})
|
18
|
+
return if disabled?
|
19
|
+
return if ScoutApm::Agent.instance.context.ignored_exceptions.ignore?(exception)
|
20
|
+
|
21
|
+
context.errors_buffer.capture(exception, env)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.enabled?
|
25
|
+
ScoutApm::Agent.instance.context.config.value("errors_enabled")
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.disabled?
|
29
|
+
!enabled?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Holds onto exceptions, and moves them forward to shipping when appropriate
|
2
|
+
module ScoutApm
|
3
|
+
module ErrorService
|
4
|
+
class ErrorBuffer
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_reader :agent_context
|
8
|
+
|
9
|
+
def initialize(agent_context)
|
10
|
+
@agent_context = agent_context
|
11
|
+
@error_records = []
|
12
|
+
@mutex = Monitor.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def capture(exception, env)
|
16
|
+
context = ScoutApm::Context.current
|
17
|
+
|
18
|
+
@mutex.synchronize {
|
19
|
+
@error_records << ErrorRecord.new(agent_context, exception, env, context)
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_and_reset_error_records
|
24
|
+
@mutex.synchronize {
|
25
|
+
ret = @error_records
|
26
|
+
@error_records = []
|
27
|
+
ret
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
# Enables enumerable - for count and each and similar methods
|
32
|
+
def each
|
33
|
+
@error_records.each do |error_record|
|
34
|
+
yield error_record
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module ErrorService
|
3
|
+
# Converts the raw error data captured into the captured data, and holds it
|
4
|
+
# until it's ready to be reported.
|
5
|
+
class ErrorRecord
|
6
|
+
attr_reader :exception_class
|
7
|
+
attr_reader :message
|
8
|
+
attr_reader :request_uri
|
9
|
+
attr_reader :request_params
|
10
|
+
attr_reader :request_session
|
11
|
+
attr_reader :environment
|
12
|
+
attr_reader :trace
|
13
|
+
attr_reader :request_components
|
14
|
+
attr_reader :context
|
15
|
+
|
16
|
+
def initialize(agent_context, exception, env, context=nil)
|
17
|
+
@agent_context = agent_context
|
18
|
+
|
19
|
+
@context = if context
|
20
|
+
context.to_hash
|
21
|
+
else
|
22
|
+
{}
|
23
|
+
end
|
24
|
+
|
25
|
+
@exception_class = LengthLimit.new(exception.class.name).to_s
|
26
|
+
@message = LengthLimit.new(exception.message, 100).to_s
|
27
|
+
@request_uri = LengthLimit.new(rack_request_url(env), 200).to_s
|
28
|
+
@request_params = clean_params(env["action_dispatch.request.parameters"])
|
29
|
+
@request_session = clean_params(session_data(env))
|
30
|
+
@environment = clean_params(strip_env(env))
|
31
|
+
@trace = clean_backtrace(exception.backtrace)
|
32
|
+
@request_components = components(env)
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO: This is rails specific
|
36
|
+
def components(env)
|
37
|
+
components = {}
|
38
|
+
unless env["action_dispatch.request.parameters"].nil?
|
39
|
+
components[:controller] = env["action_dispatch.request.parameters"][:controller] || nil
|
40
|
+
components[:action] = env["action_dispatch.request.parameters"][:action] || nil
|
41
|
+
components[:module] = env["action_dispatch.request.parameters"][:module] || nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# For background workers like sidekiq
|
45
|
+
# TODO: extract data creation for background jobs
|
46
|
+
components[:controller] ||= env[:custom_controller]
|
47
|
+
|
48
|
+
components
|
49
|
+
end
|
50
|
+
|
51
|
+
# TODO: Can I use the same thing we use in traces?
|
52
|
+
def rack_request_url(env)
|
53
|
+
protocol = rack_scheme(env)
|
54
|
+
protocol = protocol.nil? ? "" : "#{protocol}://"
|
55
|
+
|
56
|
+
host = env["SERVER_NAME"] || ""
|
57
|
+
path = env["REQUEST_URI"] || ""
|
58
|
+
port = env["SERVER_PORT"] || "80"
|
59
|
+
port = ["80", "443"].include?(port.to_s) ? "" : ":#{port}"
|
60
|
+
|
61
|
+
protocol.to_s + host.to_s + port.to_s + path.to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
def rack_scheme(env)
|
65
|
+
if env["HTTPS"] == "on"
|
66
|
+
"https"
|
67
|
+
elsif env["HTTP_X_FORWARDED_PROTO"]
|
68
|
+
env["HTTP_X_FORWARDED_PROTO"].split(",")[0]
|
69
|
+
else
|
70
|
+
env["rack.url_scheme"]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# TODO: This name is too vague
|
75
|
+
def clean_params(params)
|
76
|
+
return if params.nil?
|
77
|
+
|
78
|
+
normalized = normalize_data(params)
|
79
|
+
filter_params(normalized)
|
80
|
+
end
|
81
|
+
|
82
|
+
# TODO: When was backtrace_cleaner introduced?
|
83
|
+
def clean_backtrace(backtrace)
|
84
|
+
if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner)
|
85
|
+
Rails.backtrace_cleaner.send(:filter, backtrace)
|
86
|
+
else
|
87
|
+
backtrace
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Deletes params from env
|
92
|
+
#
|
93
|
+
# These are not configurable, and will leak PII info up to Scout if
|
94
|
+
# allowed through. Things like specific parameters can be exposed with
|
95
|
+
# the ScoutApm::Context interface.
|
96
|
+
KEYS_TO_REMOVE = [
|
97
|
+
"rack.request.form_hash",
|
98
|
+
"rack.request.form_vars",
|
99
|
+
"async.callback",
|
100
|
+
|
101
|
+
# Security related items
|
102
|
+
"action_dispatch.secret_key_base",
|
103
|
+
"action_dispatch.http_auth_salt",
|
104
|
+
"action_dispatch.signed_cookie_salt",
|
105
|
+
"action_dispatch.encrypted_cookie_salt",
|
106
|
+
"action_dispatch.encrypted_signed_cookie_salt",
|
107
|
+
"action_dispatch.authenticated_encrypted_cookie_salt",
|
108
|
+
|
109
|
+
# Raw data from the URL & parameters. Would bypass our normal params filtering
|
110
|
+
"QUERY_STRING",
|
111
|
+
"REQUEST_URI",
|
112
|
+
"REQUEST_PATH",
|
113
|
+
"ORIGINAL_FULLPATH",
|
114
|
+
"action_dispatch.request.query_parameters",
|
115
|
+
"action_dispatch.request.parameters",
|
116
|
+
"rack.request.query_string",
|
117
|
+
"rack.request.query_hash",
|
118
|
+
]
|
119
|
+
def strip_env(env)
|
120
|
+
env.reject { |k, v| KEYS_TO_REMOVE.include?(k) }
|
121
|
+
end
|
122
|
+
|
123
|
+
def session_data(env)
|
124
|
+
session = env["action_dispatch.request.session"]
|
125
|
+
return if session.nil?
|
126
|
+
|
127
|
+
if session.respond_to?(:to_hash)
|
128
|
+
session.to_hash
|
129
|
+
else
|
130
|
+
session.data
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# TODO: Rename and make this clearer. I think it maps over the whole tree of a hash, and to_s each leaf node?
|
135
|
+
def normalize_data(hash)
|
136
|
+
new_hash = {}
|
137
|
+
|
138
|
+
hash.each do |key, value|
|
139
|
+
if value.respond_to?(:to_hash)
|
140
|
+
begin
|
141
|
+
new_hash[key] = normalize_data(value.to_hash)
|
142
|
+
rescue
|
143
|
+
new_hash[key] = LengthLimit.new(value.to_s).to_s
|
144
|
+
end
|
145
|
+
else
|
146
|
+
new_hash[key] = LengthLimit.new(value.to_s).to_s
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
new_hash
|
151
|
+
end
|
152
|
+
|
153
|
+
###################
|
154
|
+
# Filtering Params
|
155
|
+
###################
|
156
|
+
|
157
|
+
# Replaces parameter values with a string / set in config file
|
158
|
+
def filter_params(params)
|
159
|
+
return params unless filtered_params_config
|
160
|
+
|
161
|
+
params.each do |k, v|
|
162
|
+
if filter_key?(k)
|
163
|
+
params[k] = "[FILTERED]"
|
164
|
+
elsif v.respond_to?(:to_hash)
|
165
|
+
filter_params(params[k])
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
params
|
170
|
+
end
|
171
|
+
|
172
|
+
# Check, if a key should be filtered
|
173
|
+
def filter_key?(key)
|
174
|
+
params_to_filter.any? do |filter|
|
175
|
+
key.to_s == filter.to_s # key.to_s.include?(filter.to_s)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def params_to_filter
|
180
|
+
@params_to_filter ||= filtered_params_config + rails_filtered_params
|
181
|
+
end
|
182
|
+
|
183
|
+
# Accessor for the filtered params config value. Will be removed as we refactor and clean up this code.
|
184
|
+
# TODO: Flip this over to use a new class like filtered exceptions?
|
185
|
+
def filtered_params_config
|
186
|
+
@agent_context.config.value("errors_filtered_params")
|
187
|
+
end
|
188
|
+
|
189
|
+
def rails_filtered_params
|
190
|
+
return [] unless defined?(Rails)
|
191
|
+
Rails.configuration.filter_parameters
|
192
|
+
rescue
|
193
|
+
[]
|
194
|
+
end
|
195
|
+
|
196
|
+
class LengthLimit
|
197
|
+
attr_reader :text
|
198
|
+
attr_reader :char_limit
|
199
|
+
|
200
|
+
def initialize(text, char_limit=100)
|
201
|
+
@text = text
|
202
|
+
@char_limit = char_limit
|
203
|
+
end
|
204
|
+
|
205
|
+
def to_s
|
206
|
+
text[0..char_limit]
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# Encapsulates the management and checking of ignored exceptions. Allows using
|
2
|
+
# string matches on the class name, or arbitrary matching with a callback
|
3
|
+
module ScoutApm
|
4
|
+
module ErrorService
|
5
|
+
class IgnoredExceptions
|
6
|
+
attr_reader :ignored_exceptions
|
7
|
+
attr_reader :blocks
|
8
|
+
|
9
|
+
def initialize(context, from_config)
|
10
|
+
@context = context
|
11
|
+
@ignored_exceptions = Array(from_config).map{ |e| normalize_as_klass(e) }
|
12
|
+
@blocks = []
|
13
|
+
end
|
14
|
+
|
15
|
+
# Add a single ignored exception by class name
|
16
|
+
def add(klass_or_str)
|
17
|
+
@ignored_exceptions << normalize_as_klass(klass_or_str)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Add a callback block that will be called on every error. If it returns
|
21
|
+
# Signature of blocks: ->(exception object): truthy or falsy value
|
22
|
+
def add_callback(&block)
|
23
|
+
@blocks << block
|
24
|
+
end
|
25
|
+
|
26
|
+
def ignored?(exception_object)
|
27
|
+
klass = normalize_as_klass(exception_object)
|
28
|
+
|
29
|
+
# Check if we ignored this error by name (typical way to ignore)
|
30
|
+
if ignored_exceptions.any? { |ignored| klass.ancestors.include?(ignored) }
|
31
|
+
return true
|
32
|
+
end
|
33
|
+
|
34
|
+
# For each block, see if it says we should ignore this error
|
35
|
+
blocks.each do |b|
|
36
|
+
if b.call(exception_object)
|
37
|
+
return true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def normalize_as_klass(klass_or_str)
|
47
|
+
if Module === klass_or_str
|
48
|
+
return klass_or_str
|
49
|
+
end
|
50
|
+
|
51
|
+
if klass_or_str.is_a?(Exception)
|
52
|
+
return klass_or_str.class
|
53
|
+
end
|
54
|
+
|
55
|
+
if String === klass_or_str
|
56
|
+
maybe = ScoutApm::Utils::KlassHelper.lookup(klass_or_str)
|
57
|
+
if Module === maybe
|
58
|
+
return maybe
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
klass_or_str
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module ErrorService
|
3
|
+
class Middleware
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
begin
|
10
|
+
response = @app.call(env)
|
11
|
+
rescue Exception => exception
|
12
|
+
puts "[Scout Error Service] Caught Exception: #{exception.class.name}"
|
13
|
+
|
14
|
+
context = ScoutApm::Agent.instance.context
|
15
|
+
|
16
|
+
# Bail out early, and reraise if the error is not interesting.
|
17
|
+
if context.ignored_exceptions.ignored?(exception)
|
18
|
+
raise
|
19
|
+
end
|
20
|
+
|
21
|
+
# Capture the error for further processing and shipping
|
22
|
+
context.error_buffer.capture(exception, env)
|
23
|
+
|
24
|
+
# Finally re-raise
|
25
|
+
raise
|
26
|
+
end
|
27
|
+
|
28
|
+
response
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|