scout_apm 2.6.5 → 2.6.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +28 -0
  3. data/Gemfile +4 -0
  4. data/lib/scout_apm.rb +15 -0
  5. data/lib/scout_apm/agent.rb +22 -0
  6. data/lib/scout_apm/agent_context.rb +12 -0
  7. data/lib/scout_apm/background_job_integrations/sidekiq.rb +2 -2
  8. data/lib/scout_apm/config.rb +17 -2
  9. data/lib/scout_apm/detailed_trace.rb +2 -1
  10. data/lib/scout_apm/error.rb +27 -0
  11. data/lib/scout_apm/error_service.rb +32 -0
  12. data/lib/scout_apm/error_service/error_buffer.rb +39 -0
  13. data/lib/scout_apm/error_service/error_record.rb +211 -0
  14. data/lib/scout_apm/error_service/ignored_exceptions.rb +66 -0
  15. data/lib/scout_apm/error_service/middleware.rb +32 -0
  16. data/lib/scout_apm/error_service/notifier.rb +33 -0
  17. data/lib/scout_apm/error_service/payload.rb +47 -0
  18. data/lib/scout_apm/error_service/periodic_work.rb +17 -0
  19. data/lib/scout_apm/error_service/railtie.rb +11 -0
  20. data/lib/scout_apm/error_service/sidekiq.rb +80 -0
  21. data/lib/scout_apm/extensions/transaction_callback_payload.rb +1 -1
  22. data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +1 -0
  23. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +47 -26
  24. data/lib/scout_apm/instruments/action_view.rb +7 -2
  25. data/lib/scout_apm/middleware.rb +1 -1
  26. data/lib/scout_apm/reporter.rb +8 -3
  27. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +28 -10
  28. data/lib/scout_apm/utils/sql_sanitizer.rb +10 -1
  29. data/lib/scout_apm/utils/sql_sanitizer_regex.rb +7 -0
  30. data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +6 -0
  31. data/lib/scout_apm/version.rb +1 -1
  32. data/test/unit/agent_context_test.rb +15 -0
  33. data/test/unit/error_service/error_buffer_test.rb +25 -0
  34. data/test/unit/error_service/ignored_exceptions_test.rb +49 -0
  35. data/test/unit/serializers/payload_serializer_test.rb +36 -0
  36. data/test/unit/sql_sanitizer_test.rb +47 -0
  37. metadata +18 -58
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41f15f4e7f91d0274a280b34d03697ff422963a39bcadc9f9056f6380b949603
4
- data.tar.gz: c76090db49ba3f24ea685ccf61c0fbe17555affe556d947990cb584bb8f56bb5
3
+ metadata.gz: 503d33cb7c689cb239c328bee4ba2a1df380df703f3e3a6f5f843676b9bff0e4
4
+ data.tar.gz: 444e240dc8c4922ee932684c1c10b3fb5d658e96228c901b33a9a5ca3cb1e469
5
5
  SHA512:
6
- metadata.gz: b33379d021bbb49e1c2c60ca0c1a72f4603539142830b92f3846e87fdf9c9d2244e73dd0be5a84fc567945ca028b62450f48dbfd6fdc0fcab81eb6d44f82bab9
7
- data.tar.gz: 35183553754b6a56397ff10579f014abf168c1b3ef83c22318c691ba6ab2798d6a4264e7d17270b318fe43c0ea5a61ab75c025b974354b10f4f5b1395ad77bb5
6
+ metadata.gz: 715c6a3f044d2091207667fc6a9857573da528cf03ee5d64ccb758c10ec387b49829a891ae43bb51b444077c898f57fc73257f2d963407820ed7114fdedb52c4
7
+ data.tar.gz: 373b60dc42e0417e6003698c143138ff50024802285ff3754cc706efc1bc305a941fddbe27e6a471d8f09b3ffc84b5a3cfb43e57a1715968d7cac47ee069f5b3
@@ -1,3 +1,31 @@
1
+ # 2.6.10
2
+
3
+ * Fix an edge case in JSON serialization (#360)
4
+
5
+ # 2.6.9
6
+
7
+ * Add `ssl_cert_file` config option (#352)
8
+ * Improve sanitization of Postgres UPDATE SQL (#351)
9
+ * Allow custom URL sanitization (#341)
10
+
11
+ # 2.6.8
12
+
13
+ * Lock rake version for 1.8.7 to older version (#329)
14
+ * Delete unneeded .DS_Store file that snuck in (#334)
15
+ * Fix typo in "queue_time_ms"
16
+ * Fix Rails 6 deprecation warning at boot time (#337)
17
+ * Fix partial naming on Rails 6.0 (#339)
18
+ * Support Sidekiq 6.1 instrumentation (#340)
19
+
20
+ # 2.6.7
21
+
22
+ * Remove accidental call to `as_json`
23
+
24
+ # 2.6.6
25
+
26
+ * Add basic support for parsing Microsoft SQLServer queries (#317)
27
+ * Refine Postgresql Sanitization with subqueries and JSON operations (#262)
28
+
1
29
  # 2.6.5
2
30
 
3
31
  * Add a tag to any requests that reach maximum number of spans (#316)
data/Gemfile CHANGED
@@ -10,4 +10,8 @@ if RUBY_VERSION <= "1.8.7"
10
10
  gem "pry", "~> 0.9.12"
11
11
  gem "rake", "~> 10.5"
12
12
  gem "minitest", "~> 5.11.3"
13
+ elsif RUBY_VERSION <= "1.9.3"
14
+ gem "rake", "~> 10.5"
15
+ else
16
+ gem "rake", ">= 12.3.3"
13
17
  end
@@ -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
  #############
@@ -40,10 +40,10 @@ module ScoutApm
40
40
  require 'sidekiq/processor' # sidekiq v4 has not loaded this file by this point
41
41
 
42
42
  ::Sidekiq::Processor.class_eval do
43
- def initialize_with_scout(boss)
43
+ def initialize_with_scout(*args)
44
44
  agent = ::ScoutApm::Agent.instance
45
45
  agent.start
46
- initialize_without_scout(boss)
46
+ initialize_without_scout(*args)
47
47
  end
48
48
 
49
49
  alias_method :initialize_without_scout, :initialize
@@ -75,11 +75,18 @@ module ScoutApm
75
75
  'revision_sha',
76
76
  'scm_subdirectory',
77
77
  'start_resque_server_instrument',
78
+ 'ssl_cert_file',
78
79
  'uri_reporting',
79
80
  'instrument_http_url_length',
80
81
  'timeline_traces',
81
82
  'auto_instruments',
82
- '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',
83
90
  ]
84
91
 
85
92
  ################################################################################
@@ -175,6 +182,9 @@ module ScoutApm
175
182
  'timeline_traces' => BooleanCoercion.new,
176
183
  'auto_instruments' => BooleanCoercion.new,
177
184
  'auto_instruments_ignore' => JsonCoercion.new,
185
+ 'errors_enabled' => BooleanCoercion.new,
186
+ 'errors_ignored_exceptions' => JsonCoercion.new,
187
+ 'errors_filtered_params' => JsonCoercion.new,
178
188
  }
179
189
 
180
190
 
@@ -284,7 +294,12 @@ module ScoutApm
284
294
  'collect_remote_ip' => true,
285
295
  'timeline_traces' => true,
286
296
  'auto_instruments' => false,
287
- 'auto_instruments_ignore' => []
297
+ 'auto_instruments_ignore' => [],
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',
288
303
  }.freeze
289
304
 
290
305
  def value(key)
@@ -200,8 +200,9 @@ class DetailedTraceTags
200
200
  @tags = hash
201
201
  end
202
202
 
203
+ # @tags is already a hash, so no conversion needed
203
204
  def as_json(*)
204
- @tags.as_json
205
+ @tags
205
206
  end
206
207
  end
207
208
 
@@ -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