scout_apm 2.6.6 → 4.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +49 -0
  3. data/.rubocop.yml +2 -5
  4. data/.travis.yml +3 -7
  5. data/CHANGELOG.markdown +51 -0
  6. data/Gemfile +1 -8
  7. data/gems/rails6.gemfile +1 -1
  8. data/lib/scout_apm.rb +23 -1
  9. data/lib/scout_apm/agent.rb +22 -0
  10. data/lib/scout_apm/agent_context.rb +14 -2
  11. data/lib/scout_apm/background_job_integrations/delayed_job.rb +1 -1
  12. data/lib/scout_apm/background_job_integrations/faktory.rb +103 -0
  13. data/lib/scout_apm/background_job_integrations/sidekiq.rb +2 -2
  14. data/lib/scout_apm/config.rb +17 -2
  15. data/lib/scout_apm/detailed_trace.rb +2 -1
  16. data/lib/scout_apm/environment.rb +17 -1
  17. data/lib/scout_apm/error.rb +27 -0
  18. data/lib/scout_apm/error_service.rb +32 -0
  19. data/lib/scout_apm/error_service/error_buffer.rb +39 -0
  20. data/lib/scout_apm/error_service/error_record.rb +211 -0
  21. data/lib/scout_apm/error_service/ignored_exceptions.rb +66 -0
  22. data/lib/scout_apm/error_service/middleware.rb +32 -0
  23. data/lib/scout_apm/error_service/notifier.rb +33 -0
  24. data/lib/scout_apm/error_service/payload.rb +47 -0
  25. data/lib/scout_apm/error_service/periodic_work.rb +17 -0
  26. data/lib/scout_apm/error_service/railtie.rb +11 -0
  27. data/lib/scout_apm/error_service/sidekiq.rb +80 -0
  28. data/lib/scout_apm/extensions/transaction_callback_payload.rb +1 -1
  29. data/lib/scout_apm/instrument_manager.rb +1 -0
  30. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +47 -26
  31. data/lib/scout_apm/instruments/action_view.rb +21 -8
  32. data/lib/scout_apm/instruments/active_record.rb +17 -28
  33. data/lib/scout_apm/instruments/typhoeus.rb +88 -0
  34. data/lib/scout_apm/layer.rb +1 -1
  35. data/lib/scout_apm/middleware.rb +1 -1
  36. data/lib/scout_apm/remote/server.rb +13 -1
  37. data/lib/scout_apm/reporter.rb +8 -3
  38. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +6 -2
  39. data/lib/scout_apm/slow_policy/age_policy.rb +33 -0
  40. data/lib/scout_apm/slow_policy/percent_policy.rb +22 -0
  41. data/lib/scout_apm/slow_policy/percentile_policy.rb +24 -0
  42. data/lib/scout_apm/slow_policy/policy.rb +21 -0
  43. data/lib/scout_apm/slow_policy/speed_policy.rb +16 -0
  44. data/lib/scout_apm/slow_request_policy.rb +18 -77
  45. data/lib/scout_apm/tracer.rb +2 -2
  46. data/lib/scout_apm/utils/sql_sanitizer.rb +1 -0
  47. data/lib/scout_apm/utils/sql_sanitizer_regex.rb +3 -3
  48. data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +1 -0
  49. data/lib/scout_apm/version.rb +1 -1
  50. data/scout_apm.gemspec +6 -6
  51. data/test/unit/agent_context_test.rb +29 -0
  52. data/test/unit/environment_test.rb +2 -2
  53. data/test/unit/error_service/error_buffer_test.rb +25 -0
  54. data/test/unit/error_service/ignored_exceptions_test.rb +49 -0
  55. data/test/unit/serializers/payload_serializer_test.rb +36 -0
  56. data/test/unit/slow_request_policy_test.rb +41 -13
  57. data/test/unit/sql_sanitizer_test.rb +38 -0
  58. data/test/unit/tracer_test.rb +25 -0
  59. metadata +27 -61
  60. data/lib/scout_apm/slow_job_policy.rb +0 -111
  61. data/test/unit/slow_job_policy_test.rb +0 -6
@@ -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
 
@@ -30,6 +30,7 @@ module ScoutApm
30
30
  ScoutApm::BackgroundJobIntegrations::Sneakers.new,
31
31
  ScoutApm::BackgroundJobIntegrations::DelayedJob.new,
32
32
  ScoutApm::BackgroundJobIntegrations::Que.new,
33
+ ScoutApm::BackgroundJobIntegrations::Faktory.new,
33
34
  ]
34
35
 
35
36
  FRAMEWORK_INTEGRATIONS = [
@@ -182,9 +183,24 @@ module ScoutApm
182
183
  @ruby_2 = defined?(RUBY_VERSION) && RUBY_VERSION.match(/^2/)
183
184
  end
184
185
 
186
+ def ruby_3?
187
+ return @ruby_3 if defined?(@ruby_3)
188
+ @ruby_3 = defined?(RUBY_VERSION) && RUBY_VERSION.match(/^3/)
189
+ end
190
+
191
+ def ruby_minor
192
+ return @ruby_minor if defined?(@ruby_minor)
193
+ @ruby_minor = defined?(RUBY_VERSION) && RUBY_VERSION.split(".")[1].to_i
194
+ end
195
+
185
196
  # Returns true if this Ruby version supports Module#prepend.
186
197
  def supports_module_prepend?
187
- ruby_2?
198
+ ruby_2? || ruby_3?
199
+ end
200
+
201
+ # Returns true if this Ruby version makes positional and keyword arguments incompatible
202
+ def supports_kwarg_delegation?
203
+ ruby_3? || (ruby_2? && ruby_minor >= 7)
188
204
  end
189
205
 
190
206
  # Returns a string representation of the OS (ex: darwin, linux)
@@ -0,0 +1,27 @@
1
+ # Public API for the Scout Error Monitoring service
2
+ #
3
+ # See-Also ScoutApm::Transaction and ScoutApm::Tracing for APM related APIs
4
+ module ScoutApm
5
+ module Error
6
+ # Capture an exception, optionally with an environment hash. This may be a
7
+ # Rack environment, but is not required.
8
+ def self.capture(exception, env={})
9
+ context = ScoutApm::Agent.instance.context
10
+
11
+ # Skip if error monitoring isn't enabled at all
12
+ if ! context.config.value("errors_enabled")
13
+ return false
14
+ end
15
+
16
+ # Skip if this one error is ignored
17
+ if context.ignored_exceptions.ignored?(exception)
18
+ return false
19
+ end
20
+
21
+ # Capture the error for further processing and shipping
22
+ context.error_buffer.capture(exception, env)
23
+
24
+ return true
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ require "net/http"
2
+ require "net/https"
3
+ require "uri"
4
+
5
+ module ScoutApm
6
+ module ErrorService
7
+ API_VERSION = "1"
8
+
9
+ HEADERS = {
10
+ "Content-type" => "application/json",
11
+ "Accept" => "application/json"
12
+ }
13
+
14
+ # Public API to force a given exception to be captured.
15
+ # Still obeys the ignore list
16
+ # Used internally by SidekiqException
17
+ def self.capture(exception, params = {})
18
+ return if disabled?
19
+ return if ScoutApm::Agent.instance.context.ignored_exceptions.ignore?(exception)
20
+
21
+ context.errors_buffer.capture(exception, env)
22
+ end
23
+
24
+ def self.enabled?
25
+ ScoutApm::Agent.instance.context.config.value("errors_enabled")
26
+ end
27
+
28
+ def self.disabled?
29
+ !enabled?
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,39 @@
1
+ # Holds onto exceptions, and moves them forward to shipping when appropriate
2
+ module ScoutApm
3
+ module ErrorService
4
+ class ErrorBuffer
5
+ include Enumerable
6
+
7
+ attr_reader :agent_context
8
+
9
+ def initialize(agent_context)
10
+ @agent_context = agent_context
11
+ @error_records = []
12
+ @mutex = Monitor.new
13
+ end
14
+
15
+ def capture(exception, env)
16
+ context = ScoutApm::Context.current
17
+
18
+ @mutex.synchronize {
19
+ @error_records << ErrorRecord.new(agent_context, exception, env, context)
20
+ }
21
+ end
22
+
23
+ def get_and_reset_error_records
24
+ @mutex.synchronize {
25
+ ret = @error_records
26
+ @error_records = []
27
+ ret
28
+ }
29
+ end
30
+
31
+ # Enables enumerable - for count and each and similar methods
32
+ def each
33
+ @error_records.each do |error_record|
34
+ yield error_record
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,211 @@
1
+ module ScoutApm
2
+ module ErrorService
3
+ # Converts the raw error data captured into the captured data, and holds it
4
+ # until it's ready to be reported.
5
+ class ErrorRecord
6
+ attr_reader :exception_class
7
+ attr_reader :message
8
+ attr_reader :request_uri
9
+ attr_reader :request_params
10
+ attr_reader :request_session
11
+ attr_reader :environment
12
+ attr_reader :trace
13
+ attr_reader :request_components
14
+ attr_reader :context
15
+
16
+ def initialize(agent_context, exception, env, context=nil)
17
+ @agent_context = agent_context
18
+
19
+ @context = if context
20
+ context.to_hash
21
+ else
22
+ {}
23
+ end
24
+
25
+ @exception_class = LengthLimit.new(exception.class.name).to_s
26
+ @message = LengthLimit.new(exception.message, 100).to_s
27
+ @request_uri = LengthLimit.new(rack_request_url(env), 200).to_s
28
+ @request_params = clean_params(env["action_dispatch.request.parameters"])
29
+ @request_session = clean_params(session_data(env))
30
+ @environment = clean_params(strip_env(env))
31
+ @trace = clean_backtrace(exception.backtrace)
32
+ @request_components = components(env)
33
+ end
34
+
35
+ # TODO: This is rails specific
36
+ def components(env)
37
+ components = {}
38
+ unless env["action_dispatch.request.parameters"].nil?
39
+ components[:controller] = env["action_dispatch.request.parameters"][:controller] || nil
40
+ components[:action] = env["action_dispatch.request.parameters"][:action] || nil
41
+ components[:module] = env["action_dispatch.request.parameters"][:module] || nil
42
+ end
43
+
44
+ # For background workers like sidekiq
45
+ # TODO: extract data creation for background jobs
46
+ components[:controller] ||= env[:custom_controller]
47
+
48
+ components
49
+ end
50
+
51
+ # TODO: Can I use the same thing we use in traces?
52
+ def rack_request_url(env)
53
+ protocol = rack_scheme(env)
54
+ protocol = protocol.nil? ? "" : "#{protocol}://"
55
+
56
+ host = env["SERVER_NAME"] || ""
57
+ path = env["REQUEST_URI"] || ""
58
+ port = env["SERVER_PORT"] || "80"
59
+ port = ["80", "443"].include?(port.to_s) ? "" : ":#{port}"
60
+
61
+ protocol.to_s + host.to_s + port.to_s + path.to_s
62
+ end
63
+
64
+ def rack_scheme(env)
65
+ if env["HTTPS"] == "on"
66
+ "https"
67
+ elsif env["HTTP_X_FORWARDED_PROTO"]
68
+ env["HTTP_X_FORWARDED_PROTO"].split(",")[0]
69
+ else
70
+ env["rack.url_scheme"]
71
+ end
72
+ end
73
+
74
+ # TODO: This name is too vague
75
+ def clean_params(params)
76
+ return if params.nil?
77
+
78
+ normalized = normalize_data(params)
79
+ filter_params(normalized)
80
+ end
81
+
82
+ # TODO: When was backtrace_cleaner introduced?
83
+ def clean_backtrace(backtrace)
84
+ if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner)
85
+ Rails.backtrace_cleaner.send(:filter, backtrace)
86
+ else
87
+ backtrace
88
+ end
89
+ end
90
+
91
+ # Deletes params from env
92
+ #
93
+ # These are not configurable, and will leak PII info up to Scout if
94
+ # allowed through. Things like specific parameters can be exposed with
95
+ # the ScoutApm::Context interface.
96
+ KEYS_TO_REMOVE = [
97
+ "rack.request.form_hash",
98
+ "rack.request.form_vars",
99
+ "async.callback",
100
+
101
+ # Security related items
102
+ "action_dispatch.secret_key_base",
103
+ "action_dispatch.http_auth_salt",
104
+ "action_dispatch.signed_cookie_salt",
105
+ "action_dispatch.encrypted_cookie_salt",
106
+ "action_dispatch.encrypted_signed_cookie_salt",
107
+ "action_dispatch.authenticated_encrypted_cookie_salt",
108
+
109
+ # Raw data from the URL & parameters. Would bypass our normal params filtering
110
+ "QUERY_STRING",
111
+ "REQUEST_URI",
112
+ "REQUEST_PATH",
113
+ "ORIGINAL_FULLPATH",
114
+ "action_dispatch.request.query_parameters",
115
+ "action_dispatch.request.parameters",
116
+ "rack.request.query_string",
117
+ "rack.request.query_hash",
118
+ ]
119
+ def strip_env(env)
120
+ env.reject { |k, v| KEYS_TO_REMOVE.include?(k) }
121
+ end
122
+
123
+ def session_data(env)
124
+ session = env["action_dispatch.request.session"]
125
+ return if session.nil?
126
+
127
+ if session.respond_to?(:to_hash)
128
+ session.to_hash
129
+ else
130
+ session.data
131
+ end
132
+ end
133
+
134
+ # TODO: Rename and make this clearer. I think it maps over the whole tree of a hash, and to_s each leaf node?
135
+ def normalize_data(hash)
136
+ new_hash = {}
137
+
138
+ hash.each do |key, value|
139
+ if value.respond_to?(:to_hash)
140
+ begin
141
+ new_hash[key] = normalize_data(value.to_hash)
142
+ rescue
143
+ new_hash[key] = LengthLimit.new(value.to_s).to_s
144
+ end
145
+ else
146
+ new_hash[key] = LengthLimit.new(value.to_s).to_s
147
+ end
148
+ end
149
+
150
+ new_hash
151
+ end
152
+
153
+ ###################
154
+ # Filtering Params
155
+ ###################
156
+
157
+ # Replaces parameter values with a string / set in config file
158
+ def filter_params(params)
159
+ return params unless filtered_params_config
160
+
161
+ params.each do |k, v|
162
+ if filter_key?(k)
163
+ params[k] = "[FILTERED]"
164
+ elsif v.respond_to?(:to_hash)
165
+ filter_params(params[k])
166
+ end
167
+ end
168
+
169
+ params
170
+ end
171
+
172
+ # Check, if a key should be filtered
173
+ def filter_key?(key)
174
+ params_to_filter.any? do |filter|
175
+ key.to_s == filter.to_s # key.to_s.include?(filter.to_s)
176
+ end
177
+ end
178
+
179
+ def params_to_filter
180
+ @params_to_filter ||= filtered_params_config + rails_filtered_params
181
+ end
182
+
183
+ # Accessor for the filtered params config value. Will be removed as we refactor and clean up this code.
184
+ # TODO: Flip this over to use a new class like filtered exceptions?
185
+ def filtered_params_config
186
+ @agent_context.config.value("errors_filtered_params")
187
+ end
188
+
189
+ def rails_filtered_params
190
+ return [] unless defined?(Rails)
191
+ Rails.configuration.filter_parameters
192
+ rescue
193
+ []
194
+ end
195
+
196
+ class LengthLimit
197
+ attr_reader :text
198
+ attr_reader :char_limit
199
+
200
+ def initialize(text, char_limit=100)
201
+ @text = text
202
+ @char_limit = char_limit
203
+ end
204
+
205
+ def to_s
206
+ text[0..char_limit]
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,66 @@
1
+ # Encapsulates the management and checking of ignored exceptions. Allows using
2
+ # string matches on the class name, or arbitrary matching with a callback
3
+ module ScoutApm
4
+ module ErrorService
5
+ class IgnoredExceptions
6
+ attr_reader :ignored_exceptions
7
+ attr_reader :blocks
8
+
9
+ def initialize(context, from_config)
10
+ @context = context
11
+ @ignored_exceptions = Array(from_config).map{ |e| normalize_as_klass(e) }
12
+ @blocks = []
13
+ end
14
+
15
+ # Add a single ignored exception by class name
16
+ def add(klass_or_str)
17
+ @ignored_exceptions << normalize_as_klass(klass_or_str)
18
+ end
19
+
20
+ # Add a callback block that will be called on every error. If it returns
21
+ # Signature of blocks: ->(exception object): truthy or falsy value
22
+ def add_callback(&block)
23
+ @blocks << block
24
+ end
25
+
26
+ def ignored?(exception_object)
27
+ klass = normalize_as_klass(exception_object)
28
+
29
+ # Check if we ignored this error by name (typical way to ignore)
30
+ if ignored_exceptions.any? { |ignored| klass.ancestors.include?(ignored) }
31
+ return true
32
+ end
33
+
34
+ # For each block, see if it says we should ignore this error
35
+ blocks.each do |b|
36
+ if b.call(exception_object)
37
+ return true
38
+ end
39
+ end
40
+
41
+ false
42
+ end
43
+
44
+ private
45
+
46
+ def normalize_as_klass(klass_or_str)
47
+ if Module === klass_or_str
48
+ return klass_or_str
49
+ end
50
+
51
+ if klass_or_str.is_a?(Exception)
52
+ return klass_or_str.class
53
+ end
54
+
55
+ if String === klass_or_str
56
+ maybe = ScoutApm::Utils::KlassHelper.lookup(klass_or_str)
57
+ if Module === maybe
58
+ return maybe
59
+ end
60
+ end
61
+
62
+ klass_or_str
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,32 @@
1
+ module ScoutApm
2
+ module ErrorService
3
+ class Middleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ begin
10
+ response = @app.call(env)
11
+ rescue Exception => exception
12
+ puts "[Scout Error Service] Caught Exception: #{exception.class.name}"
13
+
14
+ context = ScoutApm::Agent.instance.context
15
+
16
+ # Bail out early, and reraise if the error is not interesting.
17
+ if context.ignored_exceptions.ignored?(exception)
18
+ raise
19
+ end
20
+
21
+ # Capture the error for further processing and shipping
22
+ context.error_buffer.capture(exception, env)
23
+
24
+ # Finally re-raise
25
+ raise
26
+ end
27
+
28
+ response
29
+ end
30
+ end
31
+ end
32
+ end