scout_apm 2.6.6 → 4.0.3
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 +45 -0
- data/Gemfile +1 -8
- data/gems/rails6.gemfile +1 -1
- data/lib/scout_apm.rb +22 -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/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 +16 -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 +28 -10
- 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/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
- metadata +26 -61
- data/lib/scout_apm/slow_job_policy.rb +0 -111
- data/test/unit/slow_job_policy_test.rb +0 -6
@@ -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
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module ErrorService
|
3
|
+
class Notifier
|
4
|
+
attr_reader :context
|
5
|
+
attr_reader :reporter
|
6
|
+
|
7
|
+
def initialize(context)
|
8
|
+
@context = context
|
9
|
+
@reporter = ScoutApm::Reporter.new(context, :errors)
|
10
|
+
end
|
11
|
+
|
12
|
+
def ship
|
13
|
+
error_records = context.error_buffer.get_and_reset_error_records
|
14
|
+
if error_records.any?
|
15
|
+
payload = ScoutApm::ErrorService::Payload.new(context, error_records)
|
16
|
+
reporter.report(
|
17
|
+
payload.serialize,
|
18
|
+
default_headers.merge("X-Error-Count" => error_records.length.to_s)
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def default_headers
|
26
|
+
{
|
27
|
+
"Content-Type" => "application/json",
|
28
|
+
"Accept" => "application/json"
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module ErrorService
|
3
|
+
class Payload
|
4
|
+
attr_reader :context
|
5
|
+
attr_reader :errors
|
6
|
+
|
7
|
+
def initialize(context, errors)
|
8
|
+
@context = context
|
9
|
+
@errors = errors
|
10
|
+
end
|
11
|
+
|
12
|
+
# TODO: Don't use to_json since it isn't supported in Ruby 1.8.7
|
13
|
+
def serialize
|
14
|
+
payload = as_json.to_json
|
15
|
+
context.logger.info(payload)
|
16
|
+
payload
|
17
|
+
end
|
18
|
+
|
19
|
+
def as_json
|
20
|
+
serialized_errors = errors.map do |error_record|
|
21
|
+
serialize_error_record(error_record)
|
22
|
+
end
|
23
|
+
|
24
|
+
{
|
25
|
+
:notifier => "scout_apm_ruby",
|
26
|
+
:environment => context.environment.env,
|
27
|
+
:root => context.environment.root,
|
28
|
+
:problems => serialized_errors,
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def serialize_error_record(error_record)
|
33
|
+
{
|
34
|
+
:exception_class => error_record.exception_class,
|
35
|
+
:message => error_record.message,
|
36
|
+
:request_uri => error_record.request_uri,
|
37
|
+
:request_params => error_record.request_params,
|
38
|
+
:request_session => error_record.request_session,
|
39
|
+
:environment => error_record.environment,
|
40
|
+
:trace => error_record.trace,
|
41
|
+
:request_components => error_record.request_components,
|
42
|
+
:context => error_record.context,
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module ErrorService
|
3
|
+
class PeriodicWork
|
4
|
+
attr_reader :context
|
5
|
+
|
6
|
+
def initialize(context)
|
7
|
+
@context = context
|
8
|
+
@notifier = ScoutApm::ErrorService::Notifier.new(context)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Expected to be called many times over the life of the agent
|
12
|
+
def run
|
13
|
+
@notifier.ship
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|