appsignal 3.4.13 → 3.6.1

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/.semaphore/semaphore.yml +180 -14
  3. data/CHANGELOG.md +164 -0
  4. data/README.md +2 -0
  5. data/Rakefile +3 -1
  6. data/build_matrix.yml +7 -13
  7. data/ext/Rakefile +8 -1
  8. data/ext/agent.rb +27 -27
  9. data/ext/appsignal_extension.c +0 -24
  10. data/ext/base.rb +4 -1
  11. data/gemfiles/redis-4.gemfile +5 -0
  12. data/gemfiles/redis-5.gemfile +6 -0
  13. data/lib/appsignal/cli/diagnose/paths.rb +33 -10
  14. data/lib/appsignal/cli/diagnose.rb +6 -1
  15. data/lib/appsignal/config.rb +19 -5
  16. data/lib/appsignal/demo.rb +1 -1
  17. data/lib/appsignal/environment.rb +24 -13
  18. data/lib/appsignal/event_formatter.rb +1 -1
  19. data/lib/appsignal/extension/jruby.rb +4 -17
  20. data/lib/appsignal/extension.rb +1 -1
  21. data/lib/appsignal/helpers/instrumentation.rb +10 -10
  22. data/lib/appsignal/helpers/metrics.rb +15 -13
  23. data/lib/appsignal/hooks/active_job.rb +9 -1
  24. data/lib/appsignal/hooks/redis.rb +1 -0
  25. data/lib/appsignal/hooks/redis_client.rb +27 -0
  26. data/lib/appsignal/hooks.rb +3 -2
  27. data/lib/appsignal/integrations/hanami.rb +1 -1
  28. data/lib/appsignal/integrations/padrino.rb +1 -1
  29. data/lib/appsignal/integrations/railtie.rb +1 -1
  30. data/lib/appsignal/integrations/redis_client.rb +20 -0
  31. data/lib/appsignal/integrations/sidekiq.rb +2 -2
  32. data/lib/appsignal/integrations/sinatra.rb +1 -1
  33. data/lib/appsignal/logger.rb +2 -0
  34. data/lib/appsignal/minutely.rb +4 -4
  35. data/lib/appsignal/probes/gvl.rb +1 -1
  36. data/lib/appsignal/probes/helpers.rb +1 -1
  37. data/lib/appsignal/probes/mri.rb +1 -1
  38. data/lib/appsignal/probes/sidekiq.rb +10 -8
  39. data/lib/appsignal/rack/body_wrapper.rb +161 -0
  40. data/lib/appsignal/rack/generic_instrumentation.rb +18 -5
  41. data/lib/appsignal/rack/rails_instrumentation.rb +17 -5
  42. data/lib/appsignal/rack/sinatra_instrumentation.rb +17 -5
  43. data/lib/appsignal/rack/streaming_listener.rb +27 -36
  44. data/lib/appsignal/span.rb +2 -2
  45. data/lib/appsignal/transaction.rb +46 -10
  46. data/lib/appsignal/utils/deprecation_message.rb +2 -2
  47. data/lib/appsignal/version.rb +1 -1
  48. data/lib/appsignal.rb +38 -31
  49. data/resources/cacert.pem +321 -159
  50. data/spec/lib/appsignal/cli/diagnose/utils_spec.rb +11 -0
  51. data/spec/lib/appsignal/cli/diagnose_spec.rb +38 -12
  52. data/spec/lib/appsignal/config_spec.rb +3 -2
  53. data/spec/lib/appsignal/hooks/activejob_spec.rb +26 -1
  54. data/spec/lib/appsignal/hooks/redis_client_spec.rb +222 -0
  55. data/spec/lib/appsignal/hooks/redis_spec.rb +98 -76
  56. data/spec/lib/appsignal/hooks_spec.rb +4 -4
  57. data/spec/lib/appsignal/integrations/railtie_spec.rb +2 -2
  58. data/spec/lib/appsignal/integrations/sidekiq_spec.rb +3 -3
  59. data/spec/lib/appsignal/integrations/sinatra_spec.rb +2 -2
  60. data/spec/lib/appsignal/minutely_spec.rb +2 -2
  61. data/spec/lib/appsignal/probes/sidekiq_spec.rb +29 -6
  62. data/spec/lib/appsignal/rack/body_wrapper_spec.rb +220 -0
  63. data/spec/lib/appsignal/rack/generic_instrumentation_spec.rb +3 -2
  64. data/spec/lib/appsignal/rack/rails_instrumentation_spec.rb +5 -3
  65. data/spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb +3 -1
  66. data/spec/lib/appsignal/rack/streaming_listener_spec.rb +9 -53
  67. data/spec/lib/appsignal/transaction_spec.rb +95 -2
  68. data/spec/lib/appsignal_spec.rb +62 -60
  69. data/spec/spec_helper.rb +1 -1
  70. data/spec/support/fixtures/projects/valid/config/appsignal.yml +3 -3
  71. data/spec/support/helpers/config_helpers.rb +6 -2
  72. data/spec/support/helpers/dependency_helper.rb +9 -1
  73. data/spec/support/helpers/log_helpers.rb +2 -2
  74. metadata +9 -2
@@ -10,22 +10,24 @@ module Appsignal
10
10
  Appsignal::Utils::Data.generate(tags)
11
11
  )
12
12
  rescue RangeError
13
- Appsignal.logger
13
+ Appsignal.internal_logger
14
14
  .warn("Gauge value #{value} for key '#{key}' is too big")
15
15
  end
16
16
 
17
- def set_host_gauge(key, value)
18
- Appsignal::Extension.set_host_gauge(key.to_s, value.to_f)
19
- rescue RangeError
20
- Appsignal.logger
21
- .warn("Host gauge value #{value} for key '#{key}' is too big")
17
+ def set_host_gauge(_key, _value)
18
+ Appsignal::Utils::DeprecationMessage.message \
19
+ "The `set_host_gauge` method has been deprecated. " \
20
+ "Calling this method has no effect. " \
21
+ "Please remove method call in the following file to remove " \
22
+ "this message.\n#{caller.first}"
22
23
  end
23
24
 
24
- def set_process_gauge(key, value)
25
- Appsignal::Extension.set_process_gauge(key.to_s, value.to_f)
26
- rescue RangeError
27
- Appsignal.logger
28
- .warn("Process gauge value #{value} for key '#{key}' is too big")
25
+ def set_process_gauge(_key, _value)
26
+ Appsignal::Utils::DeprecationMessage.message \
27
+ "The `set_process_gauge` method has been deprecated. " \
28
+ "Calling this method has no effect. " \
29
+ "Please remove method call in the following file to remove " \
30
+ "this message.\n#{caller.first}"
29
31
  end
30
32
 
31
33
  def increment_counter(key, value = 1.0, tags = {})
@@ -35,7 +37,7 @@ module Appsignal
35
37
  Appsignal::Utils::Data.generate(tags)
36
38
  )
37
39
  rescue RangeError
38
- Appsignal.logger
40
+ Appsignal.internal_logger
39
41
  .warn("Counter value #{value} for key '#{key}' is too big")
40
42
  end
41
43
 
@@ -46,7 +48,7 @@ module Appsignal
46
48
  Appsignal::Utils::Data.generate(tags)
47
49
  )
48
50
  rescue RangeError
49
- Appsignal.logger
51
+ Appsignal.internal_logger
50
52
  .warn("Distribution value #{value} for key '#{key}' is too big")
51
53
  end
52
54
  end
@@ -56,7 +56,7 @@ module Appsignal
56
56
  super
57
57
  rescue Exception => exception # rubocop:disable Lint/RescueException
58
58
  job_status = :failed
59
- transaction.set_error(exception)
59
+ transaction_set_error(transaction, exception)
60
60
  raise exception
61
61
  ensure
62
62
  if transaction
@@ -82,6 +82,14 @@ module Appsignal
82
82
  tags.merge(:status => :processed)
83
83
  end
84
84
  end
85
+
86
+ private
87
+
88
+ def transaction_set_error(transaction, exception)
89
+ return if Appsignal.config[:activejob_report_errors] == "none"
90
+
91
+ transaction.set_error(exception)
92
+ end
85
93
  end
86
94
 
87
95
  module ActiveJobHelpers
@@ -8,6 +8,7 @@ module Appsignal
8
8
 
9
9
  def dependencies_present?
10
10
  defined?(::Redis) &&
11
+ !defined?(::RedisClient) &&
11
12
  Appsignal.config &&
12
13
  Appsignal.config[:instrument_redis]
13
14
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ class Hooks
5
+ # @api private
6
+ class RedisClientHook < Appsignal::Hooks::Hook
7
+ register :redis_client
8
+
9
+ def dependencies_present?
10
+ defined?(::RedisClient) &&
11
+ Appsignal.config &&
12
+ Appsignal.config[:instrument_redis]
13
+ end
14
+
15
+ def install
16
+ require "appsignal/integrations/redis_client"
17
+ ::RedisClient::RubyConnection.prepend Appsignal::Integrations::RedisClientIntegration
18
+ Appsignal::Environment.report_enabled("redis")
19
+
20
+ return unless defined?(::RedisClient::HiredisConnection)
21
+
22
+ ::RedisClient::HiredisConnection.prepend Appsignal::Integrations::RedisClientIntegration
23
+ Appsignal::Environment.report_enabled("hiredis")
24
+ end
25
+ end
26
+ end
27
+ end
@@ -32,12 +32,12 @@ module Appsignal
32
32
  return unless dependencies_present?
33
33
  return if installed?
34
34
 
35
- Appsignal.logger.debug("Installing #{name} hook")
35
+ Appsignal.internal_logger.debug("Installing #{name} hook")
36
36
  begin
37
37
  install
38
38
  @installed = true
39
39
  rescue => ex
40
- logger = Appsignal.logger
40
+ logger = Appsignal.internal_logger
41
41
  logger.error("Error while installing #{name} hook: #{ex}")
42
42
  logger.debug ex.backtrace.join("\n")
43
43
  end
@@ -103,6 +103,7 @@ require "appsignal/hooks/passenger"
103
103
  require "appsignal/hooks/puma"
104
104
  require "appsignal/hooks/rake"
105
105
  require "appsignal/hooks/redis"
106
+ require "appsignal/hooks/redis_client"
106
107
  require "appsignal/hooks/resque"
107
108
  require "appsignal/hooks/sequel"
108
109
  require "appsignal/hooks/shoryuken"
@@ -6,7 +6,7 @@ module Appsignal
6
6
  module Integrations
7
7
  module HanamiPlugin
8
8
  def self.init
9
- Appsignal.logger.debug("Loading Hanami integration")
9
+ Appsignal.internal_logger.debug("Loading Hanami integration")
10
10
 
11
11
  hanami_app_config = ::Hanami.app.config
12
12
  Appsignal.config = Appsignal::Config.new(
@@ -7,7 +7,7 @@ module Appsignal
7
7
  # @api private
8
8
  module PadrinoPlugin
9
9
  def self.init
10
- Appsignal.logger.debug("Loading Padrino (#{Padrino::VERSION}) integration")
10
+ Appsignal.internal_logger.debug("Loading Padrino (#{Padrino::VERSION}) integration")
11
11
 
12
12
  root = Padrino.mounted_root
13
13
  Appsignal.config = Appsignal::Config.new(root, Padrino.env)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Appsignal.logger.debug("Loading Rails (#{Rails.version}) integration")
3
+ Appsignal.internal_logger.debug("Loading Rails (#{Rails.version}) integration")
4
4
 
5
5
  require "appsignal/utils/rails_helper"
6
6
  require "appsignal/rack/rails_instrumentation"
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Integrations
5
+ module RedisClientIntegration
6
+ def write(command)
7
+ sanitized_command =
8
+ if command[0] == :eval
9
+ "#{command[1]}#{" ?" * (command.size - 3)}"
10
+ else
11
+ "#{command[0]}#{" ?" * (command.size - 1)}"
12
+ end
13
+
14
+ Appsignal.instrument "query.redis", @config.id, sanitized_command do
15
+ super
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -9,7 +9,7 @@ module Appsignal
9
9
  #
10
10
  # @api private
11
11
  class SidekiqErrorHandler
12
- def call(exception, sidekiq_context)
12
+ def call(exception, sidekiq_context, _sidekiq_config = nil)
13
13
  transaction =
14
14
  if Appsignal::Transaction.current?
15
15
  Appsignal::Transaction.current
@@ -168,7 +168,7 @@ module Appsignal
168
168
  # Sidekiq issue #1761: in dev mode, it's possible to have jobs enqueued
169
169
  # which haven't been loaded into memory yet so the YAML can't be
170
170
  # loaded.
171
- Appsignal.logger.warn "Unable to load YAML: #{error.message}"
171
+ Appsignal.internal_logger.warn "Unable to load YAML: #{error.message}"
172
172
  default
173
173
  end
174
174
  end
@@ -3,7 +3,7 @@
3
3
  require "appsignal"
4
4
  require "appsignal/rack/sinatra_instrumentation"
5
5
 
6
- Appsignal.logger.debug("Loading Sinatra (#{Sinatra::VERSION}) integration")
6
+ Appsignal.internal_logger.debug("Loading Sinatra (#{Sinatra::VERSION}) integration")
7
7
 
8
8
  app_settings = ::Sinatra::Application.settings
9
9
  Appsignal.config = Appsignal::Config.new(
@@ -17,6 +17,8 @@ module Appsignal
17
17
  FATAL => 7
18
18
  }.freeze
19
19
 
20
+ attr_reader :level
21
+
20
22
  # Create a new logger instance
21
23
  #
22
24
  # @param group Name of the group for this logger.
@@ -108,7 +108,7 @@ module Appsignal
108
108
  attr_reader :probes
109
109
 
110
110
  def logger
111
- Appsignal.logger
111
+ Appsignal.internal_logger
112
112
  end
113
113
  end
114
114
 
@@ -132,7 +132,7 @@ module Appsignal
132
132
  sleep initial_wait_time
133
133
  initialize_probes
134
134
  loop do
135
- logger = Appsignal.logger
135
+ logger = Appsignal.internal_logger
136
136
  logger.debug("Gathering minutely metrics with #{probe_instances.count} probes")
137
137
  probe_instances.each do |name, probe|
138
138
  logger.debug("Gathering minutely metrics with '#{name}' probe")
@@ -181,13 +181,13 @@ module Appsignal
181
181
  klass = instance.class
182
182
  end
183
183
  unless dependencies_present?(klass)
184
- Appsignal.logger.debug "Skipping '#{name}' probe, " \
184
+ Appsignal.internal_logger.debug "Skipping '#{name}' probe, " \
185
185
  "#{klass}.dependency_present? returned falsy"
186
186
  return
187
187
  end
188
188
  probe_instances[name] = instance
189
189
  rescue => error
190
- logger = Appsignal.logger
190
+ logger = Appsignal.internal_logger
191
191
  logger.error "Error while initializing minutely probe '#{name}': #{error}"
192
192
  logger.debug error.backtrace.join("\n")
193
193
  end
@@ -22,7 +22,7 @@ module Appsignal
22
22
  end
23
23
 
24
24
  def initialize(appsignal: Appsignal, gvl_tools: ::GVLTools)
25
- Appsignal.logger.debug("Initializing GVL probe")
25
+ Appsignal.internal_logger.debug("Initializing GVL probe")
26
26
  @appsignal = appsignal
27
27
  @gvl_tools = gvl_tools
28
28
  end
@@ -47,7 +47,7 @@ module Appsignal
47
47
  # Auto detect hostname as fallback. May be inaccurate.
48
48
  @hostname =
49
49
  config[:hostname] || Socket.gethostname
50
- Appsignal.logger.debug "Probe helper: Using hostname config " \
50
+ Appsignal.internal_logger.debug "Probe helper: Using hostname config " \
51
51
  "option '#{@hostname.inspect}' as hostname"
52
52
 
53
53
  @hostname
@@ -11,7 +11,7 @@ module Appsignal
11
11
  end
12
12
 
13
13
  def initialize(appsignal: Appsignal, gc_profiler: Appsignal::GarbageCollection.profiler)
14
- Appsignal.logger.debug("Initializing VM probe")
14
+ Appsignal.internal_logger.debug("Initializing VM probe")
15
15
  @appsignal = appsignal
16
16
  @gc_profiler = gc_profiler
17
17
  end
@@ -59,7 +59,7 @@ module Appsignal
59
59
  @adapter = is_sidekiq7 ? Sidekiq7Adapter : Sidekiq6Adapter
60
60
 
61
61
  config_string = " with config: #{config}" unless config.empty?
62
- Appsignal.logger.debug("Initializing Sidekiq probe#{config_string}")
62
+ Appsignal.internal_logger.debug("Initializing Sidekiq probe#{config_string}")
63
63
  require "sidekiq/api"
64
64
  end
65
65
 
@@ -78,9 +78,9 @@ module Appsignal
78
78
  redis_info = adapter.redis_info
79
79
  return unless redis_info
80
80
 
81
- gauge "connection_count", redis_info.fetch("connected_clients")
82
- gauge "memory_usage", redis_info.fetch("used_memory")
83
- gauge "memory_usage_rss", redis_info.fetch("used_memory_rss")
81
+ gauge "connection_count", redis_info["connected_clients"]
82
+ gauge "memory_usage", redis_info["used_memory"]
83
+ gauge "memory_usage_rss", redis_info["used_memory_rss"]
84
84
  end
85
85
 
86
86
  def track_stats
@@ -112,6 +112,8 @@ module Appsignal
112
112
 
113
113
  # Track a gauge metric with the `sidekiq_` prefix
114
114
  def gauge(key, value, tags = {})
115
+ return if value.nil?
116
+
115
117
  tags[:hostname] = hostname if hostname
116
118
  Appsignal.set_gauge "sidekiq_#{key}", value, tags
117
119
  end
@@ -121,14 +123,14 @@ module Appsignal
121
123
 
122
124
  if config.key?(:hostname)
123
125
  @hostname = config[:hostname]
124
- Appsignal.logger.debug "Sidekiq probe: Using hostname config " \
125
- "option #{@hostname.inspect} as hostname"
126
+ Appsignal.internal_logger.debug "Sidekiq probe: Using hostname " \
127
+ "config option #{@hostname.inspect} as hostname"
126
128
  return @hostname
127
129
  end
128
130
 
129
131
  host = adapter.hostname
130
- Appsignal.logger.debug "Sidekiq probe: Using Redis server hostname " \
131
- "#{host.inspect} as hostname"
132
+ Appsignal.internal_logger.debug "Sidekiq probe: Using Redis server " \
133
+ "hostname #{host.inspect} as hostname"
132
134
  @hostname = host
133
135
  end
134
136
  end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ # @api private
5
+ module Rack
6
+ class BodyWrapper
7
+ def self.wrap(original_body, appsignal_transaction)
8
+ # The logic of how Rack treats a response body differs based on which methods
9
+ # the body responds to. This means that to support the Rack 3.x spec in full
10
+ # we need to return a wrapper which matches the API of the wrapped body as closely
11
+ # as possible. Pick the wrapper from the most specific to the least specific.
12
+ # See https://github.com/rack/rack/blob/main/SPEC.rdoc#the-body-
13
+ #
14
+ # What is important is that our Body wrapper responds to the same methods Rack
15
+ # (or a webserver) would be checking and calling, and passes through that functionality
16
+ # to the original body. This can be done using delegation via i.e. SimpleDelegate
17
+ # but we also need "close" to get called correctly so that the Appsignal transaction
18
+ # gets completed - which will not happen, for example, when #to_ary gets called
19
+ # just on the delegated Rack body.
20
+ #
21
+ # This comment https://github.com/rails/rails/pull/49627#issuecomment-1769802573
22
+ # is of particular interest to understand why this has to be somewhat complicated.
23
+ if original_body.respond_to?(:to_path)
24
+ PathableBodyWrapper.new(original_body, appsignal_transaction)
25
+ elsif original_body.respond_to?(:to_ary)
26
+ ArrayableBodyWrapper.new(original_body, appsignal_transaction)
27
+ elsif !original_body.respond_to?(:each) && original_body.respond_to?(:call)
28
+ # This body only supports #call, so we must be running a Rack 3 application
29
+ # It is possible that a body exposes both `each` and `call` in the hopes of
30
+ # being backwards-compatible with both Rack 3.x and Rack 2.x, however
31
+ # this is not going to work since the SPEC says that if both are available,
32
+ # `each` should be used and `call` should be ignored.
33
+ # So for that case we can drop by to our default EnumerableBodyWrapper
34
+ CallableBodyWrapper.new(original_body, appsignal_transaction)
35
+ else
36
+ EnumerableBodyWrapper.new(original_body, appsignal_transaction)
37
+ end
38
+ end
39
+
40
+ def initialize(body, appsignal_transaction)
41
+ @body_already_closed = false
42
+ @body = body
43
+ @transaction = appsignal_transaction
44
+ end
45
+
46
+ # This must be present in all Rack bodies and will be called by the serving adapter
47
+ def close
48
+ # The @body_already_closed check is needed so that if `to_ary`
49
+ # of the body has already closed itself (as prescribed) we do not
50
+ # attempt to close it twice
51
+ if !@body_already_closed && @body.respond_to?(:close)
52
+ Appsignal.instrument("response_body_close.rack") { @body.close }
53
+ end
54
+ @body_already_closed = true
55
+ rescue Exception => error # rubocop:disable Lint/RescueException
56
+ @transaction.set_error(error)
57
+ raise error
58
+ ensure
59
+ complete_transaction!
60
+ end
61
+
62
+ def complete_transaction!
63
+ # We need to call the Transaction class method and not
64
+ # @transaction.complete because the transaction is still
65
+ # thread-local and it needs to remove itself from the
66
+ # thread variables correctly, which does not happen on
67
+ # Transaction#complete.
68
+ #
69
+ # In the future it would be a good idea to ensure
70
+ # that the current transaction is the same as @transaction,
71
+ # or allow @transaction to complete itself and remove
72
+ # itself from Thread.current
73
+ Appsignal::Transaction.complete_current!
74
+ end
75
+ end
76
+
77
+ # The standard Rack body wrapper which exposes "each" for iterating
78
+ # over the response body. This is supported across all 3 major Rack
79
+ # versions.
80
+ #
81
+ # @api private
82
+ class EnumerableBodyWrapper < BodyWrapper
83
+ def each(&blk)
84
+ # This is a workaround for the Rails bug when there was a bit too much
85
+ # eagerness in implementing to_ary, see:
86
+ # https://github.com/rails/rails/pull/44953
87
+ # https://github.com/rails/rails/pull/47092
88
+ # https://github.com/rails/rails/pull/49627
89
+ # https://github.com/rails/rails/issues/49588
90
+ # While the Rack SPEC does not mandate `each` to be callable
91
+ # in a blockless way it is still a good idea to have it in place.
92
+ return enum_for(:each) unless block_given?
93
+
94
+ Appsignal.instrument("process_response_body.rack", "Process Rack response body (#each)") do
95
+ @body.each(&blk)
96
+ end
97
+ rescue Exception => error # rubocop:disable Lint/RescueException
98
+ @transaction.set_error(error)
99
+ raise error
100
+ end
101
+ end
102
+
103
+ # The callable response bodies are a new Rack 3.x feature, and would not work
104
+ # with older Rack versions. They must not respond to `each` because
105
+ # "If it responds to each, you must call each and not call". This is why
106
+ # it inherits from BodyWrapper directly and not from EnumerableBodyWrapper
107
+ #
108
+ # @api private
109
+ class CallableBodyWrapper < BodyWrapper
110
+ def call(stream)
111
+ # `stream` will be closed by the app we are calling, no need for us
112
+ # to close it ourselves
113
+ Appsignal.instrument("process_response_body.rack", "Process Rack response body (#call)") do
114
+ @body.call(stream)
115
+ end
116
+ rescue Exception => error # rubocop:disable Lint/RescueException
117
+ @transaction.set_error(error)
118
+ raise error
119
+ end
120
+ end
121
+
122
+ # "to_ary" takes precedence over "each" and allows the response body
123
+ # to be read eagerly. If the body supports that method, it takes precedence
124
+ # over "each":
125
+ # "Middleware may call to_ary directly on the Body and return a new Body in its place"
126
+ # One could "fold" both the to_ary API and the each() API into one Body object, but
127
+ # to_ary must also call "close" after it executes - and in the Rails implementation
128
+ # this pecularity was not handled properly.
129
+ #
130
+ # @api private
131
+ class ArrayableBodyWrapper < EnumerableBodyWrapper
132
+ def to_ary
133
+ @body_already_closed = true
134
+ Appsignal.instrument(
135
+ "process_response_body.rack",
136
+ "Process Rack response body (#to_ary)"
137
+ ) do
138
+ @body.to_ary
139
+ end
140
+ rescue Exception => error # rubocop:disable Lint/RescueException
141
+ @transaction.set_error(error)
142
+ raise error
143
+ ensure
144
+ # We do not call "close" on ourselves as the only action
145
+ # we need to complete is completing the transaction.
146
+ complete_transaction!
147
+ end
148
+ end
149
+
150
+ # Having "to_path" on a body allows Rack to serve out a static file, or to
151
+ # pass that file to the downstream webserver for sending using X-Sendfile
152
+ class PathableBodyWrapper < EnumerableBodyWrapper
153
+ def to_path
154
+ Appsignal.instrument("response_body_to_path.rack") { @body.to_path }
155
+ rescue Exception => error # rubocop:disable Lint/RescueException
156
+ @transaction.set_error(error)
157
+ raise error
158
+ end
159
+ end
160
+ end
161
+ end
@@ -7,7 +7,7 @@ module Appsignal
7
7
  module Rack
8
8
  class GenericInstrumentation
9
9
  def initialize(app, options = {})
10
- Appsignal.logger.debug "Initializing Appsignal::Rack::GenericInstrumentation"
10
+ Appsignal.internal_logger.debug "Initializing Appsignal::Rack::GenericInstrumentation"
11
11
  @app = app
12
12
  @options = options
13
13
  end
@@ -16,7 +16,9 @@ module Appsignal
16
16
  if Appsignal.active?
17
17
  call_with_appsignal_monitoring(env)
18
18
  else
19
- @app.call(env)
19
+ nil_transaction = Appsignal::Transaction::NilTransaction.new
20
+ status, headers, obody = @app.call(env)
21
+ [status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, nil_transaction)]
20
22
  end
21
23
  end
22
24
 
@@ -27,19 +29,30 @@ module Appsignal
27
29
  Appsignal::Transaction::HTTP_REQUEST,
28
30
  request
29
31
  )
32
+ # We need to complete the transaction if there is an exception inside the `call`
33
+ # of the app. If there isn't one and the app returns us a Rack response triplet, we let
34
+ # the BodyWrapper complete the transaction when #close gets called on it
35
+ # (guaranteed by the webserver)
36
+ complete_transaction_without_body = false
30
37
  begin
31
38
  Appsignal.instrument("process_action.generic") do
32
- @app.call(env)
39
+ status, headers, obody = @app.call(env)
40
+ [status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, transaction)]
33
41
  end
34
42
  rescue Exception => error # rubocop:disable Lint/RescueException
35
43
  transaction.set_error(error)
44
+ complete_transaction_without_body = true
36
45
  raise error
37
46
  ensure
38
- transaction.set_action_if_nil(env["appsignal.route"] || "unknown")
47
+ default_action = env["appsignal.route"] || env["appsignal.action"] || "unknown"
48
+ transaction.set_action_if_nil(default_action)
39
49
  transaction.set_metadata("path", request.path)
40
50
  transaction.set_metadata("method", request.request_method)
41
51
  transaction.set_http_or_background_queue_start
42
- Appsignal::Transaction.complete_current!
52
+
53
+ # Transaction gets completed when the body gets read out, except in cases when
54
+ # the app failed before returning us the Rack response triplet.
55
+ Appsignal::Transaction.complete_current! if complete_transaction_without_body
43
56
  end
44
57
  end
45
58
  end
@@ -7,7 +7,7 @@ module Appsignal
7
7
  module Rack
8
8
  class RailsInstrumentation
9
9
  def initialize(app, options = {})
10
- Appsignal.logger.debug "Initializing Appsignal::Rack::RailsInstrumentation"
10
+ Appsignal.internal_logger.debug "Initializing Appsignal::Rack::RailsInstrumentation"
11
11
  @app = app
12
12
  @options = options
13
13
  end
@@ -16,7 +16,9 @@ module Appsignal
16
16
  if Appsignal.active?
17
17
  call_with_appsignal_monitoring(env)
18
18
  else
19
- @app.call(env)
19
+ nil_transaction = Appsignal::Transaction::NilTransaction.new
20
+ status, headers, obody = @app.call(env)
21
+ [status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, nil_transaction)]
20
22
  end
21
23
  end
22
24
 
@@ -28,10 +30,17 @@ module Appsignal
28
30
  request,
29
31
  :params_method => :filtered_parameters
30
32
  )
33
+ # We need to complete the transaction if there is an exception exception inside the `call`
34
+ # of the app. If there isn't one and the app returns us a Rack response triplet, we let
35
+ # the BodyWrapper complete the transaction when #close gets called on it
36
+ # (guaranteed by the webserver)
37
+ complete_transaction_without_body = false
31
38
  begin
32
- @app.call(env)
39
+ status, headers, obody = @app.call(env)
40
+ [status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, transaction)]
33
41
  rescue Exception => error # rubocop:disable Lint/RescueException
34
42
  transaction.set_error(error)
43
+ complete_transaction_without_body = true
35
44
  raise error
36
45
  ensure
37
46
  controller = env["action_controller.instance"]
@@ -43,9 +52,12 @@ module Appsignal
43
52
  begin
44
53
  transaction.set_metadata("method", request.request_method)
45
54
  rescue => error
46
- Appsignal.logger.error("Unable to report HTTP request method: '#{error}'")
55
+ Appsignal.internal_logger.error("Unable to report HTTP request method: '#{error}'")
47
56
  end
48
- Appsignal::Transaction.complete_current!
57
+
58
+ # Transaction gets completed when the body gets read out, except in cases when
59
+ # the app failed before returning us the Rack response triplet.
60
+ Appsignal::Transaction.complete_current! if complete_transaction_without_body
49
61
  end
50
62
  end
51
63