appsignal 4.8.6-java → 4.9.0-java

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/appsignal.gemspec +2 -2
  4. data/build_matrix.yml +25 -0
  5. data/ext/base.rb +3 -7
  6. data/ext/extconf.rb +1 -1
  7. data/lib/appsignal/check_in/event.rb +5 -4
  8. data/lib/appsignal/cli/diagnose.rb +1 -1
  9. data/lib/appsignal/cli/helpers.rb +2 -2
  10. data/lib/appsignal/cli/install.rb +3 -3
  11. data/lib/appsignal/config.rb +44 -1
  12. data/lib/appsignal/environment.rb +1 -0
  13. data/lib/appsignal/extension/jruby.rb +6 -5
  14. data/lib/appsignal/extension.rb +1 -1
  15. data/lib/appsignal/helpers/instrumentation.rb +12 -12
  16. data/lib/appsignal/hooks/action_cable.rb +2 -2
  17. data/lib/appsignal/hooks/active_job.rb +42 -3
  18. data/lib/appsignal/hooks/delayed_job.rb +2 -1
  19. data/lib/appsignal/hooks/excon.rb +1 -1
  20. data/lib/appsignal/hooks/faraday.rb +21 -0
  21. data/lib/appsignal/hooks/http.rb +9 -0
  22. data/lib/appsignal/hooks/mongo_ruby_driver.rb +2 -1
  23. data/lib/appsignal/hooks/que.rb +7 -1
  24. data/lib/appsignal/hooks/resque.rb +5 -1
  25. data/lib/appsignal/hooks/shoryuken.rb +15 -1
  26. data/lib/appsignal/hooks/sidekiq.rb +14 -1
  27. data/lib/appsignal/hooks.rb +1 -0
  28. data/lib/appsignal/integrations/action_cable.rb +1 -1
  29. data/lib/appsignal/integrations/active_support_notifications.rb +19 -6
  30. data/lib/appsignal/integrations/delayed_job_plugin.rb +38 -1
  31. data/lib/appsignal/integrations/excon.rb +8 -0
  32. data/lib/appsignal/integrations/faraday.rb +51 -0
  33. data/lib/appsignal/integrations/http.rb +9 -0
  34. data/lib/appsignal/integrations/mongo_ruby_driver.rb +4 -2
  35. data/lib/appsignal/integrations/net_http.rb +7 -0
  36. data/lib/appsignal/integrations/object.rb +3 -3
  37. data/lib/appsignal/integrations/que.rb +68 -1
  38. data/lib/appsignal/integrations/rake.rb +1 -1
  39. data/lib/appsignal/integrations/resque.rb +20 -1
  40. data/lib/appsignal/integrations/shoryuken.rb +33 -1
  41. data/lib/appsignal/integrations/sidekiq.rb +73 -36
  42. data/lib/appsignal/integrations/webmachine.rb +1 -1
  43. data/lib/appsignal/logger.rb +4 -1
  44. data/lib/appsignal/rack/abstract_middleware.rb +1 -1
  45. data/lib/appsignal/rack/body_wrapper.rb +5 -5
  46. data/lib/appsignal/rack/hanami_middleware.rb +1 -0
  47. data/lib/appsignal/sample_data.rb +1 -0
  48. data/lib/appsignal/transaction.rb +59 -11
  49. data/lib/appsignal/utils/query_params_sanitizer.rb +2 -0
  50. data/lib/appsignal/version.rb +1 -1
  51. data/lib/appsignal.rb +2 -2
  52. data/lib/puma/plugin/appsignal.rb +1 -1
  53. data/sig/appsignal.rbi +41 -1
  54. data/sig/appsignal.rbs +30 -0
  55. metadata +7 -6
  56. data/lib/appsignal/event_formatter/faraday/request_formatter.rb +0 -24
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ class Hooks
5
+ # @!visibility private
6
+ class FaradayHook < Appsignal::Hooks::Hook
7
+ register :faraday
8
+
9
+ def dependencies_present?
10
+ defined?(::Faraday) && Appsignal.config && Appsignal.config[:instrument_faraday]
11
+ end
12
+
13
+ def install
14
+ require "appsignal/integrations/faraday"
15
+ ::Faraday::RackBuilder.prepend(Appsignal::Integrations::FaradayRackBuilderPatch)
16
+
17
+ Appsignal::Environment.report_enabled("faraday")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -16,6 +16,8 @@ module Appsignal
16
16
 
17
17
  def install
18
18
  require "appsignal/integrations/http"
19
+ # `Client#request` takes positional options in http5 and keyword options
20
+ # in http6.
19
21
  integration =
20
22
  if self.class.http6_or_higher?
21
23
  Appsignal::Integrations::HttpIntegration::KeywordOptions
@@ -23,6 +25,13 @@ module Appsignal
23
25
  Appsignal::Integrations::HttpIntegration::HashOptions
24
26
  end
25
27
  HTTP::Client.prepend integration
28
+ # In http6 a chained request (`.follow`, `.headers`, ...) goes through
29
+ # `HTTP::Session#request` instead of `HTTP::Client#request`, so
30
+ # instrument it too (keyword options). http5 has no Session; chained
31
+ # requests run through `Client#request` there.
32
+ if defined?(HTTP::Session)
33
+ HTTP::Session.prepend Appsignal::Integrations::HttpIntegration::KeywordOptions
34
+ end
26
35
 
27
36
  Appsignal::Environment.report_enabled("http_rb")
28
37
  end
@@ -7,7 +7,8 @@ module Appsignal
7
7
  register :mongo_ruby_driver
8
8
 
9
9
  def dependencies_present?
10
- defined?(::Mongo::Monitoring::Global)
10
+ defined?(::Mongo::Monitoring::Global) && Appsignal.config &&
11
+ Appsignal.config[:instrument_mongo]
11
12
  end
12
13
 
13
14
  def install
@@ -7,12 +7,18 @@ module Appsignal
7
7
  register :que
8
8
 
9
9
  def dependencies_present?
10
- defined?(::Que::Job)
10
+ defined?(::Que::Job) && Appsignal.config && Appsignal.config[:instrument_que]
11
11
  end
12
12
 
13
13
  def install
14
14
  require "appsignal/integrations/que"
15
15
  ::Que::Job.prepend Appsignal::Integrations::QuePlugin
16
+ ::Que::Job.singleton_class.prepend Appsignal::Integrations::QueClientPlugin
17
+
18
+ # `bulk_enqueue` exists only on Que 2+; don't define one where it's absent.
19
+ if ::Que::Job.respond_to?(:bulk_enqueue)
20
+ ::Que::Job.singleton_class.prepend Appsignal::Integrations::QueBulkClientPlugin
21
+ end
16
22
 
17
23
  ::Que.error_notifier = proc do |error, _job|
18
24
  Appsignal::Transaction.current.set_error(error)
@@ -7,12 +7,16 @@ module Appsignal
7
7
  register :resque
8
8
 
9
9
  def dependencies_present?
10
- defined?(::Resque)
10
+ defined?(::Resque) && Appsignal.config && Appsignal.config[:instrument_resque]
11
11
  end
12
12
 
13
13
  def install
14
14
  require "appsignal/integrations/resque"
15
15
  Resque::Job.prepend Appsignal::Integrations::ResqueIntegration
16
+
17
+ # Resque enqueues through the `Resque.push` singleton method, so prepend
18
+ # onto its singleton class to record the enqueue event.
19
+ Resque.singleton_class.prepend Appsignal::Integrations::ResquePushIntegration
16
20
  end
17
21
  end
18
22
  end
@@ -7,7 +7,7 @@ module Appsignal
7
7
  register :shoryuken
8
8
 
9
9
  def dependencies_present?
10
- defined?(::Shoryuken)
10
+ defined?(::Shoryuken) && Appsignal.config && Appsignal.config[:instrument_shoryuken]
11
11
  end
12
12
 
13
13
  def install
@@ -17,6 +17,20 @@ module Appsignal
17
17
  config.server_middleware do |chain|
18
18
  chain.add Appsignal::Integrations::ShoryukenMiddleware
19
19
  end
20
+
21
+ # Servers enqueue jobs too, so they need the client middleware that
22
+ # records the enqueue event. Shoryuken only yields `configure_client`
23
+ # outside the server, so register it here as well for enqueues from
24
+ # within a worker.
25
+ config.client_middleware do |chain|
26
+ chain.add Appsignal::Integrations::ShoryukenClientMiddleware
27
+ end
28
+ end
29
+
30
+ ::Shoryuken.configure_client do |config|
31
+ config.client_middleware do |chain|
32
+ chain.add Appsignal::Integrations::ShoryukenClientMiddleware
33
+ end
20
34
  end
21
35
  end
22
36
  end
@@ -20,7 +20,8 @@ module Appsignal
20
20
  end
21
21
 
22
22
  def dependencies_present?
23
- self.class.dependencies_present?
23
+ self.class.dependencies_present? && Appsignal.config &&
24
+ Appsignal.config[:instrument_sidekiq]
24
25
  end
25
26
 
26
27
  def install
@@ -42,6 +43,18 @@ module Appsignal
42
43
  chain.add Appsignal::Integrations::SidekiqMiddleware
43
44
  end
44
45
  end
46
+
47
+ # Servers enqueue jobs too, so they need the client middleware that
48
+ # records the enqueue event.
49
+ config.client_middleware do |chain|
50
+ chain.add Appsignal::Integrations::SidekiqClientMiddleware
51
+ end
52
+ end
53
+
54
+ ::Sidekiq.configure_client do |config|
55
+ config.client_middleware do |chain|
56
+ chain.add Appsignal::Integrations::SidekiqClientMiddleware
57
+ end
45
58
  end
46
59
  end
47
60
  end
@@ -85,6 +85,7 @@ require "appsignal/hooks/code_ownership"
85
85
  require "appsignal/hooks/delayed_job"
86
86
  require "appsignal/hooks/gvl"
87
87
  require "appsignal/hooks/dry_monitor"
88
+ require "appsignal/hooks/faraday"
88
89
  require "appsignal/hooks/http"
89
90
  require "appsignal/hooks/mri"
90
91
  require "appsignal/hooks/net_http"
@@ -15,7 +15,7 @@ module Appsignal
15
15
 
16
16
  begin
17
17
  super
18
- rescue Exception => exception # rubocop:disable Lint/RescueException
18
+ rescue Exception => exception
19
19
  transaction.set_error(exception)
20
20
  raise exception
21
21
  ensure
@@ -7,16 +7,21 @@ module Appsignal
7
7
  class << self
8
8
  BANG = "!"
9
9
 
10
+ # Events a dedicated AppSignal integration already records, so the
11
+ # generic notifications path must not record them a second time. The
12
+ # ActiveJob hook owns `enqueue.active_job` (it wraps the enqueue in its
13
+ # own event, with Rails' native notification nested inside), and the
14
+ # Faraday integration owns `request.faraday`.
15
+ SUPPRESSED_EVENT_NAMES = ["enqueue.active_job", "request.faraday"].freeze
16
+
10
17
  def start_event(name)
11
- # Events that start with a bang are internal to Rails
12
- instrument_this = name[0] != BANG
13
- Appsignal::Transaction.current.start_event if instrument_this
18
+ return unless record_event?(name)
19
+
20
+ Appsignal::Transaction.current.start_event
14
21
  end
15
22
 
16
23
  def finish_event(name, payload = {})
17
- # Events that start with a bang are internal to Rails
18
- instrument_this = name[0] != BANG
19
- return unless instrument_this
24
+ return unless record_event?(name)
20
25
 
21
26
  title, body, body_format = Appsignal::EventFormatter.format(name, payload)
22
27
  Appsignal::Transaction.current.finish_event(
@@ -26,6 +31,14 @@ module Appsignal
26
31
  body_format
27
32
  )
28
33
  end
34
+
35
+ # Events starting with a bang are internal to Rails; suppressed events
36
+ # are recorded by a dedicated integration instead. Both `start_event`
37
+ # and `finish_event` gate on this so the event stack stays balanced.
38
+ def record_event?(name)
39
+ name = name.to_s
40
+ name[0] != BANG && !SUPPRESSED_EVENT_NAMES.include?(name)
41
+ end
29
42
  end
30
43
 
31
44
  module InstrumentIntegration
@@ -7,6 +7,10 @@ module Appsignal
7
7
  extend Appsignal::Hooks::Helpers
8
8
 
9
9
  callbacks do |lifecycle|
10
+ lifecycle.around(:enqueue) do |job, &block|
11
+ enqueue_with_instrumentation(job, block)
12
+ end
13
+
10
14
  lifecycle.around(:invoke_job) do |job, &block|
11
15
  invoke_with_instrumentation(job, block)
12
16
  end
@@ -16,6 +20,37 @@ module Appsignal
16
20
  end
17
21
  end
18
22
 
23
+ # Records an `enqueue.delayed_job` event so the enqueue shows up under the
24
+ # active transaction (e.g. when enqueuing from within a web request or
25
+ # another job). An enqueue with no active transaction is a transparent
26
+ # pass-through.
27
+ def self.enqueue_with_instrumentation(job, block)
28
+ # Under Active Job the enqueue is already recorded as an
29
+ # `enqueue.active_job` event, so skip recording it again here.
30
+ if Appsignal::Transaction.current? &&
31
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
32
+ return block.call(job)
33
+ end
34
+
35
+ Appsignal.instrument("enqueue.delayed_job", "enqueue #{enqueue_name(job)} job") do
36
+ block.call(job)
37
+ end
38
+ end
39
+
40
+ # Titles the enqueue event after the job. The `appsignal_name` override is
41
+ # honored verbatim, as it is when naming the perform action. That override
42
+ # is a full action name, so an enqueue that uses it reads as
43
+ # `enqueue Class#method job` rather than the bare `enqueue Class job`. We
44
+ # accept that inconsistency so the enqueue and perform events stay tied to
45
+ # the same name for the rare job that sets it.
46
+ def self.enqueue_name(job)
47
+ payload = job.payload_object
48
+ appsignal_name = extract_value(payload, :appsignal_name, nil)
49
+ return appsignal_name if appsignal_name.is_a?(String)
50
+
51
+ job.name
52
+ end
53
+
19
54
  def self.invoke_with_instrumentation(job, block)
20
55
  transaction =
21
56
  Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
@@ -24,7 +59,7 @@ module Appsignal
24
59
  Appsignal.instrument("perform_job.delayed_job") do
25
60
  block.call(job)
26
61
  end
27
- rescue Exception => error # rubocop:disable Lint/RescueException
62
+ rescue Exception => error
28
63
  transaction.set_error(error)
29
64
  raise
30
65
  ensure
@@ -63,6 +98,7 @@ module Appsignal
63
98
  "#{default_name}#perform"
64
99
  end
65
100
 
101
+ # rubocop:disable Style/OptionalBooleanParameter
66
102
  def self.extract_value(object_or_hash, field, default_value = nil, convert_to_s = false)
67
103
  value = nil
68
104
 
@@ -87,6 +123,7 @@ module Appsignal
87
123
  value
88
124
  end
89
125
  end
126
+ # rubocop:enable Style/OptionalBooleanParameter
90
127
  end
91
128
  end
92
129
  end
@@ -5,6 +5,14 @@ module Appsignal
5
5
  # @!visibility private
6
6
  module ExconIntegration
7
7
  def self.instrument(name, data, &block)
8
+ # Skip when an outer HTTP client integration (Faraday) already records
9
+ # this request, so it isn't instrumented twice. Excon calls the
10
+ # instrumentor for block-less notifications too, hence the `block_given?`.
11
+ if Appsignal::Transaction.current? &&
12
+ Appsignal::Transaction.current.http_client_events_suppressed?
13
+ return block_given? ? yield : nil
14
+ end
15
+
8
16
  namespace, *event = name.split(".")
9
17
  rails_name = [event, namespace].flatten.join(".")
10
18
 
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Integrations
5
+ # Faraday middleware that records each request as a `request.faraday` event
6
+ # and suppresses the downstream HTTP client's own instrumentation, so the
7
+ # request is recorded once rather than as nested Faraday + Net::HTTP (or
8
+ # Excon) client events.
9
+ #
10
+ # @!visibility private
11
+ class FaradayMiddleware < ::Faraday::Middleware
12
+ def call(env)
13
+ http_method = env[:method].to_s.upcase
14
+ uri = env[:url]
15
+ # Title only, no body: the path is left out so the event matches
16
+ # Net::HTTP's (scheme and host only), keeping paths out of event titles.
17
+ Appsignal.instrument(
18
+ "request.faraday",
19
+ "#{http_method} #{uri.scheme}://#{uri.host}"
20
+ ) do
21
+ # Faraday's default adapter is Net::HTTP, which AppSignal also
22
+ # instruments. Suppress the adapter's own instrumentation so the
23
+ # request appears once (as the Faraday event) rather than as nested
24
+ # Faraday + Net::HTTP client events.
25
+ if Appsignal::Transaction.current?
26
+ Appsignal::Transaction.current.suppress_http_client_events { @app.call(env) }
27
+ else
28
+ @app.call(env)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ # Prepended to `Faraday::RackBuilder#adapter`, the single point every
35
+ # connection passes through as it finishes building its middleware stack.
36
+ # Faraday has no global default middleware stack (unlike Excon), so patching
37
+ # the build path is the only way to instrument every connection automatically.
38
+ #
39
+ # Just before the adapter (the innermost handler, where the request is sent)
40
+ # it inserts `FaradayMiddleware`, which records the `request.faraday` event
41
+ # and suppresses the downstream client. Skipped if it's already present.
42
+ #
43
+ # @!visibility private
44
+ module FaradayRackBuilderPatch
45
+ def adapter(*)
46
+ use(FaradayMiddleware) unless handlers.any? { |handler| handler.klass == FaradayMiddleware }
47
+ super
48
+ end
49
+ end
50
+ end
51
+ end
@@ -12,6 +12,15 @@ module Appsignal
12
12
  Appsignal.instrument("request.http_rb", "#{verb.upcase} #{request_uri}", &block)
13
13
  end
14
14
 
15
+ # The event is recorded at the request boundary, so a redirected request
16
+ # stays a single `request.http_rb` event spanning every hop. That boundary
17
+ # lives in more than one place: a bare request runs through
18
+ # `HTTP::Client#request`, but in http6 a chained request (`.follow`,
19
+ # `.headers`, `.timeout`, ...) runs through `HTTP::Session#request`
20
+ # instead, which never touches `Client#request`. The hook prepends one of
21
+ # these onto each. `Client#request` takes positional options in http5 and
22
+ # keyword options in http6; `Session#request` (http6 only) takes keyword
23
+ # options.
15
24
  module HashOptions
16
25
  def request(verb, uri, opts = {})
17
26
  HttpIntegration.instrument(verb, uri) { super }
@@ -46,11 +46,13 @@ module Appsignal
46
46
  store = transaction.store("mongo_driver")
47
47
  command = store.delete(event.request_id) || {}
48
48
 
49
- # Finish the event in the extension.
49
+ # Finish the event. The sanitized command is a (nested) Hash; emit it
50
+ # as a JSON string. The agent serializes structured bodies to JSON
51
+ # anyway, so this is equivalent output.
50
52
  transaction.finish_event(
51
53
  "query.mongodb",
52
54
  "#{event.command_name} | #{event.database_name} | #{result}",
53
- Appsignal::Utils::Data.generate(command),
55
+ Appsignal::Utils::JSON.generate(command),
54
56
  Appsignal::EventFormatter::DEFAULT
55
57
  )
56
58
 
@@ -5,6 +5,13 @@ module Appsignal
5
5
  # @!visibility private
6
6
  module NetHttpIntegration
7
7
  def request(request, body = nil, &block)
8
+ # Skip when an outer HTTP client integration (Faraday) already records
9
+ # this request, so it isn't instrumented twice.
10
+ if Appsignal::Transaction.current? &&
11
+ Appsignal::Transaction.current.http_client_events_suppressed?
12
+ return super
13
+ end
14
+
8
15
  Appsignal.instrument(
9
16
  "request.net_http",
10
17
  "#{request.method} #{use_ssl? ? "https" : "http"}://#{request["host"] || address}"
@@ -27,9 +27,9 @@ class Object
27
27
  end
28
28
  end
29
29
 
30
- if singleton_class.respond_to?(:ruby2_keywords, true) # rubocop:disable Style/GuardClause
31
- singleton_class.send(:ruby2_keywords, method_name)
32
- end
30
+ return unless singleton_class.respond_to?(:ruby2_keywords, true)
31
+
32
+ singleton_class.send(:ruby2_keywords, method_name)
33
33
  end
34
34
 
35
35
  # Instruments an instance method with AppSignal monitoring.
@@ -10,7 +10,7 @@ module Appsignal
10
10
 
11
11
  begin
12
12
  Appsignal.instrument("perform_job.que") { super }
13
- rescue Exception => error # rubocop:disable Lint/RescueException
13
+ rescue Exception => error
14
14
  transaction.set_error(error)
15
15
  raise error
16
16
  ensure
@@ -34,5 +34,72 @@ module Appsignal
34
34
  end
35
35
  end
36
36
  end
37
+
38
+ # @!visibility private
39
+ #
40
+ # Prepended to `Que::Job`'s singleton so it records each enqueue as an
41
+ # `enqueue.que` event under the active transaction. Like all AppSignal
42
+ # events, it only records when there's an active transaction (e.g. enqueuing
43
+ # from within a web request or another job); otherwise it's a transparent
44
+ # pass-through.
45
+ module QueClientPlugin
46
+ def enqueue(*_args, job_options: {}, **_rest)
47
+ # Inside a `bulk_enqueue` block the batch is recorded once by the
48
+ # `bulk_enqueue` wrapper, so each inner enqueue is a pass-through to
49
+ # avoid recording an event per job.
50
+ return super if Thread.current[:appsignal_que_bulk_enqueue]
51
+
52
+ # Under Active Job the enqueue is already recorded as an
53
+ # `enqueue.active_job` event, so skip recording it again here.
54
+ return super if Appsignal::Transaction.current? &&
55
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
56
+
57
+ # Resolve the job class the way Que does: an explicit `:job_class`, else
58
+ # the class `enqueue` was called on.
59
+ title = "enqueue #{job_options[:job_class] || name} job"
60
+ Appsignal.instrument("enqueue.que", title) { super }
61
+ end
62
+ end
63
+
64
+ # @!visibility private
65
+ #
66
+ # `bulk_enqueue` exists only on Que 2+, so this lives in its own module that
67
+ # the hook prepends only when Que has the method -- otherwise we'd define a
68
+ # `bulk_enqueue` on Que versions that have none. The whole batch records a
69
+ # single `bulk_enqueue.que` event; the inner enqueues are pass-throughs.
70
+ module QueBulkClientPlugin
71
+ def bulk_enqueue(*_args, job_options: {}, **_rest)
72
+ # Under Active Job the enqueue is already recorded as an
73
+ # `enqueue.active_job` event, so skip recording it again here.
74
+ return super if Appsignal::Transaction.current? &&
75
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
76
+
77
+ Appsignal.instrument("bulk_enqueue.que", bulk_enqueue_title(job_options)) do
78
+ # Flag the batch so the enqueues this block triggers pass through
79
+ # without recording, without reading Que's internal bulk state.
80
+ was_bulk = Thread.current[:appsignal_que_bulk_enqueue]
81
+ Thread.current[:appsignal_que_bulk_enqueue] = true
82
+ begin
83
+ super
84
+ ensure
85
+ Thread.current[:appsignal_que_bulk_enqueue] = was_bulk
86
+ end
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ # The batch's job class is known up front only from an explicit
93
+ # `:job_class` or when `bulk_enqueue` is called on a concrete subclass;
94
+ # called on `Que::Job` itself the class isn't known until the inner
95
+ # enqueues run, so the title is left class-less.
96
+ def bulk_enqueue_title(job_options)
97
+ job_class = job_options[:job_class]
98
+ job_class ||= name unless equal?(::Que::Job)
99
+ return "bulk enqueue jobs" unless job_class
100
+
101
+ "bulk enqueue #{job_class} jobs"
102
+ end
103
+ end
37
104
  end
38
105
  end
@@ -25,7 +25,7 @@ module Appsignal
25
25
  Appsignal.instrument "task.rake" do
26
26
  super
27
27
  end
28
- rescue Exception => error # rubocop:disable Lint/RescueException
28
+ rescue Exception => error
29
29
  Appsignal::Integrations::RakeIntegrationHelper.register_at_exit_hook
30
30
  unless RakeIntegration.ignored_error?(error)
31
31
  transaction ||= _appsignal_create_transaction
@@ -10,7 +10,7 @@ module Appsignal
10
10
  Appsignal.instrument "perform.resque" do
11
11
  super
12
12
  end
13
- rescue Exception => exception # rubocop:disable Lint/RescueException
13
+ rescue Exception => exception
14
14
  transaction.set_error(exception)
15
15
  raise exception
16
16
  ensure
@@ -25,6 +25,25 @@ module Appsignal
25
25
  end
26
26
  end
27
27
 
28
+ # Wraps `Resque.push` to record an `enqueue.resque` event so the enqueue
29
+ # shows up under the active transaction.
30
+ #
31
+ # Like all AppSignal events, this only records when there's an active
32
+ # transaction (e.g. enqueuing from within a web request or another job).
33
+ # An enqueue with no transaction is a transparent pass-through.
34
+ #
35
+ # @!visibility private
36
+ module ResquePushIntegration
37
+ def push(_queue, item)
38
+ # Under Active Job the enqueue is already recorded as an
39
+ # `enqueue.active_job` event, so skip recording it again here.
40
+ return super if Appsignal::Transaction.current? &&
41
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
42
+
43
+ Appsignal.instrument("enqueue.resque", "enqueue #{item["class"]} job") { super }
44
+ end
45
+ end
46
+
28
47
  # @!visibility private
29
48
  class ResqueHelpers
30
49
  def self.arguments(payload)
@@ -8,7 +8,7 @@ module Appsignal
8
8
  transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
9
9
 
10
10
  Appsignal.instrument("perform_job.shoryuken", &block)
11
- rescue Exception => error # rubocop:disable Lint/RescueException
11
+ rescue Exception => error
12
12
  transaction.set_error(error)
13
13
  raise
14
14
  ensure
@@ -71,5 +71,37 @@ module Appsignal
71
71
  end
72
72
  end
73
73
  end
74
+
75
+ # Shoryuken client middleware that records an `enqueue.shoryuken` event so
76
+ # the enqueue shows up under the active transaction.
77
+ #
78
+ # Like all AppSignal events, this only records when there's an active
79
+ # transaction (e.g. enqueuing from within a web request or another job). An
80
+ # enqueue with no transaction is a transparent pass-through.
81
+ #
82
+ # @!visibility private
83
+ class ShoryukenClientMiddleware
84
+ def call(options, &block)
85
+ # Under Active Job the enqueue is already recorded as an
86
+ # `enqueue.active_job` event, so skip recording it again here.
87
+ return yield if Appsignal::Transaction.current? &&
88
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
89
+
90
+ Appsignal.instrument("enqueue.shoryuken", enqueue_title(options), &block)
91
+ end
92
+
93
+ private
94
+
95
+ # Enqueues through a Shoryuken worker carry the worker class in the
96
+ # `shoryuken_class` message attribute. Raw `send_message` enqueues don't,
97
+ # so there's no worker class to name -- fall back to the queue instead.
98
+ def enqueue_title(options)
99
+ worker_class = options.dig(:message_attributes, "shoryuken_class", :string_value)
100
+ return "enqueue #{worker_class} job" if worker_class
101
+
102
+ queue = options[:queue_url].to_s.split("/").last
103
+ "enqueue on #{queue}"
104
+ end
105
+ end
74
106
  end
75
107
  end