scout_apm 2.5.1 → 5.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (156) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +68 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +5 -5
  5. data/CHANGELOG.markdown +176 -3
  6. data/Gemfile +1 -7
  7. data/LICENSE.md +21 -28
  8. data/gems/README.md +28 -0
  9. data/gems/instruments.gemfile +6 -0
  10. data/gems/octoshark.gemfile +4 -0
  11. data/gems/rails3.gemfile +5 -0
  12. data/gems/rails4.gemfile +4 -0
  13. data/gems/rails5.gemfile +4 -0
  14. data/gems/rails6.gemfile +4 -0
  15. data/gems/sidekiq.gemfile +4 -0
  16. data/gems/typhoeus.gemfile +3 -0
  17. data/lib/scout_apm/agent/preconditions.rb +3 -3
  18. data/lib/scout_apm/agent.rb +22 -0
  19. data/lib/scout_apm/agent_context.rb +21 -2
  20. data/lib/scout_apm/app_server_load.rb +7 -2
  21. data/lib/scout_apm/auto_instrument/instruction_sequence.rb +31 -0
  22. data/lib/scout_apm/auto_instrument/layer.rb +23 -0
  23. data/lib/scout_apm/auto_instrument/parser.rb +27 -0
  24. data/lib/scout_apm/auto_instrument/rails.rb +174 -0
  25. data/lib/scout_apm/auto_instrument.rb +5 -0
  26. data/lib/scout_apm/background_job_integrations/delayed_job.rb +1 -1
  27. data/lib/scout_apm/background_job_integrations/faktory.rb +103 -0
  28. data/lib/scout_apm/background_job_integrations/legacy_sneakers.rb +55 -0
  29. data/lib/scout_apm/background_job_integrations/que.rb +134 -0
  30. data/lib/scout_apm/background_job_integrations/shoryuken.rb +2 -0
  31. data/lib/scout_apm/background_job_integrations/sidekiq.rb +15 -10
  32. data/lib/scout_apm/background_job_integrations/sneakers.rb +11 -11
  33. data/lib/scout_apm/config.rb +54 -6
  34. data/lib/scout_apm/detailed_trace.rb +3 -2
  35. data/lib/scout_apm/environment.rb +18 -1
  36. data/lib/scout_apm/error.rb +27 -0
  37. data/lib/scout_apm/error_service/error_buffer.rb +39 -0
  38. data/lib/scout_apm/error_service/error_record.rb +211 -0
  39. data/lib/scout_apm/error_service/ignored_exceptions.rb +66 -0
  40. data/lib/scout_apm/error_service/middleware.rb +32 -0
  41. data/lib/scout_apm/error_service/notifier.rb +33 -0
  42. data/lib/scout_apm/error_service/payload.rb +47 -0
  43. data/lib/scout_apm/error_service/periodic_work.rb +17 -0
  44. data/lib/scout_apm/error_service/railtie.rb +11 -0
  45. data/lib/scout_apm/error_service/sidekiq.rb +80 -0
  46. data/lib/scout_apm/error_service.rb +34 -0
  47. data/lib/scout_apm/exceptions.rb +12 -0
  48. data/lib/scout_apm/extensions/transaction_callback_payload.rb +1 -1
  49. data/lib/scout_apm/external_service_metric_set.rb +97 -0
  50. data/lib/scout_apm/external_service_metric_stats.rb +85 -0
  51. data/lib/scout_apm/fake_store.rb +3 -0
  52. data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +7 -2
  53. data/lib/scout_apm/git_revision.rb +9 -0
  54. data/lib/scout_apm/ignored_uris.rb +3 -1
  55. data/lib/scout_apm/instant/middleware.rb +4 -1
  56. data/lib/scout_apm/instrument_manager.rb +22 -1
  57. data/lib/scout_apm/instruments/action_controller_rails_2.rb +1 -1
  58. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +53 -29
  59. data/lib/scout_apm/instruments/action_view.rb +30 -9
  60. data/lib/scout_apm/instruments/active_record.rb +69 -19
  61. data/lib/scout_apm/instruments/elasticsearch.rb +93 -42
  62. data/lib/scout_apm/instruments/grape.rb +1 -1
  63. data/lib/scout_apm/instruments/http.rb +68 -0
  64. data/lib/scout_apm/instruments/http_client.rb +33 -14
  65. data/lib/scout_apm/instruments/influxdb.rb +2 -2
  66. data/lib/scout_apm/instruments/memcached.rb +58 -0
  67. data/lib/scout_apm/instruments/middleware_detailed.rb +1 -1
  68. data/lib/scout_apm/instruments/middleware_summary.rb +1 -1
  69. data/lib/scout_apm/instruments/mongoid.rb +10 -5
  70. data/lib/scout_apm/instruments/moped.rb +44 -19
  71. data/lib/scout_apm/instruments/net_http.rb +51 -16
  72. data/lib/scout_apm/instruments/rails_router.rb +1 -1
  73. data/lib/scout_apm/instruments/redis.rb +27 -12
  74. data/lib/scout_apm/instruments/redis5.rb +59 -0
  75. data/lib/scout_apm/instruments/sinatra.rb +3 -1
  76. data/lib/scout_apm/instruments/typhoeus.rb +90 -0
  77. data/lib/scout_apm/job_record.rb +4 -2
  78. data/lib/scout_apm/layaway_file.rb +4 -0
  79. data/lib/scout_apm/layer.rb +5 -2
  80. data/lib/scout_apm/layer_children_set.rb +9 -8
  81. data/lib/scout_apm/layer_converters/external_service_converter.rb +65 -0
  82. data/lib/scout_apm/layer_converters/find_layer_by_type.rb +4 -0
  83. data/lib/scout_apm/layer_converters/request_queue_time_converter.rb +2 -0
  84. data/lib/scout_apm/layer_converters/trace_converter.rb +7 -4
  85. data/lib/scout_apm/logger.rb +5 -1
  86. data/lib/scout_apm/middleware.rb +1 -1
  87. data/lib/scout_apm/periodic_work.rb +19 -0
  88. data/lib/scout_apm/remote/message.rb +4 -0
  89. data/lib/scout_apm/remote/server.rb +13 -1
  90. data/lib/scout_apm/reporter.rb +8 -3
  91. data/lib/scout_apm/reporting.rb +2 -1
  92. data/lib/scout_apm/request_histograms.rb +8 -0
  93. data/lib/scout_apm/serializers/app_server_load_serializer.rb +4 -0
  94. data/lib/scout_apm/serializers/directive_serializer.rb +4 -0
  95. data/lib/scout_apm/serializers/external_service_serializer_to_json.rb +15 -0
  96. data/lib/scout_apm/serializers/payload_serializer.rb +4 -3
  97. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +10 -3
  98. data/lib/scout_apm/slow_policy/age_policy.rb +33 -0
  99. data/lib/scout_apm/slow_policy/percent_policy.rb +22 -0
  100. data/lib/scout_apm/slow_policy/percentile_policy.rb +24 -0
  101. data/lib/scout_apm/slow_policy/policy.rb +21 -0
  102. data/lib/scout_apm/slow_policy/speed_policy.rb +16 -0
  103. data/lib/scout_apm/slow_request_policy.rb +18 -77
  104. data/lib/scout_apm/store.rb +31 -1
  105. data/lib/scout_apm/tracer.rb +2 -2
  106. data/lib/scout_apm/tracked_request.rb +35 -4
  107. data/lib/scout_apm/utils/backtrace_parser.rb +3 -0
  108. data/lib/scout_apm/utils/marshal_logging.rb +90 -0
  109. data/lib/scout_apm/utils/sql_sanitizer.rb +47 -7
  110. data/lib/scout_apm/version.rb +1 -1
  111. data/lib/scout_apm.rb +46 -1
  112. data/scout_apm.gemspec +14 -9
  113. data/test/test_helper.rb +2 -2
  114. data/test/tmp/README.md +17 -0
  115. data/test/unit/agent_context_test.rb +29 -0
  116. data/test/unit/auto_instrument/anonymous_block_value.rb +7 -0
  117. data/test/unit/auto_instrument/assignments-instrumented.rb +31 -0
  118. data/test/unit/auto_instrument/assignments.rb +31 -0
  119. data/test/unit/auto_instrument/controller-ast.txt +57 -0
  120. data/test/unit/auto_instrument/controller-instrumented.rb +49 -0
  121. data/test/unit/auto_instrument/controller.rb +49 -0
  122. data/test/unit/auto_instrument/hanging_method.rb +6 -0
  123. data/test/unit/auto_instrument/rescue_from-instrumented.rb +13 -0
  124. data/test/unit/auto_instrument/rescue_from.rb +13 -0
  125. data/test/unit/auto_instrument_test.rb +62 -0
  126. data/test/unit/background_job_integrations/sidekiq_test.rb +17 -0
  127. data/test/unit/environment_test.rb +2 -2
  128. data/test/unit/error_service/error_buffer_test.rb +25 -0
  129. data/test/unit/error_service/ignored_exceptions_test.rb +49 -0
  130. data/test/unit/external_service_metric_set_test.rb +67 -0
  131. data/test/unit/external_service_metric_stats_test.rb +106 -0
  132. data/test/unit/ignored_uris_test.rb +6 -0
  133. data/test/unit/instruments/active_record_test.rb +40 -0
  134. data/test/unit/instruments/http_client_test.rb +24 -0
  135. data/test/unit/instruments/http_test.rb +24 -0
  136. data/test/unit/instruments/moped_test.rb +24 -0
  137. data/test/unit/instruments/net_http_test.rb +11 -1
  138. data/test/unit/instruments/redis_test.rb +24 -0
  139. data/test/unit/instruments/typhoeus_test.rb +42 -0
  140. data/test/unit/layer_children_set_test.rb +9 -0
  141. data/test/unit/remote/{test_message.rb → message_test.rb} +0 -0
  142. data/test/unit/remote/{test_router.rb → route_test.rb} +0 -0
  143. data/test/unit/remote/{test_server.rb → server_test.rb} +4 -1
  144. data/test/unit/request_histograms_test.rb +17 -0
  145. data/test/unit/serializers/payload_serializer_test.rb +39 -3
  146. data/test/unit/slow_request_policy_test.rb +41 -13
  147. data/test/unit/sql_sanitizer_test.rb +106 -0
  148. data/test/unit/tracer_test.rb +25 -0
  149. metadata +118 -60
  150. data/.travis.yml +0 -25
  151. data/lib/scout_apm/instruments/.DS_Store +0 -0
  152. data/lib/scout_apm/slow_job_policy.rb +0 -111
  153. data/lib/scout_apm/utils/sql_sanitizer_regex.rb +0 -25
  154. data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +0 -26
  155. data/test/unit/instruments/active_record_instruments_test.rb +0 -5
  156. data/test/unit/slow_job_policy_test.rb +0 -6
@@ -29,10 +29,19 @@ require 'scout_apm/environment'
29
29
  # report_format - 'json' or 'marshal'. Marshal is legacy and will be removed.
30
30
  # scm_subdirectory - if the app root lives in source management in a subdirectory. E.g. #{SCM_ROOT}/src
31
31
  # uri_reporting - 'path' or 'full_path' default is 'full_path', which reports URL params as well as the path.
32
+ # record_queue_time - true/false to enable recording of queuetime.
32
33
  # remote_agent_host - Internal: What host to bind to, and also send messages to for remote. Default: 127.0.0.1.
33
34
  # remote_agent_port - What port to bind the remote webserver to
34
35
  # start_resque_server_instrument - Used in special situations with certain Resque installs
35
36
  # timeline_traces - true/false to enable sending of of the timeline trace format.
37
+ # auto_instruments - true/false whether to install autoinstruments. Only installed if on a supported Ruby version.
38
+ # auto_instruments_ignore - An array of file names to exclude from autoinstruments (Ex: ['application_controller']).
39
+ # use_prepend - Whether to apply instrumentation using Module#Prepend instead
40
+ # of Module#alias_method (Default: false)
41
+ # alias_method_instruments - If `use_prepend` is true, continue to use Module#alias_method for
42
+ # any instruments listed in this array. Default: []
43
+ # prepend_instruments - If `use_prepend` is false, force using Module#prepend for any
44
+ # instruments listed in this array. Default: []
36
45
  #
37
46
  # Any of these config settings can be set with an environment variable prefixed
38
47
  # by SCOUT_ and uppercasing the key: SCOUT_LOG_LEVEL for instance.
@@ -53,6 +62,8 @@ module ScoutApm
53
62
  'direct_host',
54
63
  'disabled_instruments',
55
64
  'enable_background_jobs',
65
+ 'external_service_metric_limit',
66
+ 'external_service_metric_report_limit',
56
67
  'host',
57
68
  'hostname',
58
69
  'ignore',
@@ -67,15 +78,28 @@ module ScoutApm
67
78
  'name',
68
79
  'profile',
69
80
  'proxy',
81
+ 'record_queue_time',
70
82
  'remote_agent_host',
71
83
  'remote_agent_port',
72
84
  'report_format',
73
85
  'revision_sha',
74
86
  'scm_subdirectory',
75
87
  'start_resque_server_instrument',
88
+ 'ssl_cert_file',
76
89
  'uri_reporting',
77
90
  'instrument_http_url_length',
78
- 'timeline_traces'
91
+ 'timeline_traces',
92
+ 'auto_instruments',
93
+ 'auto_instruments_ignore',
94
+ 'use_prepend',
95
+ 'alias_method_instruments',
96
+ 'prepend_instruments',
97
+
98
+ # Error Service Related Configuration
99
+ 'errors_enabled',
100
+ 'errors_ignored_exceptions',
101
+ 'errors_filtered_params',
102
+ 'errors_host',
79
103
  ]
80
104
 
81
105
  ################################################################################
@@ -166,9 +190,20 @@ module ScoutApm
166
190
  'compress_payload' => BooleanCoercion.new,
167
191
  'database_metric_limit' => IntegerCoercion.new,
168
192
  'database_metric_report_limit' => IntegerCoercion.new,
193
+ 'external_service_metric_limit' => IntegerCoercion.new,
194
+ 'external_service_metric_report_limit' => IntegerCoercion.new,
169
195
  'instrument_http_url_length' => IntegerCoercion.new,
196
+ 'record_queue_time' => BooleanCoercion.new,
170
197
  'start_resque_server_instrument' => BooleanCoercion.new,
171
- 'timeline_traces' => BooleanCoercion.new
198
+ 'timeline_traces' => BooleanCoercion.new,
199
+ 'auto_instruments' => BooleanCoercion.new,
200
+ 'auto_instruments_ignore' => JsonCoercion.new,
201
+ 'use_prepend' => BooleanCoercion.new,
202
+ 'alias_method_instruments' => JsonCoercion.new,
203
+ 'prepend_instruments' => JsonCoercion.new,
204
+ 'errors_enabled' => BooleanCoercion.new,
205
+ 'errors_ignored_exceptions' => JsonCoercion.new,
206
+ 'errors_filtered_params' => JsonCoercion.new,
172
207
  }
173
208
 
174
209
 
@@ -273,10 +308,23 @@ module ScoutApm
273
308
  'remote_agent_port' => 7721, # picked at random
274
309
  'database_metric_limit' => 5000, # The hard limit on db metrics
275
310
  'database_metric_report_limit' => 1000,
311
+ 'external_service_metric_limit' => 5000, # The hard limit on external service metrics
312
+ 'external_service_metric_report_limit' => 1000,
276
313
  'instrument_http_url_length' => 300,
277
314
  'start_resque_server_instrument' => true, # still only starts if Resque is detected
278
315
  'collect_remote_ip' => true,
279
- 'timeline_traces' => true
316
+ 'record_queue_time' => true,
317
+ 'timeline_traces' => true,
318
+ 'auto_instruments' => false,
319
+ 'auto_instruments_ignore' => [],
320
+ 'use_prepend' => false,
321
+ 'alias_method_instruments' => [],
322
+ 'prepend_instruments' => [],
323
+ 'ssl_cert_file' => File.join( File.dirname(__FILE__), *%w[.. .. data cacert.pem] ),
324
+ 'errors_enabled' => false,
325
+ 'errors_ignored_exceptions' => %w(ActiveRecord::RecordNotFound ActionController::RoutingError),
326
+ 'errors_filtered_params' => %w(password s3-key),
327
+ 'errors_host' => 'https://errors.scoutapm.com',
280
328
  }.freeze
281
329
 
282
330
  def value(key)
@@ -399,7 +447,7 @@ module ScoutApm
399
447
  begin
400
448
  raw_file = File.read(@resolved_file_path)
401
449
  erb_file = ERB.new(raw_file).result(binding)
402
- parsed_yaml = YAML.load(erb_file)
450
+ parsed_yaml = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(erb_file) : YAML.load(erb_file)
403
451
  file_settings = parsed_yaml[app_environment]
404
452
 
405
453
  if file_settings.is_a? Hash
@@ -410,8 +458,8 @@ module ScoutApm
410
458
  logger.info("Couldn't find configuration in #{@resolved_file_path} for environment: #{app_environment}. Configuration in ENV will still be applied.")
411
459
  @file_loaded = false
412
460
  end
413
- rescue Exception => e # Explicit `Exception` handling to catch SyntaxError and anything else that ERB or YAML may throw
414
- logger.info("Failed loading configuration file (#{@resolved_file_path}): #{e.message}. ScoutAPM will continue starting with configuration from ENV and defaults")
461
+ rescue ScoutApm::AllExceptionsExceptOnesWeMustNotRescue => e # Everything except the most important exceptions we should never interfere with
462
+ logger.info("Failed loading configuration file (#{@resolved_file_path}): ScoutAPM will continue starting with configuration from ENV and defaults. Exception was #{e.class}: #{e.message}#{e.backtrace.map { |bt| "\n #{bt}" }.join('')}")
415
463
  @file_loaded = false
416
464
  end
417
465
  end
@@ -98,7 +98,7 @@ class DetailedTrace
98
98
  }
99
99
  },
100
100
  :tags => tags.as_json,
101
- :spans => spans.as_json,
101
+ :spans => spans.map{|span| span.as_json},
102
102
  }
103
103
  end
104
104
 
@@ -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
 
@@ -29,6 +29,8 @@ module ScoutApm
29
29
  ScoutApm::BackgroundJobIntegrations::Shoryuken.new,
30
30
  ScoutApm::BackgroundJobIntegrations::Sneakers.new,
31
31
  ScoutApm::BackgroundJobIntegrations::DelayedJob.new,
32
+ ScoutApm::BackgroundJobIntegrations::Que.new,
33
+ ScoutApm::BackgroundJobIntegrations::Faktory.new,
32
34
  ]
33
35
 
34
36
  FRAMEWORK_INTEGRATIONS = [
@@ -181,9 +183,24 @@ module ScoutApm
181
183
  @ruby_2 = defined?(RUBY_VERSION) && RUBY_VERSION.match(/^2/)
182
184
  end
183
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
+
184
196
  # Returns true if this Ruby version supports Module#prepend.
185
197
  def supports_module_prepend?
186
- 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)
187
204
  end
188
205
 
189
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,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
+ context = ScoutApm::Agent.instance.context
13
+
14
+ context.logger.debug "[Scout Error Service] Caught Exception: #{exception.class.name}"
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.debug(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