scout_apm 2.6.6 → 4.0.4
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 +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
|