scout_apm 5.7.1 → 5.8.0

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: 2e96ebe1b7d9a1fb4bd1427ba0564e645cb9bed376da56bc92f9d86f0515adf7
4
- data.tar.gz: d1c4692af9e37bb6107f14064ce89f94626d987bf55d845dea19b522fa910ce1
3
+ metadata.gz: 80d2cac0818a8487fe1e9ca2395faccbbbadf2be368151e636e39dd7188b34ea
4
+ data.tar.gz: 1c46ee7b70ac1bd2e9ff4ed4232c7304edc02088563df840619aedcb5e43384d
5
5
  SHA512:
6
- metadata.gz: 958530e614d6ad7ac0ef8805d40843883fae446d5dbfdee7f982a504eff838e0f6abe9fc70e7ead42f0e3a7075540291954264704da73694f132674ef2edc48b
7
- data.tar.gz: d35f05a9f2c6094b87e1df04a95fa54138c901b25b479ce5962c616d54806fea1a25ab5e89feb5d98c0ecf0d5e13163e97f990c3e74229544751f3b6e0d4fe9f
6
+ metadata.gz: da2b3e51bea0eb7198d76edd997ef62da6719b3e0df7bd7093f56cbd51d2f0426f6798ef57d641e786a3a16732ad6e9ad141ce977fce0d9888541ff72bfbb11d
7
+ data.tar.gz: 42e1bb5874b213da9c8be9b4024263737bf8c34ae7c968b2259939f87bb3f983a105036d6cd3222de02964b8f40aedab8f5e47f10248ed5053c517acddf67833
data/CHANGELOG.markdown CHANGED
@@ -1,4 +1,12 @@
1
1
  # Unreleased
2
+ - Enable Delayed Job error tracking (#565)
3
+
4
+ # 5.8.0
5
+ - Add error monitoring to SolidQueue, faktory, goodjob, que, shoryuken, sneakers (#571)
6
+ - Treat exclusive time as total time for limited layers in background jobs (#576)
7
+ - Remove unused time util conflict with DeviseTokenAuth (#575)
8
+ - Add transaction ID to error records (#568)-
9
+ - Adds request method to captured error context (#577)
2
10
 
3
11
  # 5.7.1
4
12
  - Update error capture API to use context (#560)
@@ -4,6 +4,7 @@ module ScoutApm
4
4
  ACTIVE_JOB_KLASS = 'ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper'.freeze
5
5
  DJ_PERFORMABLE_METHOD = 'Delayed::PerformableMethod'.freeze
6
6
 
7
+
7
8
  attr_reader :logger
8
9
 
9
10
  def name
@@ -69,8 +70,17 @@ module ScoutApm
69
70
 
70
71
  # Call the job itself.
71
72
  block.call(job, *args)
72
- rescue
73
+ rescue Exception => exception
74
+ # Capture the error for further processing and shipping
73
75
  req.error!
76
+ # Abusing this key to pass job info
77
+ params_key = 'action_dispatch.request.parameters'
78
+ env = {}
79
+ env[params_key] = job.payload_object.job_data
80
+ env[:custom_controller] = name
81
+ env[:custom_action] = queue
82
+ context = ScoutApm::Agent.instance.context
83
+ context.error_buffer.capture(exception, env)
74
84
  raise
75
85
  ensure
76
86
  req.stop_layer if started_job
@@ -60,8 +60,14 @@ module ScoutApm
60
60
  started_job = true
61
61
 
62
62
  yield
63
- rescue
63
+ rescue Exception => exception
64
64
  req.error!
65
+ env = {
66
+ :custom_controller => job_class(job),
67
+ :custom_action => queue
68
+ }
69
+ context = ScoutApm::Agent.instance.context
70
+ context.error_buffer.capture(exception, env)
65
71
  raise
66
72
  ensure
67
73
  req.stop_layer if started_job
@@ -34,8 +34,14 @@ module ScoutApm
34
34
  started_job = true # Following Convention
35
35
 
36
36
  block.call
37
- rescue
37
+ rescue Exception => exception
38
38
  req.error!
39
+ env = {
40
+ :custom_controller => job.class.name,
41
+ :custom_action => job.queue_name.presence || UNKNOWN_QUEUE_PLACEHOLDER
42
+ }
43
+ context = ScoutApm::Agent.instance.context
44
+ context.error_buffer.capture(exception, env)
39
45
  raise
40
46
  ensure
41
47
  req.stop_layer if started_job
@@ -42,8 +42,14 @@ module ScoutApm
42
42
  else
43
43
  super
44
44
  end
45
- rescue Exception
45
+ rescue Exception => exception
46
46
  req.error!
47
+ env = {
48
+ :custom_controller => job_class,
49
+ :custom_action => queue
50
+ }
51
+ context = ScoutApm::Agent.instance.context
52
+ context.error_buffer.capture(exception, env)
47
53
  raise
48
54
  ensure
49
55
  req.stop_layer if started_job
@@ -115,8 +115,14 @@ module ScoutApm
115
115
  started_job = true
116
116
 
117
117
  _run_without_scout(*args)
118
- rescue Exception => e
118
+ rescue Exception => exception
119
119
  req.error!
120
+ env = {
121
+ :custom_controller => job_class,
122
+ :custom_action => queue
123
+ }
124
+ context = ScoutApm::Agent.instance.context
125
+ context.error_buffer.capture(exception, env)
120
126
  raise
121
127
  ensure
122
128
  req.stop_layer if started_job
@@ -79,8 +79,14 @@ module ScoutApm
79
79
  started_job = true
80
80
 
81
81
  yield
82
- rescue Exception => e
82
+ rescue Exception => exception
83
83
  req.error!
84
+ env = {
85
+ :custom_controller => job_class,
86
+ :custom_action => queue
87
+ }
88
+ context = ScoutApm::Agent.instance.context
89
+ context.error_buffer.capture(exception, env)
84
90
  raise
85
91
  ensure
86
92
  req.stop_layer if started_job
@@ -65,8 +65,14 @@ module ScoutApm
65
65
  started_job = true
66
66
 
67
67
  process_work_without_scout(*args)
68
- rescue Exception => e
68
+ rescue Exception => exception
69
69
  req.error!
70
+ env = {
71
+ :custom_controller => job_class,
72
+ :custom_action => queue
73
+ }
74
+ context = ScoutApm::Agent.instance.context
75
+ context.error_buffer.capture(exception, env)
70
76
  raise
71
77
  ensure
72
78
  req.stop_layer if started_job
@@ -32,8 +32,26 @@ module ScoutApm
32
32
  started_job = true # Following Convention
33
33
 
34
34
  block.call
35
- rescue
35
+ rescue Exception => exception
36
36
  req.error!
37
+ # Extract job parameters like DelayedJob does
38
+ params_key = 'action_dispatch.request.parameters'
39
+ job_args = begin
40
+ {
41
+ arguments: job.arguments,
42
+ job_id: job.job_id,
43
+ }
44
+ rescue => e
45
+ { error_extracting_params: e.message }
46
+ end
47
+
48
+ env = {
49
+ params_key => job_args,
50
+ :custom_controller => job.class.name,
51
+ :custom_action => job.queue_name.presence || UNKNOWN_QUEUE_PLACEHOLDER
52
+ }
53
+ context = ScoutApm::Agent.instance.context
54
+ context.error_buffer.capture(exception, env)
37
55
  raise
38
56
  ensure
39
57
  req.stop_layer if started_job
@@ -22,6 +22,8 @@ module ScoutApm
22
22
  else
23
23
  {}
24
24
  end
25
+ # Add the transaction_id, as it won't be added to the context normally until the request has been recorded.
26
+ @context[:transaction_id] ||= RequestManager.lookup.transaction_id
25
27
 
26
28
  @exception_class = LengthLimit.new(exception.class.name).to_s
27
29
  @message = LengthLimit.new(exception.message, 100).to_s
@@ -46,6 +48,7 @@ module ScoutApm
46
48
  # For background workers like sidekiq
47
49
  # TODO: extract data creation for background jobs
48
50
  components[:controller] ||= env[:custom_controller]
51
+ components[:action] ||= env[:custom_action]
49
52
 
50
53
  components
51
54
  end
@@ -92,6 +95,7 @@ module ScoutApm
92
95
 
93
96
  # Capture params from env
94
97
  KEYS_TO_KEEP = [
98
+ "REQUEST_METHOD",
95
99
  "HTTP_USER_AGENT",
96
100
  "HTTP_REFERER",
97
101
  "HTTP_ACCEPT_ENCODING",
@@ -21,10 +21,13 @@ module ScoutApm
21
21
  @total_layers += 1
22
22
 
23
23
  @total_call_time += layer.total_call_time
24
- @total_exclusive_time += layer.total_exclusive_time
24
+ # For limited layers, exclusive time should equal total time since limited layers
25
+ # report no children. As such, we need to consider all absorbed time as exclusive.
26
+ @total_exclusive_time += layer.total_call_time
25
27
 
26
28
  @total_allocations += layer.total_allocations
27
- @total_exclusive_allocations += layer.total_exclusive_allocations
29
+ # Same logic applies to allocations
30
+ @total_exclusive_allocations += layer.total_allocations
28
31
  end
29
32
 
30
33
  def total_call_time
@@ -145,7 +145,7 @@ module ScoutApm
145
145
  def call(severity, time, progname, msg)
146
146
  # since STDOUT isn't exclusive like the scout_apm.log file, apply a prefix.
147
147
  # XXX: Pass in context to the formatter
148
- "[#{Utils::Time.to_s(time)} #{ScoutApm::Agent.instance.context.environment.hostname} (#{$$})] #{severity} : #{msg}\n"
148
+ "[#{time.strftime("%m/%d/%y %H:%M:%S %z")} #{ScoutApm::Agent.instance.context.environment.hostname} (#{$$})] #{severity} : #{msg}\n"
149
149
  end
150
150
  end
151
151
 
@@ -1,3 +1,3 @@
1
1
  module ScoutApm
2
- VERSION = "5.7.1"
2
+ VERSION = "5.8.0"
3
3
  end
data/lib/scout_apm.rb CHANGED
@@ -118,7 +118,6 @@ require 'scout_apm/utils/installed_gems'
118
118
  require 'scout_apm/utils/klass_helper'
119
119
  require 'scout_apm/utils/scm'
120
120
  require 'scout_apm/utils/sql_sanitizer'
121
- require 'scout_apm/utils/time'
122
121
  require 'scout_apm/utils/unique_id'
123
122
  require 'scout_apm/utils/numbers'
124
123
  require 'scout_apm/utils/gzip_helper'
@@ -0,0 +1,109 @@
1
+ require 'test_helper'
2
+ require 'scout_apm/background_job_integrations/faktory'
3
+
4
+ class FaktoryTest < Minitest::Test
5
+ FaktoryMiddleware = ScoutApm::BackgroundJobIntegrations::FaktoryMiddleware
6
+
7
+ def test_middleware_call_job_exception_with_error_monitoring
8
+ # Test that error buffer is called on exception
9
+ fake_request = mock
10
+ fake_request.expects(:annotate_request)
11
+ fake_request.expects(:start_layer).twice
12
+ fake_request.expects(:stop_layer).twice
13
+ fake_request.expects(:error!)
14
+
15
+ fake_context = mock
16
+ fake_error_buffer = mock
17
+ fake_context.expects(:error_buffer).returns(fake_error_buffer)
18
+
19
+ expected_env = {
20
+ :custom_controller => "TestJob",
21
+ :custom_action => "critical"
22
+ }
23
+ fake_error_buffer.expects(:capture).with(kind_of(RuntimeError), expected_env)
24
+
25
+ ScoutApm::RequestManager.stubs(:lookup).returns(fake_request)
26
+ ScoutApm::Agent.instance.expects(:context).returns(fake_context)
27
+
28
+ worker_instance = mock
29
+ job = {
30
+ "queue" => "critical",
31
+ "jobtype" => "TestJob",
32
+ "enqueued_at" => Time.now.iso8601
33
+ }
34
+
35
+ assert_raises RuntimeError do
36
+ FaktoryMiddleware.new.call(worker_instance, job) do
37
+ raise RuntimeError, "Job failed"
38
+ end
39
+ end
40
+ end
41
+
42
+ def test_middleware_call_activejob_wrapper
43
+ # Test ActiveJob job class extraction
44
+ fake_request = mock
45
+ fake_request.expects(:annotate_request)
46
+ fake_request.expects(:start_layer).twice
47
+ fake_request.expects(:stop_layer).twice
48
+ fake_request.expects(:error!)
49
+
50
+ fake_context = mock
51
+ fake_error_buffer = mock
52
+ fake_context.expects(:error_buffer).returns(fake_error_buffer)
53
+
54
+ expected_env = {
55
+ :custom_controller => "MyRealJob", # Should extract from custom.wrapped
56
+ :custom_action => "default"
57
+ }
58
+ fake_error_buffer.expects(:capture).with(kind_of(RuntimeError), expected_env)
59
+
60
+ ScoutApm::RequestManager.stubs(:lookup).returns(fake_request)
61
+ ScoutApm::Agent.instance.expects(:context).returns(fake_context)
62
+
63
+ # ActiveJob wrapper scenario
64
+ worker_instance = mock
65
+ job = {
66
+ "queue" => "default",
67
+ "jobtype" => "ActiveJob::QueueAdapters::FaktoryAdapter::JobWrapper",
68
+ "custom" => { "wrapped" => "MyRealJob" },
69
+ "created_at" => Time.now.iso8601
70
+ }
71
+
72
+ assert_raises RuntimeError do
73
+ FaktoryMiddleware.new.call(worker_instance, job) do
74
+ raise RuntimeError, "ActiveJob failed"
75
+ end
76
+ end
77
+ end
78
+
79
+ def test_middleware_call_missing_queue_fallback
80
+ # Test behavior when queue is missing
81
+ fake_request = mock
82
+ fake_request.expects(:annotate_request)
83
+ fake_request.expects(:start_layer).twice
84
+ fake_request.expects(:stop_layer).twice
85
+ fake_request.expects(:error!)
86
+
87
+ fake_context = mock
88
+ fake_error_buffer = mock
89
+ fake_context.expects(:error_buffer).returns(fake_error_buffer)
90
+
91
+ expected_env = {
92
+ :custom_controller => "UnknownJob", # Fallback when jobtype missing
93
+ :custom_action => nil # No queue
94
+ }
95
+ fake_error_buffer.expects(:capture).with(kind_of(RuntimeError), expected_env)
96
+
97
+ ScoutApm::RequestManager.stubs(:lookup).returns(fake_request)
98
+ ScoutApm::Agent.instance.expects(:context).returns(fake_context)
99
+
100
+ worker_instance = mock
101
+ job = {} # Empty job data
102
+
103
+ assert_raises RuntimeError do
104
+ FaktoryMiddleware.new.call(worker_instance, job) do
105
+ raise RuntimeError, "Job failed"
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,81 @@
1
+ require 'test_helper'
2
+ require 'scout_apm/background_job_integrations/shoryuken'
3
+
4
+ class ShoryukenTest < Minitest::Test
5
+ ShoryukenIntegration = ScoutApm::BackgroundJobIntegrations::Shoryuken
6
+ ShoryukenMiddleware = ScoutApm::BackgroundJobIntegrations::ShoryukenMiddleware
7
+
8
+ def test_middleware_call_job_exception_with_error_monitoring
9
+ # Test that error buffer is called on exception
10
+ fake_request = mock
11
+ fake_request.expects(:annotate_request)
12
+ fake_request.expects(:start_layer).twice
13
+ fake_request.expects(:stop_layer).twice
14
+ fake_request.expects(:error!)
15
+
16
+ fake_context = mock
17
+ fake_error_buffer = mock
18
+ fake_context.expects(:error_buffer).returns(fake_error_buffer)
19
+
20
+ expected_env = {
21
+ :custom_controller => "TestWorker",
22
+ :custom_action => "test-queue"
23
+ }
24
+ fake_error_buffer.expects(:capture).with(kind_of(RuntimeError), expected_env)
25
+
26
+ ScoutApm::RequestManager.stubs(:lookup).returns(fake_request)
27
+ ScoutApm::Agent.instance.expects(:context).returns(fake_context)
28
+
29
+ worker_instance = mock
30
+ mock_class = mock
31
+ mock_class.expects(:to_s).twice.returns("TestWorker")
32
+ worker_instance.expects(:class).twice.returns(mock_class)
33
+ queue = "test-queue"
34
+ msg = mock
35
+ msg.expects(:attributes).returns({'SentTimestamp' => '1534873927868'})
36
+ body = {}
37
+
38
+ assert_raises RuntimeError do
39
+ ShoryukenMiddleware.new.call(worker_instance, queue, msg, body) do
40
+ raise RuntimeError, "Job failed"
41
+ end
42
+ end
43
+ end
44
+
45
+ def test_middleware_call_activejob_wrapper
46
+ # Test ActiveJob job class extraction
47
+ fake_request = mock
48
+ fake_request.expects(:annotate_request)
49
+ fake_request.expects(:start_layer).twice
50
+ fake_request.expects(:stop_layer).twice
51
+ fake_request.expects(:error!)
52
+
53
+ fake_context = mock
54
+ fake_error_buffer = mock
55
+ fake_context.expects(:error_buffer).returns(fake_error_buffer)
56
+
57
+ expected_env = {
58
+ :custom_controller => "MyRealJob", # Should extract from body
59
+ :custom_action => "priority-queue"
60
+ }
61
+ fake_error_buffer.expects(:capture).with(kind_of(RuntimeError), expected_env)
62
+
63
+ ScoutApm::RequestManager.stubs(:lookup).returns(fake_request)
64
+ ScoutApm::Agent.instance.expects(:context).returns(fake_context)
65
+
66
+ worker_instance = mock
67
+ mock_class = mock
68
+ mock_class.expects(:to_s).returns("ActiveJob::QueueAdapters::ShoryukenAdapter::JobWrapper")
69
+ worker_instance.expects(:class).returns(mock_class)
70
+ queue = "priority-queue"
71
+ msg = mock
72
+ msg.expects(:attributes).returns({'SentTimestamp' => '1534873927868'})
73
+ body = { "job_class" => "MyRealJob" }
74
+
75
+ assert_raises RuntimeError do
76
+ ShoryukenMiddleware.new.call(worker_instance, queue, msg, body) do
77
+ raise RuntimeError, "ActiveJob failed"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -25,6 +25,7 @@ class ErrorBufferTest < Minitest::Test
25
25
 
26
26
  exception = exceptions[0]
27
27
  expected_env_keys = [
28
+ "REQUEST_METHOD",
28
29
  "ANOTHER_HEADER",
29
30
  "HTTP_X_FORWARDED_FOR",
30
31
  "HTTP_USER_AGENT",
@@ -55,7 +55,7 @@ class ErrorTest < Minitest::Test
55
55
 
56
56
  assert_equal "No context or env", exceptions[3].message
57
57
  assert_equal "AnotherError", exceptions[3].exception_class
58
- assert_equal assert_empty_context, exceptions[3].context
58
+ assert_equal assert_default_context, exceptions[3].context
59
59
 
60
60
  assert_equal "Whoops", exceptions[4].message
61
61
  assert_equal "ErrorTest::FakeError", exceptions[4].exception_class
@@ -84,8 +84,8 @@ class ErrorTest < Minitest::Test
84
84
  }
85
85
  end
86
86
 
87
- def assert_empty_context
88
- {user: {}}
87
+ def assert_default_context
88
+ {user: {}, transaction_id: ScoutApm::RequestManager.lookup.transaction_id}
89
89
  end
90
90
 
91
91
  def ex(msg="Whoops")
@@ -18,16 +18,16 @@ class LimitedLayerTest < Minitest::Test
18
18
  ll = ScoutApm::LimitedLayer.new("ActiveRecord")
19
19
 
20
20
  ll.absorb faux_layer("ActiveRecord", "User#Find", 2, 1, 200, 100)
21
- assert_equal 1, ll.total_exclusive_time
21
+ assert_equal 2, ll.total_exclusive_time
22
22
  assert_equal 2, ll.total_call_time
23
- assert_equal 100, ll.total_exclusive_allocations
23
+ assert_equal 200, ll.total_exclusive_allocations
24
24
  assert_equal 200, ll.total_allocations
25
25
 
26
26
 
27
27
  ll.absorb faux_layer("ActiveRecord", "User#Find", 4, 3, 400, 300)
28
- assert_equal 4, ll.total_exclusive_time # 3 + 1
28
+ assert_equal 6, ll.total_exclusive_time # 4 + 2 (for limited layers, should equal total time)
29
29
  assert_equal 6, ll.total_call_time # 4 + 2
30
- assert_equal 400, ll.total_exclusive_allocations # 300 + 100
30
+ assert_equal 600, ll.total_exclusive_allocations # 400 + 200 (same goes for allocations)
31
31
  assert_equal 600, ll.total_allocations # 400 + 200
32
32
  end
33
33
 
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: 5.7.1
4
+ version: 5.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Haynes
@@ -413,7 +413,6 @@ files:
413
413
  - lib/scout_apm/utils/numbers.rb
414
414
  - lib/scout_apm/utils/scm.rb
415
415
  - lib/scout_apm/utils/sql_sanitizer.rb
416
- - lib/scout_apm/utils/time.rb
417
416
  - lib/scout_apm/utils/unique_id.rb
418
417
  - lib/scout_apm/version.rb
419
418
  - lib/tasks/doctor.rake
@@ -435,6 +434,8 @@ files:
435
434
  - test/unit/auto_instrument/rescue_from-instrumented.rb
436
435
  - test/unit/auto_instrument/rescue_from.rb
437
436
  - test/unit/auto_instrument_test.rb
437
+ - test/unit/background_job_integrations/faktory_test.rb
438
+ - test/unit/background_job_integrations/shoryuken_test.rb
438
439
  - test/unit/background_job_integrations/sidekiq_test.rb
439
440
  - test/unit/config_test.rb
440
441
  - test/unit/context_test.rb
@@ -1,12 +0,0 @@
1
- module ScoutApm
2
- module Utils
3
- class Time
4
- # Handles both integer (unix) time and Time objects
5
- # example output: "09/10/15 04:34:28 -0600"
6
- def self.to_s(time)
7
- return to_s(::Time.at(time)) if time.is_a? Integer
8
- time.strftime("%m/%d/%y %H:%M:%S %z")
9
- end
10
- end
11
- end
12
- end