scout_apm 2.6.9 → 2.6.10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54f1c7e07f92a0d5d67a22354b0273c4894e8408ec00bcdada275457ed2f00f2
4
- data.tar.gz: 50cf2d441c948e769b2f2222895c84aadb0a5e3e3f75d16e647a2096a86551e4
3
+ metadata.gz: 503d33cb7c689cb239c328bee4ba2a1df380df703f3e3a6f5f843676b9bff0e4
4
+ data.tar.gz: 444e240dc8c4922ee932684c1c10b3fb5d658e96228c901b33a9a5ca3cb1e469
5
5
  SHA512:
6
- metadata.gz: 27a012457a6871cdbed206f2a55914163571e81af1b1f6253079c2fe9954354a9888b59627d832f3635a536ddf3f331c066b2bc4e053956f5135c706ffea1a17
7
- data.tar.gz: cfc5d80fb3b0ccebc2e78246a394a23a8e16d52a89868cff024931d6ce5b3539aafc42ef6f491105db43f8c544750b270cbbc7cc1fed556ab543bb0d963f1c75
6
+ metadata.gz: 715c6a3f044d2091207667fc6a9857573da528cf03ee5d64ccb758c10ec387b49829a891ae43bb51b444077c898f57fc73257f2d963407820ed7114fdedb52c4
7
+ data.tar.gz: 373b60dc42e0417e6003698c143138ff50024802285ff3754cc706efc1bc305a941fddbe27e6a471d8f09b3ffc84b5a3cfb43e57a1715968d7cac47ee069f5b3
@@ -1,3 +1,7 @@
1
+ # 2.6.10
2
+
3
+ * Fix an edge case in JSON serialization (#360)
4
+
1
5
  # 2.6.9
2
6
 
3
7
  * Add `ssl_cert_file` config option (#352)
@@ -186,6 +186,16 @@ require 'scout_apm/tasks/support'
186
186
  require 'scout_apm/extensions/config'
187
187
  require 'scout_apm/extensions/transaction_callback_payload'
188
188
 
189
+ require 'scout_apm/error_service'
190
+ require 'scout_apm/error_service/middleware'
191
+ require 'scout_apm/error_service/notifier'
192
+ require 'scout_apm/error_service/sidekiq'
193
+ require 'scout_apm/error_service/ignored_exceptions'
194
+ require 'scout_apm/error_service/error_buffer'
195
+ require 'scout_apm/error_service/error_record'
196
+ require 'scout_apm/error_service/periodic_work'
197
+ require 'scout_apm/error_service/payload'
198
+
189
199
  if defined?(Rails) && defined?(Rails::VERSION) && defined?(Rails::VERSION::MAJOR) && Rails::VERSION::MAJOR >= 3 && defined?(Rails::Railtie)
190
200
  module ScoutApm
191
201
  class Railtie < Rails::Railtie
@@ -205,6 +215,11 @@ if defined?(Rails) && defined?(Rails::VERSION) && defined?(Rails::VERSION::MAJOR
205
215
  ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is disabled.")
206
216
  end
207
217
 
218
+ if ScoutApm::Agent.instance.context.config.value("errors_enabled")
219
+ app.config.middleware.insert_after ActionDispatch::DebugExceptions, ScoutApm::ErrorService::Middleware
220
+ ScoutApm::ErrorService::Sidekiq.new.install
221
+ end
222
+
208
223
  # Install the middleware every time in development mode.
209
224
  # The middleware is a noop if dev_trace is not enabled in config
210
225
  if Rails.env.development?
@@ -66,6 +66,7 @@ module ScoutApm
66
66
 
67
67
  if context.started?
68
68
  start_background_worker unless background_worker_running?
69
+ start_error_service_background_worker unless error_service_background_worker_running?
69
70
  return
70
71
  end
71
72
 
@@ -81,6 +82,7 @@ module ScoutApm
81
82
  @app_server_load ||= AppServerLoad.new(context).run
82
83
 
83
84
  start_background_worker
85
+ start_error_service_background_worker
84
86
  end
85
87
 
86
88
  def instrument_manager
@@ -198,5 +200,25 @@ module ScoutApm
198
200
  @background_worker &&
199
201
  @background_worker.running?
200
202
  end
203
+
204
+ # seconds to batch error reports
205
+ ERROR_SEND_FREQUENCY = 5
206
+ def start_error_service_background_worker
207
+ periodic_work = ScoutApm::ErrorService::PeriodicWork.new(context)
208
+
209
+ @error_service_background_worker = ScoutApm::BackgroundWorker.new(context, ERROR_SEND_FREQUENCY)
210
+ @error_service_background_worker_thread = Thread.new do
211
+ @error_service_background_worker.start {
212
+ periodic_work.run
213
+ }
214
+ end
215
+ end
216
+
217
+ def error_service_background_worker_running?
218
+ @error_service_background_worker_thread &&
219
+ @error_service_background_worker_thread.alive? &&
220
+ @error_service_background_worker &&
221
+ @error_service_background_worker.running?
222
+ end
201
223
  end
202
224
  end
@@ -142,6 +142,18 @@ module ScoutApm
142
142
  config.value('dev_trace') && environment.env == "development"
143
143
  end
144
144
 
145
+ ###################
146
+ # Error Service #
147
+ ###################
148
+
149
+ def error_buffer
150
+ @error_buffer ||= ScoutApm::ErrorService::ErrorBuffer.new(self)
151
+ end
152
+
153
+ def ignored_exceptions
154
+ @ignored_exceptions ||= ScoutApm::ErrorService::IgnoredExceptions.new(self, config.value('errors_ignored_exceptions'))
155
+ end
156
+
145
157
  #############
146
158
  # Setters #
147
159
  #############
@@ -80,7 +80,13 @@ module ScoutApm
80
80
  'instrument_http_url_length',
81
81
  'timeline_traces',
82
82
  'auto_instruments',
83
- 'auto_instruments_ignore'
83
+ 'auto_instruments_ignore',
84
+
85
+ # Error Service Related Configuration
86
+ 'errors_enabled',
87
+ 'errors_ignored_exceptions',
88
+ 'errors_filtered_params',
89
+ 'errors_host',
84
90
  ]
85
91
 
86
92
  ################################################################################
@@ -176,6 +182,9 @@ module ScoutApm
176
182
  'timeline_traces' => BooleanCoercion.new,
177
183
  'auto_instruments' => BooleanCoercion.new,
178
184
  'auto_instruments_ignore' => JsonCoercion.new,
185
+ 'errors_enabled' => BooleanCoercion.new,
186
+ 'errors_ignored_exceptions' => JsonCoercion.new,
187
+ 'errors_filtered_params' => JsonCoercion.new,
179
188
  }
180
189
 
181
190
 
@@ -286,7 +295,11 @@ module ScoutApm
286
295
  'timeline_traces' => true,
287
296
  'auto_instruments' => false,
288
297
  'auto_instruments_ignore' => [],
289
- 'ssl_cert_file' => File.join( File.dirname(__FILE__), *%w[.. .. data cacert.pem] )
298
+ 'ssl_cert_file' => File.join( File.dirname(__FILE__), *%w[.. .. data cacert.pem] ),
299
+ 'errors_enabled' => false,
300
+ 'errors_ignored_exceptions' => %w(ActiveRecord::RecordNotFound ActionController::RoutingError),
301
+ 'errors_filtered_params' => %w(password s3-key),
302
+ 'errors_host' => 'https://errors.scoutapm.com',
290
303
  }.freeze
291
304
 
292
305
  def value(key)
@@ -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
@@ -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
@@ -0,0 +1,11 @@
1
+ module ScoutApm
2
+ module ErrorService
3
+ class Railtie < Rails::Railtie
4
+ initializer "scoutapm_error_service.middleware" do |app|
5
+ next if ScoutApm::Agent.instance.config.value("error_service")
6
+
7
+ app.config.middleware.insert_after ActionDispatch::DebugExceptions, ScoutApm::ErrorService::Rack
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,80 @@
1
+ module ScoutApm
2
+ module ErrorService
3
+ class Sidekiq
4
+ def initialize
5
+ @context = ScoutApm::Agent.instance.context
6
+ end
7
+
8
+ def install
9
+ return false unless defined?(::Sidekiq)
10
+
11
+ if ::Sidekiq::VERSION < "3"
12
+ install_sidekiq_with_middleware
13
+ else
14
+ install_sidekiq_with_error_handler
15
+ end
16
+
17
+ true
18
+ end
19
+
20
+ def install_sidekiq_with_middleware
21
+ # old behavior
22
+ ::Sidekiq.configure_server do |config|
23
+ config.server_middleware do |chain|
24
+ chain.add ScoutApm::ErrorService::Sidekiq::SidekiqExceptionMiddleware
25
+ end
26
+ end
27
+ end
28
+
29
+ def install_sidekiq_with_error_handler
30
+ ::Sidekiq.configure_server do |config|
31
+ config.error_handlers << proc { |exception, job_info|
32
+ context = ScoutApm::Agent.instance.context
33
+
34
+ # Bail out early, and reraise if the error is not interesting.
35
+ if context.ignored_exceptions.ignored?(exception)
36
+ raise
37
+ end
38
+
39
+ job_class =
40
+ begin
41
+ job_class = job_info[:job]["class"]
42
+ job_class = job_info[:job]["args"][0]["job_class"] if job_class == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
43
+ job_class
44
+ rescue
45
+ "UnknownJob"
46
+ end
47
+
48
+ # Capture the error for further processing and shipping
49
+ context.error_buffer.capture(exception, job_info.merge(:custom_controller => job_class))
50
+ }
51
+ end
52
+ end
53
+
54
+ class SidekiqExceptionMiddleware
55
+ def call(worker, msg, queue)
56
+ yield
57
+ rescue => exception
58
+ context = ScoutApm::Agent.instance.context
59
+
60
+ # Bail out early, and reraise if the error is not interesting.
61
+ if context.ignored_exceptions.ignored?(exception)
62
+ raise
63
+ end
64
+
65
+ # Capture the error for further processing and shipping
66
+ context.error_buffer.capture(
67
+ exception,
68
+ {
69
+ :custom_params => msg,
70
+ :custom_controller => msg["class"]
71
+ }
72
+ )
73
+
74
+ # Finally, reraise
75
+ raise exception
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -26,7 +26,7 @@ module ScoutApm
26
26
  ScoutApm::Agent.instance.start
27
27
  @started = ScoutApm::Agent.instance.context.started? && ScoutApm::Agent.instance.background_worker_running?
28
28
  rescue => e
29
- ScoutApm::Agent.instance.context.logger("Failed to start via Middleware: #{e.message}\n\t#{e.backtrace.join("\n\t")}")
29
+ ScoutApm::Agent.instance.context.logger.info("Failed to start via Middleware: #{e.message}\n\t#{e.backtrace.join("\n\t")}")
30
30
  end
31
31
  end
32
32
  end
@@ -22,6 +22,7 @@ module ScoutApm
22
22
  context.logger
23
23
  end
24
24
 
25
+ # The fully serialized string payload to be sent
25
26
  def report(payload, headers = {})
26
27
  hosts = determine_hosts
27
28
 
@@ -35,6 +36,7 @@ module ScoutApm
35
36
  logger.debug("Original Size: #{original_payload_size} Compressed Size: #{compress_payload_size}")
36
37
  end
37
38
 
39
+ logger.info("Posting payload to #{hosts.inspect}")
38
40
  post_payload(hosts, payload, headers)
39
41
  end
40
42
 
@@ -51,6 +53,8 @@ module ScoutApm
51
53
  URI.parse("#{host}/apps/deploy.scout?key=#{key}&name=#{encoded_app_name}")
52
54
  when :instant_trace
53
55
  URI.parse("#{host}/apps/instant_trace.scout?key=#{key}&name=#{encoded_app_name}&instant_key=#{instant_key}")
56
+ when :errors
57
+ URI.parse("#{host}/apps/error.scout?key=#{key}&name=#{encoded_app_name}")
54
58
  end.tap { |u| logger.debug("Posting to #{u}") }
55
59
  end
56
60
 
@@ -89,7 +93,7 @@ module ScoutApm
89
93
  logger.debug "got response: #{response.inspect}"
90
94
  case response
91
95
  when Net::HTTPSuccess, Net::HTTPNotModified
92
- logger.debug "/#{type} OK"
96
+ logger.debug "#{type} OK"
93
97
  when Net::HTTPBadRequest
94
98
  logger.warn "/#{type} FAILED: The Account Key [#{config.value('key')}] is invalid."
95
99
  when Net::HTTPUnprocessableEntity
@@ -141,6 +145,8 @@ module ScoutApm
141
145
  def determine_hosts
142
146
  if [:deploy_hook, :instant_trace].include?(type)
143
147
  config.value('direct_host')
148
+ elsif [:errors].include?(type)
149
+ config.value('errors_host')
144
150
  else
145
151
  config.value('host')
146
152
  end
@@ -45,18 +45,36 @@ module ScoutApm
45
45
  "{#{str_parts.join(",")}}"
46
46
  end
47
47
 
48
- ESCAPE_MAPPINGS = {
49
- "\b" => '\\b',
50
- "\t" => '\\t',
51
- "\n" => '\\n',
52
- "\f" => '\\f',
53
- "\r" => '\\r',
54
- '"' => '\\"',
55
- '\\' => '\\\\',
56
- }
48
+ # Ruby 1.8.7 seems to be fundamentally different in how gsub or regexes
49
+ # work. This is a hack and will be removed as soon as we can drop
50
+ # support
51
+ if RUBY_VERSION == "1.8.7"
52
+ ESCAPE_MAPPINGS = {
53
+ "\b" => '\\b',
54
+ "\t" => '\\t',
55
+ "\n" => '\\n',
56
+ "\f" => '\\f',
57
+ "\r" => '\\r',
58
+ '"' => '\\"',
59
+ '\\' => '\\\\',
60
+ }
61
+ else
62
+ ESCAPE_MAPPINGS = {
63
+ # Stackoverflow answer on gsub matches and backslashes - https://stackoverflow.com/a/4149087/2705125
64
+ '\\' => '\\\\\\\\',
65
+ "\b" => '\\b',
66
+ "\t" => '\\t',
67
+ "\n" => '\\n',
68
+ "\f" => '\\f',
69
+ "\r" => '\\r',
70
+ '"' => '\\"',
71
+ }
72
+ end
57
73
 
58
74
  def escape(string)
59
- ESCAPE_MAPPINGS.inject(string.to_s) {|s, (bad, good)| s.gsub(bad, good) }
75
+ ESCAPE_MAPPINGS.inject(string.to_s) {|s, (bad, good)|
76
+ s.gsub(bad, good)
77
+ }
60
78
  end
61
79
 
62
80
  def format_by_type(formatee)
@@ -1,3 +1,3 @@
1
1
  module ScoutApm
2
- VERSION = "2.6.9"
2
+ VERSION = "2.6.10"
3
3
  end
@@ -0,0 +1,15 @@
1
+ require "test_helper"
2
+
3
+ require "scout_apm/agent_context"
4
+
5
+ class AgentContextTest < Minitest::Test
6
+ def test_has_error_service_ignored_exceptions
7
+ context = ScoutApm::AgentContext.new
8
+ assert ScoutApm::ErrorService::IgnoredExceptions, context.ignored_exceptions.class
9
+ end
10
+
11
+ def test_has_error_buffer
12
+ context = ScoutApm::AgentContext.new
13
+ assert ScoutApm::ErrorService::ErrorBuffer, context.error_buffer.class
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ require "test_helper"
2
+
3
+ class ErrorBufferTest < Minitest::Test
4
+ class FakeError < StandardError
5
+ end
6
+
7
+ def test_captures_and_stores_exceptions_and_env
8
+ eb = ScoutApm::ErrorService::ErrorBuffer.new(context)
9
+ eb.capture(ex, env)
10
+ end
11
+
12
+ #### Helpers
13
+
14
+ def context
15
+ ScoutApm::AgentContext.new
16
+ end
17
+
18
+ def env
19
+ {}
20
+ end
21
+
22
+ def ex(msg="Whoops")
23
+ FakeError.new(msg)
24
+ end
25
+ end
@@ -0,0 +1,49 @@
1
+ require "test_helper"
2
+
3
+ class IgnoredExceptionsTest < Minitest::Test
4
+ class FakeError < StandardError
5
+ end
6
+
7
+ class SubclassFakeError < FakeError
8
+ end
9
+
10
+ def test_ignores_with_string_match
11
+ ig = ScoutApm::ErrorService::IgnoredExceptions.new(context, ["RuntimeError"])
12
+ assert ig.ignored?(RuntimeError.new("something went wrong"))
13
+ assert !ig.ignored?(FakeError.new("something went wrong"))
14
+ end
15
+
16
+ def test_ignores_with_block
17
+ ig = ScoutApm::ErrorService::IgnoredExceptions.new(context, [])
18
+ ig.add_callback { |e| e.message == "ignore me" }
19
+
20
+ should_ignore = RuntimeError.new("ignore me")
21
+ should_not_ignore = RuntimeError.new("super legit")
22
+
23
+ assert ig.ignored?(should_ignore)
24
+ assert !ig.ignored?(should_not_ignore)
25
+ end
26
+
27
+ def test_ignores_subclasses
28
+ ig = ScoutApm::ErrorService::IgnoredExceptions.new(context, ["IgnoredExceptionsTest::FakeError"])
29
+ assert ig.ignored?(SubclassFakeError.new("Subclass"))
30
+ end
31
+
32
+ # Check that a bad exception in the list doesn't stop the whole thing from working
33
+ def test_does_not_consider_unknown_errors
34
+ ig = ScoutApm::ErrorService::IgnoredExceptions.new(context, ["ThisDoesNotExist", "IgnoredExceptionsTest::FakeError"])
35
+ assert ig.ignored?(FakeError.new("ignore this one"))
36
+ end
37
+
38
+ def test_add_module
39
+ ig = ScoutApm::ErrorService::IgnoredExceptions.new(context, [])
40
+ ig.add(IgnoredExceptionsTest::FakeError)
41
+ assert ig.ignored?(FakeError.new("ignore this one"))
42
+ end
43
+
44
+ #### Helpers
45
+
46
+ def context
47
+ ScoutApm::AgentContext.new
48
+ end
49
+ end
@@ -108,4 +108,40 @@ class PayloadSerializerTest < Minitest::Test
108
108
  json = { "foo" => "\bbar\nbaz\r" }
109
109
  assert_equal json, JSON.parse(ScoutApm::Serializers::PayloadSerializerToJson.jsonify_hash(json))
110
110
  end
111
+
112
+ def test_escapes_escaped_quotes
113
+ # Some escapes haven't ever worked on 1.8.7, and is not the issue I'm
114
+ # fixing now. Remove this when we drop support for ancient ruby
115
+ skip if RUBY_VERSION == "1.8.7"
116
+
117
+ json = {"foo" => %q|`additional_details` = '{\"amount\":1}'|}
118
+ result = ScoutApm::Serializers::PayloadSerializerToJson.jsonify_hash(json)
119
+ assert_equal json, JSON.parse(result)
120
+ end
121
+
122
+ def test_escapes_various_special_characters
123
+ # Some escapes haven't ever worked on 1.8.7, and is not the issue I'm
124
+ # fixing now. Remove this when we drop support for ancient ruby
125
+ skip if RUBY_VERSION == "1.8.7"
126
+
127
+ json = {"foo" => [
128
+ %Q|\fbar|,
129
+ %Q|\rbar|,
130
+ %Q|\nbar|,
131
+ %Q|\tbar|,
132
+ %Q|"bar|,
133
+ %Q|'bar|,
134
+ %Q|{bar|,
135
+ %Q|}bar|,
136
+ %Q|\\bar|,
137
+ if RUBY_VERSION == '1.8.7'
138
+ ""
139
+ else
140
+ %Q|\\\nbar|
141
+ end,
142
+ ]}
143
+
144
+ result = ScoutApm::Serializers::PayloadSerializerToJson.jsonify_hash(json)
145
+ assert_equal json, JSON.parse(result)
146
+ end
111
147
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.9
4
+ version: 2.6.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Haynes
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-08-17 00:00:00.000000000 Z
12
+ date: 2020-10-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: minitest
@@ -267,6 +267,17 @@ files:
267
267
  - lib/scout_apm/debug.rb
268
268
  - lib/scout_apm/detailed_trace.rb
269
269
  - lib/scout_apm/environment.rb
270
+ - lib/scout_apm/error.rb
271
+ - lib/scout_apm/error_service.rb
272
+ - lib/scout_apm/error_service/error_buffer.rb
273
+ - lib/scout_apm/error_service/error_record.rb
274
+ - lib/scout_apm/error_service/ignored_exceptions.rb
275
+ - lib/scout_apm/error_service/middleware.rb
276
+ - lib/scout_apm/error_service/notifier.rb
277
+ - lib/scout_apm/error_service/payload.rb
278
+ - lib/scout_apm/error_service/periodic_work.rb
279
+ - lib/scout_apm/error_service/railtie.rb
280
+ - lib/scout_apm/error_service/sidekiq.rb
270
281
  - lib/scout_apm/extensions/config.rb
271
282
  - lib/scout_apm/extensions/transaction_callback_payload.rb
272
283
  - lib/scout_apm/fake_store.rb
@@ -390,6 +401,7 @@ files:
390
401
  - test/data/config_test_1.yml
391
402
  - test/test_helper.rb
392
403
  - test/tmp/README.md
404
+ - test/unit/agent_context_test.rb
393
405
  - test/unit/agent_test.rb
394
406
  - test/unit/auto_instrument/assignments-instrumented.rb
395
407
  - test/unit/auto_instrument/assignments.rb
@@ -405,6 +417,8 @@ files:
405
417
  - test/unit/db_query_metric_set_test.rb
406
418
  - test/unit/db_query_metric_stats_test.rb
407
419
  - test/unit/environment_test.rb
420
+ - test/unit/error_service/error_buffer_test.rb
421
+ - test/unit/error_service/ignored_exceptions_test.rb
408
422
  - test/unit/extensions/periodic_callbacks_test.rb
409
423
  - test/unit/extensions/transaction_callbacks_test.rb
410
424
  - test/unit/fake_store_test.rb
@@ -460,61 +474,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
460
474
  - !ruby/object:Gem::Version
461
475
  version: '0'
462
476
  requirements: []
463
- rubygems_version: 3.0.6
477
+ rubygems_version: 3.0.8
464
478
  signing_key:
465
479
  specification_version: 4
466
480
  summary: Ruby application performance monitoring
467
- test_files:
468
- - test/data/config_test_1.yml
469
- - test/test_helper.rb
470
- - test/tmp/README.md
471
- - test/unit/agent_test.rb
472
- - test/unit/auto_instrument/assignments-instrumented.rb
473
- - test/unit/auto_instrument/assignments.rb
474
- - test/unit/auto_instrument/controller-ast.txt
475
- - test/unit/auto_instrument/controller-instrumented.rb
476
- - test/unit/auto_instrument/controller.rb
477
- - test/unit/auto_instrument/rescue_from-instrumented.rb
478
- - test/unit/auto_instrument/rescue_from.rb
479
- - test/unit/auto_instrument_test.rb
480
- - test/unit/background_job_integrations/sidekiq_test.rb
481
- - test/unit/config_test.rb
482
- - test/unit/context_test.rb
483
- - test/unit/db_query_metric_set_test.rb
484
- - test/unit/db_query_metric_stats_test.rb
485
- - test/unit/environment_test.rb
486
- - test/unit/extensions/periodic_callbacks_test.rb
487
- - test/unit/extensions/transaction_callbacks_test.rb
488
- - test/unit/fake_store_test.rb
489
- - test/unit/git_revision_test.rb
490
- - test/unit/histogram_test.rb
491
- - test/unit/ignored_uris_test.rb
492
- - test/unit/instruments/active_record_test.rb
493
- - test/unit/instruments/net_http_test.rb
494
- - test/unit/instruments/percentile_sampler_test.rb
495
- - test/unit/layaway_test.rb
496
- - test/unit/layer_children_set_test.rb
497
- - test/unit/layer_converters/depth_first_walker_test.rb
498
- - test/unit/layer_converters/metric_converter_test.rb
499
- - test/unit/layer_converters/stubs.rb
500
- - test/unit/limited_layer_test.rb
501
- - test/unit/logger_test.rb
502
- - test/unit/metric_set_test.rb
503
- - test/unit/remote/test_message.rb
504
- - test/unit/remote/test_router.rb
505
- - test/unit/remote/test_server.rb
506
- - test/unit/request_histograms_test.rb
507
- - test/unit/scored_item_set_test.rb
508
- - test/unit/serializers/payload_serializer_test.rb
509
- - test/unit/slow_job_policy_test.rb
510
- - test/unit/slow_request_policy_test.rb
511
- - test/unit/sql_sanitizer_test.rb
512
- - test/unit/store_test.rb
513
- - test/unit/tracer_test.rb
514
- - test/unit/tracked_request_test.rb
515
- - test/unit/transaction_test.rb
516
- - test/unit/transaction_time_consumed_test.rb
517
- - test/unit/utils/active_record_metric_name_test.rb
518
- - test/unit/utils/backtrace_parser_test.rb
519
- - test/unit/utils/numbers_test.rb
520
- - test/unit/utils/scm.rb
481
+ test_files: []