appsignal 3.0.0.beta.1 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +1 -1
  3. data/.semaphore/semaphore.yml +88 -88
  4. data/CHANGELOG.md +41 -1
  5. data/Rakefile +12 -4
  6. data/appsignal.gemspec +7 -5
  7. data/build_matrix.yml +11 -11
  8. data/ext/agent.yml +17 -17
  9. data/gemfiles/no_dependencies.gemfile +0 -7
  10. data/lib/appsignal.rb +1 -2
  11. data/lib/appsignal/config.rb +1 -1
  12. data/lib/appsignal/extension.rb +50 -0
  13. data/lib/appsignal/helpers/instrumentation.rb +69 -5
  14. data/lib/appsignal/hooks.rb +16 -0
  15. data/lib/appsignal/hooks/action_cable.rb +10 -2
  16. data/lib/appsignal/hooks/sidekiq.rb +9 -142
  17. data/lib/appsignal/integrations/object.rb +21 -43
  18. data/lib/appsignal/integrations/railtie.rb +0 -4
  19. data/lib/appsignal/integrations/sidekiq.rb +171 -0
  20. data/lib/appsignal/minutely.rb +6 -0
  21. data/lib/appsignal/transaction.rb +2 -2
  22. data/lib/appsignal/version.rb +1 -1
  23. data/spec/lib/appsignal/config_spec.rb +2 -0
  24. data/spec/lib/appsignal/extension_install_failure_spec.rb +0 -7
  25. data/spec/lib/appsignal/extension_spec.rb +43 -9
  26. data/spec/lib/appsignal/hooks/action_cable_spec.rb +88 -0
  27. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +60 -458
  28. data/spec/lib/appsignal/hooks_spec.rb +41 -0
  29. data/spec/lib/appsignal/integrations/object_spec.rb +91 -4
  30. data/spec/lib/appsignal/integrations/sidekiq_spec.rb +524 -0
  31. data/spec/lib/appsignal/transaction_spec.rb +17 -0
  32. data/spec/lib/appsignal/utils/data_spec.rb +133 -87
  33. data/spec/lib/appsignal_spec.rb +162 -47
  34. data/spec/lib/puma/appsignal_spec.rb +28 -0
  35. data/spec/spec_helper.rb +22 -0
  36. data/spec/support/testing.rb +11 -1
  37. metadata +9 -8
  38. data/gemfiles/rails-4.0.gemfile +0 -6
  39. data/gemfiles/rails-4.1.gemfile +0 -6
@@ -69,6 +69,22 @@ module Appsignal
69
69
  text.size > 200 ? "#{text[0...197]}..." : text
70
70
  end
71
71
  end
72
+
73
+ # Alias integration constants that have moved to their own module.
74
+ def self.const_missing(name)
75
+ case name
76
+ when :SidekiqPlugin
77
+ require "appsignal/integrations/sidekiq"
78
+ callers = caller
79
+ Appsignal::Utils::DeprecationMessage.message \
80
+ "The constant Appsignal::Hooks::SidekiqPlugin has been deprecated. " \
81
+ "Please update the constant name to Appsignal::Integrations::SidekiqMiddleware " \
82
+ "in the following file to remove this message.\n#{callers.first}"
83
+ Appsignal::Integrations::SidekiqMiddleware
84
+ else
85
+ super
86
+ end
87
+ end
72
88
  end
73
89
  end
74
90
 
@@ -25,7 +25,11 @@ module Appsignal
25
25
  def install_callbacks
26
26
  ActionCable::Channel::Base.set_callback :subscribe, :around, :prepend => true do |channel, inner|
27
27
  # The request is only the original websocket request
28
- env = channel.connection.env
28
+ connection = channel.connection
29
+ # #env is not available on the Rails ConnectionStub class used in the
30
+ # Rails app test suite. If we call `#env` it causes an error to occur
31
+ # in apps' test suites.
32
+ env = connection.respond_to?(:env) ? connection.env : {}
29
33
  request = ActionDispatch::Request.new(env)
30
34
  env[Appsignal::Hooks::ActionCableHook::REQUEST_ID] ||=
31
35
  request.request_id || SecureRandom.uuid
@@ -53,7 +57,11 @@ module Appsignal
53
57
 
54
58
  ActionCable::Channel::Base.set_callback :unsubscribe, :around, :prepend => true do |channel, inner|
55
59
  # The request is only the original websocket request
56
- env = channel.connection.env
60
+ connection = channel.connection
61
+ # #env is not available on the Rails ConnectionStub class used in the
62
+ # Rails app test suite. If we call `#env` it causes an error to occur
63
+ # in apps' test suites.
64
+ env = connection.respond_to?(:env) ? connection.env : {}
57
65
  request = ActionDispatch::Request.new(env)
58
66
  env[Appsignal::Hooks::ActionCableHook::REQUEST_ID] ||=
59
67
  request.request_id || SecureRandom.uuid
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
-
5
3
  module Appsignal
6
4
  class Hooks
7
5
  class SidekiqHook < Appsignal::Hooks::Hook
@@ -12,153 +10,22 @@ module Appsignal
12
10
  end
13
11
 
14
12
  def install
13
+ require "appsignal/integrations/sidekiq"
15
14
  Appsignal::Minutely.probes.register :sidekiq, Appsignal::Probes::SidekiqProbe
16
15
 
17
16
  ::Sidekiq.configure_server do |config|
18
- config.server_middleware do |chain|
19
- chain.add Appsignal::Hooks::SidekiqPlugin
20
- end
21
- end
22
- end
23
- end
24
-
25
- # @api private
26
- class SidekiqPlugin # rubocop:disable Metrics/ClassLength
27
- include Appsignal::Hooks::Helpers
28
-
29
- EXCLUDED_JOB_KEYS = %w[
30
- args backtrace class created_at enqueued_at error_backtrace error_class
31
- error_message failed_at jid retried_at retry wrapped
32
- ].freeze
33
-
34
- def call(_worker, item, _queue)
35
- job_status = nil
36
- transaction = Appsignal::Transaction.create(
37
- item["jid"],
38
- Appsignal::Transaction::BACKGROUND_JOB,
39
- Appsignal::Transaction::GenericRequest.new(
40
- :queue_start => item["enqueued_at"]
41
- )
42
- )
43
-
44
- Appsignal.instrument "perform_job.sidekiq" do
45
- begin
46
- yield
47
- rescue Exception => exception # rubocop:disable Lint/RescueException
48
- job_status = :failed
49
- transaction.set_error(exception)
50
- raise exception
51
- end
52
- end
53
- ensure
54
- if transaction
55
- transaction.set_action_if_nil(formatted_action_name(item))
56
-
57
- params = filtered_arguments(item)
58
- transaction.params = params if params
59
-
60
- formatted_metadata(item).each do |key, value|
61
- transaction.set_metadata key, value
62
- end
63
- transaction.set_http_or_background_queue_start
64
- Appsignal::Transaction.complete_current!
65
- queue = item["queue"] || "unknown"
66
- if job_status
67
- increment_counter "queue_job_count", 1,
68
- :queue => queue,
69
- :status => job_status
70
- end
71
- increment_counter "queue_job_count", 1,
72
- :queue => queue,
73
- :status => :processed
74
- end
75
- end
76
-
77
- private
78
-
79
- def increment_counter(key, value, tags = {})
80
- Appsignal.increment_counter "sidekiq_#{key}", value, tags
81
- end
17
+ config.error_handlers << \
18
+ Appsignal::Integrations::SidekiqErrorHandler.new
82
19
 
83
- def formatted_action_name(job)
84
- sidekiq_action_name = parse_action_name(job)
85
- return unless sidekiq_action_name
86
-
87
- complete_action = sidekiq_action_name =~ /\.|#/
88
- return sidekiq_action_name if complete_action
89
-
90
- "#{sidekiq_action_name}#perform"
91
- end
92
-
93
- def filtered_arguments(job)
94
- arguments = parse_arguments(job)
95
- return unless arguments
96
-
97
- Appsignal::Utils::HashSanitizer.sanitize(
98
- arguments,
99
- Appsignal.config[:filter_parameters]
100
- )
101
- end
102
-
103
- def formatted_metadata(item)
104
- {}.tap do |hash|
105
- (item || {}).each do |key, value|
106
- next if EXCLUDED_JOB_KEYS.include?(key)
107
-
108
- hash[key] = truncate(string_or_inspect(value))
109
- end
110
- end
111
- end
112
-
113
- # Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L316-L334
114
- def parse_action_name(job)
115
- args = job.fetch("args", [])
116
- job_class = job["class"]
117
- case job_class
118
- when "Sidekiq::Extensions::DelayedModel"
119
- safe_load(args[0], job_class) do |target, method, _|
120
- "#{target.class}##{method}"
121
- end
122
- when /\ASidekiq::Extensions::Delayed/
123
- safe_load(args[0], job_class) do |target, method, _|
124
- "#{target}.#{method}"
125
- end
126
- else
127
- job_class
128
- end
129
- end
130
-
131
- # Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L336-L358
132
- def parse_arguments(job)
133
- args = job.fetch("args", [])
134
- case job["class"]
135
- when /\ASidekiq::Extensions::Delayed/
136
- safe_load(args[0], args) do |_, _, arg|
137
- arg
138
- end
139
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
140
- nil # Set in the ActiveJob integration
141
- else
142
- # Sidekiq Enterprise argument encryption.
143
- # More information: https://github.com/mperham/sidekiq/wiki/Ent-Encryption
144
- if job["encrypt".freeze]
145
- # No point in showing 150+ bytes of random garbage
146
- args[-1] = "[encrypted data]".freeze
20
+ config.server_middleware do |chain|
21
+ if chain.respond_to? :prepend
22
+ chain.prepend Appsignal::Integrations::SidekiqMiddleware
23
+ else
24
+ chain.add Appsignal::Integrations::SidekiqMiddleware
25
+ end
147
26
  end
148
- args
149
27
  end
150
28
  end
151
-
152
- # Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L403-L412
153
- def safe_load(content, default)
154
- yield(*YAML.load(content))
155
- rescue => error
156
- # Sidekiq issue #1761: in dev mode, it's possible to have jobs enqueued
157
- # which haven't been loaded into memory yet so the YAML can't be
158
- # loaded.
159
- Appsignal.logger.warn "Unable to load YAML: #{error.message}"
160
- default
161
- end
162
29
  end
163
30
  end
164
31
  end
@@ -5,56 +5,34 @@ if defined?(Appsignal)
5
5
  end
6
6
 
7
7
  class Object
8
- if Appsignal::System.ruby_2_7_or_newer?
9
- def self.appsignal_instrument_class_method(method_name, options = {})
10
- singleton_class.send \
11
- :alias_method, "appsignal_uninstrumented_#{method_name}", method_name
12
- singleton_class.send(:define_method, method_name) do |*args, **kwargs, &block|
13
- name = options.fetch(:name) do
14
- "#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
15
- end
16
- Appsignal.instrument name do
17
- send "appsignal_uninstrumented_#{method_name}", *args, **kwargs, &block
18
- end
8
+ def self.appsignal_instrument_class_method(method_name, options = {})
9
+ singleton_class.send \
10
+ :alias_method, "appsignal_uninstrumented_#{method_name}", method_name
11
+ singleton_class.send(:define_method, method_name) do |*args, &block|
12
+ name = options.fetch(:name) do
13
+ "#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
19
14
  end
20
- end
21
-
22
- def self.appsignal_instrument_method(method_name, options = {})
23
- alias_method "appsignal_uninstrumented_#{method_name}", method_name
24
- define_method method_name do |*args, **kwargs, &block|
25
- name = options.fetch(:name) do
26
- "#{method_name}.#{appsignal_reverse_class_name}.other"
27
- end
28
- Appsignal.instrument name do
29
- send "appsignal_uninstrumented_#{method_name}", *args, **kwargs, &block
30
- end
15
+ Appsignal.instrument name do
16
+ send "appsignal_uninstrumented_#{method_name}", *args, &block
31
17
  end
32
18
  end
33
- else
34
- def self.appsignal_instrument_class_method(method_name, options = {})
35
- singleton_class.send \
36
- :alias_method, "appsignal_uninstrumented_#{method_name}", method_name
37
- singleton_class.send(:define_method, method_name) do |*args, &block|
38
- name = options.fetch(:name) do
39
- "#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
40
- end
41
- Appsignal.instrument name do
42
- send "appsignal_uninstrumented_#{method_name}", *args, &block
43
- end
44
- end
19
+
20
+ if singleton_class.respond_to?(:ruby2_keywords, true) # rubocop:disable Style/GuardClause
21
+ singleton_class.send(:ruby2_keywords, method_name)
45
22
  end
23
+ end
46
24
 
47
- def self.appsignal_instrument_method(method_name, options = {})
48
- alias_method "appsignal_uninstrumented_#{method_name}", method_name
49
- define_method method_name do |*args, &block|
50
- name = options.fetch(:name) do
51
- "#{method_name}.#{appsignal_reverse_class_name}.other"
52
- end
53
- Appsignal.instrument name do
54
- send "appsignal_uninstrumented_#{method_name}", *args, &block
55
- end
25
+ def self.appsignal_instrument_method(method_name, options = {})
26
+ alias_method "appsignal_uninstrumented_#{method_name}", method_name
27
+ define_method method_name do |*args, &block|
28
+ name = options.fetch(:name) do
29
+ "#{method_name}.#{appsignal_reverse_class_name}.other"
30
+ end
31
+ Appsignal.instrument name do
32
+ send "appsignal_uninstrumented_#{method_name}", *args, &block
56
33
  end
57
34
  end
35
+ ruby2_keywords method_name if respond_to?(:ruby2_keywords, true)
58
36
  end
59
37
 
60
38
  def self.appsignal_reverse_class_name
@@ -30,10 +30,6 @@ module Appsignal
30
30
  Appsignal::Rack::RailsInstrumentation
31
31
  )
32
32
 
33
- if Appsignal.config[:enable_frontend_error_catching]
34
- app.middleware.insert_before(Appsignal::Rack::RailsInstrumentation)
35
- end
36
-
37
33
  Appsignal.start
38
34
  end
39
35
  end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Appsignal
6
+ module Integrations
7
+ # Error handler for Sidekiq to report errors from jobs and internal Sidekiq
8
+ # errors.
9
+ #
10
+ # @api private
11
+ class SidekiqErrorHandler
12
+ def call(exception, sidekiq_context)
13
+ transaction = Appsignal::Transaction.current
14
+
15
+ if transaction.nil_transaction?
16
+ # Sidekiq error outside of the middleware scope.
17
+ # Can be a job JSON parse error or some other error happening in
18
+ # Sidekiq.
19
+ transaction = Appsignal::Transaction.create(
20
+ SecureRandom.uuid, # Newly generated job id
21
+ Appsignal::Transaction::BACKGROUND_JOB,
22
+ Appsignal::Transaction::GenericRequest.new({})
23
+ )
24
+ transaction.set_action_if_nil("SidekiqInternal")
25
+ transaction.set_metadata("sidekiq_error", sidekiq_context[:context])
26
+ transaction.params = { :jobstr => sidekiq_context[:jobstr] }
27
+ end
28
+
29
+ transaction.set_error(exception)
30
+ Appsignal::Transaction.complete_current!
31
+ end
32
+ end
33
+
34
+ # @api private
35
+ class SidekiqMiddleware # rubocop:disable Metrics/ClassLength
36
+ include Appsignal::Hooks::Helpers
37
+
38
+ EXCLUDED_JOB_KEYS = %w[
39
+ args backtrace class created_at enqueued_at error_backtrace error_class
40
+ error_message failed_at jid retried_at retry wrapped
41
+ ].freeze
42
+
43
+ def call(_worker, item, _queue)
44
+ job_status = nil
45
+ transaction = Appsignal::Transaction.create(
46
+ item["jid"],
47
+ Appsignal::Transaction::BACKGROUND_JOB,
48
+ Appsignal::Transaction::GenericRequest.new(
49
+ :queue_start => item["enqueued_at"]
50
+ )
51
+ )
52
+
53
+ Appsignal.instrument "perform_job.sidekiq" do
54
+ yield
55
+ end
56
+ rescue Exception => exception # rubocop:disable Lint/RescueException
57
+ job_status = :failed
58
+ raise exception
59
+ ensure
60
+ if transaction
61
+ transaction.set_action_if_nil(formatted_action_name(item))
62
+
63
+ params = filtered_arguments(item)
64
+ transaction.params = params if params
65
+
66
+ formatted_metadata(item).each do |key, value|
67
+ transaction.set_metadata key, value
68
+ end
69
+ transaction.set_http_or_background_queue_start
70
+ Appsignal::Transaction.complete_current! unless exception
71
+
72
+ queue = item["queue"] || "unknown"
73
+ if job_status
74
+ increment_counter "queue_job_count", 1,
75
+ :queue => queue,
76
+ :status => job_status
77
+ end
78
+ increment_counter "queue_job_count", 1,
79
+ :queue => queue,
80
+ :status => :processed
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def increment_counter(key, value, tags = {})
87
+ Appsignal.increment_counter "sidekiq_#{key}", value, tags
88
+ end
89
+
90
+ def formatted_action_name(job)
91
+ sidekiq_action_name = parse_action_name(job)
92
+ return unless sidekiq_action_name
93
+
94
+ complete_action = sidekiq_action_name =~ /\.|#/
95
+ return sidekiq_action_name if complete_action
96
+
97
+ "#{sidekiq_action_name}#perform"
98
+ end
99
+
100
+ def filtered_arguments(job)
101
+ arguments = parse_arguments(job)
102
+ return unless arguments
103
+
104
+ Appsignal::Utils::HashSanitizer.sanitize(
105
+ arguments,
106
+ Appsignal.config[:filter_parameters]
107
+ )
108
+ end
109
+
110
+ def formatted_metadata(item)
111
+ {}.tap do |hash|
112
+ (item || {}).each do |key, value|
113
+ next if EXCLUDED_JOB_KEYS.include?(key)
114
+
115
+ hash[key] = truncate(string_or_inspect(value))
116
+ end
117
+ end
118
+ end
119
+
120
+ # Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L316-L334
121
+ def parse_action_name(job)
122
+ args = job.fetch("args", [])
123
+ job_class = job["class"]
124
+ case job_class
125
+ when "Sidekiq::Extensions::DelayedModel"
126
+ safe_load(args[0], job_class) do |target, method, _|
127
+ "#{target.class}##{method}"
128
+ end
129
+ when /\ASidekiq::Extensions::Delayed/
130
+ safe_load(args[0], job_class) do |target, method, _|
131
+ "#{target}.#{method}"
132
+ end
133
+ else
134
+ job_class
135
+ end
136
+ end
137
+
138
+ # Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L336-L358
139
+ def parse_arguments(job)
140
+ args = job.fetch("args", [])
141
+ case job["class"]
142
+ when /\ASidekiq::Extensions::Delayed/
143
+ safe_load(args[0], args) do |_, _, arg|
144
+ arg
145
+ end
146
+ when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
147
+ nil # Set in the ActiveJob integration
148
+ else
149
+ # Sidekiq Enterprise argument encryption.
150
+ # More information: https://github.com/mperham/sidekiq/wiki/Ent-Encryption
151
+ if job["encrypt".freeze]
152
+ # No point in showing 150+ bytes of random garbage
153
+ args[-1] = "[encrypted data]".freeze
154
+ end
155
+ args
156
+ end
157
+ end
158
+
159
+ # Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L403-L412
160
+ def safe_load(content, default)
161
+ yield(*YAML.load(content))
162
+ rescue => error
163
+ # Sidekiq issue #1761: in dev mode, it's possible to have jobs enqueued
164
+ # which haven't been loaded into memory yet so the YAML can't be
165
+ # loaded.
166
+ Appsignal.logger.warn "Unable to load YAML: #{error.message}"
167
+ default
168
+ end
169
+ end
170
+ end
171
+ end