scout_apm 2.6.9 → 2.6.10

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 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: []