appsignal 4.8.5-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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/README.md +2 -2
  4. data/SUPPORT.md +1 -1
  5. data/appsignal.gemspec +2 -2
  6. data/build_matrix.yml +35 -0
  7. data/ext/agent.rb +27 -27
  8. data/ext/base.rb +3 -7
  9. data/ext/extconf.rb +1 -1
  10. data/lib/appsignal/check_in/event.rb +5 -4
  11. data/lib/appsignal/cli/diagnose.rb +1 -1
  12. data/lib/appsignal/cli/helpers.rb +2 -2
  13. data/lib/appsignal/cli/install.rb +3 -3
  14. data/lib/appsignal/config.rb +44 -1
  15. data/lib/appsignal/environment.rb +1 -0
  16. data/lib/appsignal/extension/jruby.rb +6 -5
  17. data/lib/appsignal/extension.rb +1 -1
  18. data/lib/appsignal/helpers/instrumentation.rb +12 -12
  19. data/lib/appsignal/hooks/action_cable.rb +2 -2
  20. data/lib/appsignal/hooks/active_job.rb +42 -3
  21. data/lib/appsignal/hooks/delayed_job.rb +2 -1
  22. data/lib/appsignal/hooks/excon.rb +1 -1
  23. data/lib/appsignal/hooks/faraday.rb +21 -0
  24. data/lib/appsignal/hooks/http.rb +9 -0
  25. data/lib/appsignal/hooks/mongo_ruby_driver.rb +2 -1
  26. data/lib/appsignal/hooks/que.rb +7 -1
  27. data/lib/appsignal/hooks/resque.rb +5 -1
  28. data/lib/appsignal/hooks/shoryuken.rb +15 -1
  29. data/lib/appsignal/hooks/sidekiq.rb +14 -1
  30. data/lib/appsignal/hooks.rb +1 -0
  31. data/lib/appsignal/integrations/action_cable.rb +1 -1
  32. data/lib/appsignal/integrations/active_support_notifications.rb +19 -6
  33. data/lib/appsignal/integrations/delayed_job_plugin.rb +38 -1
  34. data/lib/appsignal/integrations/excon.rb +8 -0
  35. data/lib/appsignal/integrations/faraday.rb +51 -0
  36. data/lib/appsignal/integrations/http.rb +9 -0
  37. data/lib/appsignal/integrations/mongo_ruby_driver.rb +4 -2
  38. data/lib/appsignal/integrations/net_http.rb +7 -0
  39. data/lib/appsignal/integrations/object.rb +3 -3
  40. data/lib/appsignal/integrations/que.rb +68 -1
  41. data/lib/appsignal/integrations/rake.rb +1 -1
  42. data/lib/appsignal/integrations/resque.rb +20 -1
  43. data/lib/appsignal/integrations/shoryuken.rb +33 -1
  44. data/lib/appsignal/integrations/sidekiq.rb +73 -36
  45. data/lib/appsignal/integrations/webmachine.rb +1 -1
  46. data/lib/appsignal/logger.rb +4 -1
  47. data/lib/appsignal/rack/abstract_middleware.rb +1 -1
  48. data/lib/appsignal/rack/body_wrapper.rb +5 -5
  49. data/lib/appsignal/rack/hanami_middleware.rb +1 -0
  50. data/lib/appsignal/sample_data.rb +1 -0
  51. data/lib/appsignal/transaction.rb +59 -11
  52. data/lib/appsignal/utils/query_params_sanitizer.rb +2 -0
  53. data/lib/appsignal/version.rb +1 -1
  54. data/lib/appsignal.rb +2 -2
  55. data/lib/puma/plugin/appsignal.rb +1 -1
  56. data/sig/appsignal.rbi +41 -1
  57. data/sig/appsignal.rbs +30 -0
  58. metadata +7 -6
  59. data/lib/appsignal/event_formatter/faraday/request_formatter.rb +0 -24
@@ -84,7 +84,7 @@ module Appsignal
84
84
  #
85
85
  # This class inherits from the {Data} class so that it passes type checks.
86
86
  class MockData < Data
87
- def initialize(*_args)
87
+ def initialize(*_args) # rubocop:disable Lint/MissingSuper
88
88
  # JRuby extension requirement, as it sends a pointer to the Data object
89
89
  # when creating it
90
90
  end
@@ -138,7 +138,7 @@ module Appsignal
138
138
 
139
139
  begin
140
140
  yield if block_given?
141
- rescue Exception => error # rubocop:disable Lint/RescueException
141
+ rescue Exception => error
142
142
  transaction.set_error(error)
143
143
  raise error
144
144
  ensure
@@ -242,7 +242,7 @@ module Appsignal
242
242
 
243
243
  transaction.complete
244
244
  end
245
- alias :send_exception :send_error
245
+ alias send_exception send_error
246
246
 
247
247
  # Set an error on the current transaction.
248
248
  #
@@ -305,8 +305,8 @@ module Appsignal
305
305
  transaction.set_error(exception)
306
306
  yield transaction if block_given?
307
307
  end
308
- alias :set_exception :set_error
309
- alias :add_exception :set_error
308
+ alias set_exception set_error
309
+ alias add_exception set_error
310
310
 
311
311
  # Report an error to AppSignal.
312
312
  #
@@ -377,7 +377,7 @@ module Appsignal
377
377
 
378
378
  transaction.complete unless has_parent_transaction
379
379
  end
380
- alias :report_exception :report_error
380
+ alias report_exception report_error
381
381
 
382
382
  # Set a custom action name for the current transaction.
383
383
  #
@@ -497,7 +497,7 @@ module Appsignal
497
497
  transaction = Appsignal::Transaction.current
498
498
  transaction.add_custom_data(data)
499
499
  end
500
- alias :set_custom_data :add_custom_data
500
+ alias set_custom_data add_custom_data
501
501
 
502
502
  # Add tags to the current transaction.
503
503
  #
@@ -542,9 +542,9 @@ module Appsignal
542
542
  transaction = Appsignal::Transaction.current
543
543
  transaction.add_tags(tags)
544
544
  end
545
- alias :tag_request :add_tags
546
- alias :tag_job :add_tags
547
- alias :set_tags :add_tags
545
+ alias tag_request add_tags
546
+ alias tag_job add_tags
547
+ alias set_tags add_tags
548
548
 
549
549
  # Add parameters to the current transaction.
550
550
  #
@@ -600,7 +600,7 @@ module Appsignal
600
600
  transaction = Appsignal::Transaction.current
601
601
  transaction.add_params(params, &block)
602
602
  end
603
- alias :set_params :add_params
603
+ alias set_params add_params
604
604
 
605
605
  # Mark the parameters sample data to be set as an empty value.
606
606
  #
@@ -666,7 +666,7 @@ module Appsignal
666
666
  transaction = Appsignal::Transaction.current
667
667
  transaction.add_session_data(session_data, &block)
668
668
  end
669
- alias :set_session_data :add_session_data
669
+ alias set_session_data add_session_data
670
670
 
671
671
  # Add request headers to the current transaction.
672
672
  #
@@ -708,7 +708,7 @@ module Appsignal
708
708
  transaction = Appsignal::Transaction.current
709
709
  transaction.add_headers(headers, &block)
710
710
  end
711
- alias :set_headers :add_headers
711
+ alias set_headers add_headers
712
712
 
713
713
  # Add breadcrumbs to the transaction.
714
714
  #
@@ -43,7 +43,7 @@ module Appsignal
43
43
  Appsignal.instrument "subscribed.action_cable" do
44
44
  inner.call
45
45
  end
46
- rescue Exception => exception # rubocop:disable Lint/RescueException
46
+ rescue Exception => exception
47
47
  transaction.set_error(exception)
48
48
  raise exception
49
49
  ensure
@@ -79,7 +79,7 @@ module Appsignal
79
79
  Appsignal.instrument "unsubscribed.action_cable" do
80
80
  inner.call
81
81
  end
82
- rescue Exception => exception # rubocop:disable Lint/RescueException
82
+ rescue Exception => exception
83
83
  transaction.set_error(exception)
84
84
  raise exception
85
85
  ensure
@@ -22,13 +22,16 @@ module Appsignal
22
22
  end
23
23
 
24
24
  def dependencies_present?
25
- self.class.dependencies_present?
25
+ self.class.dependencies_present? && Appsignal.config &&
26
+ Appsignal.config[:instrument_active_job]
26
27
  end
27
28
 
28
29
  def install
29
30
  ActiveSupport.on_load(:active_job) do
30
31
  ::ActiveJob::Base
31
32
  .extend ::Appsignal::Hooks::ActiveJobHook::ActiveJobClassInstrumentation
33
+ ::ActiveJob::Base
34
+ .prepend ::Appsignal::Hooks::ActiveJobHook::ActiveJobEnqueueInstrumentation
32
35
 
33
36
  next unless Appsignal::Hooks::ActiveJobHook.version_7_1_or_higher?
34
37
 
@@ -41,8 +44,44 @@ module Appsignal
41
44
  end
42
45
  end
43
46
 
47
+ # Records an `enqueue.active_job` event when a job is enqueued, so the
48
+ # enqueue shows up on the active transaction's timeline (e.g. when
49
+ # enqueuing from within a web request or another job).
50
+ #
51
+ # Wrapping `enqueue` ourselves -- rather than relying on Rails' native
52
+ # `enqueue.active_job` notification, which the AppSignal notifications
53
+ # path now suppresses -- gives us a single event we own. Like all
54
+ # AppSignal events, this only records when there's an active transaction;
55
+ # an enqueue with no transaction is a transparent pass-through.
56
+ #
57
+ # @!visibility private
58
+ module ActiveJobEnqueueInstrumentation
59
+ def enqueue(*, **)
60
+ # Skip recording the event when enqueue events are suppressed. That is
61
+ # the case when enqueue instrumentation is disabled, and it keeps this
62
+ # integration consistent with the standalone adapters (Sidekiq, ...),
63
+ # which already gate their own enqueue event on this check.
64
+ if Appsignal::Transaction.current? &&
65
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
66
+ return super
67
+ end
68
+
69
+ Appsignal.instrument("enqueue.active_job", "enqueue #{self.class.name} job") do
70
+ # Active Job enqueues through an adapter (Sidekiq, Resque, ...) that
71
+ # has its own enqueue instrumentation. Suppress it so the enqueue is
72
+ # recorded once, as this event, rather than as nested Active Job +
73
+ # adapter events.
74
+ if Appsignal::Transaction.current?
75
+ Appsignal::Transaction.current.suppress_job_enqueue_events { super }
76
+ else
77
+ super
78
+ end
79
+ end
80
+ end
81
+ end
82
+
44
83
  module ActiveJobClassInstrumentation
45
- def execute(job) # rubocop:disable Metrics/CyclomaticComplexity
84
+ def execute(job)
46
85
  enqueued_at = job["enqueued_at"]
47
86
  queue_start = Time.parse(enqueued_at) if enqueued_at
48
87
  queue_time =
@@ -75,7 +114,7 @@ module Appsignal
75
114
  transaction.set_action(ActiveJobHelpers.action_name(job))
76
115
 
77
116
  super
78
- rescue Exception => exception # rubocop:disable Lint/RescueException
117
+ rescue Exception => exception
79
118
  job_status = :failed
80
119
  transaction_set_error(transaction, exception)
81
120
  raise exception
@@ -7,7 +7,8 @@ module Appsignal
7
7
  register :delayed_job
8
8
 
9
9
  def dependencies_present?
10
- defined?(::Delayed::Plugin)
10
+ defined?(::Delayed::Plugin) && Appsignal.config &&
11
+ Appsignal.config[:instrument_delayed_job]
11
12
  end
12
13
 
13
14
  def install
@@ -7,7 +7,7 @@ module Appsignal
7
7
  register :excon
8
8
 
9
9
  def dependencies_present?
10
- Appsignal.config && defined?(::Excon)
10
+ Appsignal.config && defined?(::Excon) && Appsignal.config[:instrument_excon]
11
11
  end
12
12
 
13
13
  def install
@@ -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.