appsignal 4.5.17 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -0
  3. data/CLAUDE.md +192 -0
  4. data/README.md +17 -0
  5. data/appsignal.gemspec +1 -0
  6. data/build_matrix.yml +29 -0
  7. data/lib/appsignal/auth_check.rb +3 -3
  8. data/lib/appsignal/check_in/cron.rb +1 -1
  9. data/lib/appsignal/check_in/event.rb +1 -1
  10. data/lib/appsignal/check_in/scheduler.rb +3 -1
  11. data/lib/appsignal/check_in.rb +9 -6
  12. data/lib/appsignal/cli/diagnose.rb +1 -1
  13. data/lib/appsignal/cli.rb +1 -1
  14. data/lib/appsignal/config.rb +231 -30
  15. data/lib/appsignal/custom_marker.rb +3 -3
  16. data/lib/appsignal/environment.rb +7 -1
  17. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +1 -1
  18. data/lib/appsignal/event_formatter/active_record/instantiation_formatter.rb +1 -1
  19. data/lib/appsignal/event_formatter/active_record/sql_formatter.rb +1 -1
  20. data/lib/appsignal/event_formatter/elastic_search/search_formatter.rb +1 -1
  21. data/lib/appsignal/event_formatter/faraday/request_formatter.rb +1 -1
  22. data/lib/appsignal/event_formatter/mongo_ruby_driver/query_formatter.rb +1 -1
  23. data/lib/appsignal/event_formatter/rom/sql_formatter.rb +1 -1
  24. data/lib/appsignal/event_formatter/sequel/sql_formatter.rb +1 -1
  25. data/lib/appsignal/event_formatter/view_component/render_formatter.rb +1 -1
  26. data/lib/appsignal/event_formatter.rb +41 -3
  27. data/lib/appsignal/extension.rb +1 -1
  28. data/lib/appsignal/garbage_collection.rb +1 -1
  29. data/lib/appsignal/helpers/instrumentation.rb +82 -35
  30. data/lib/appsignal/helpers/metrics.rb +12 -9
  31. data/lib/appsignal/hooks/action_cable.rb +1 -1
  32. data/lib/appsignal/hooks/action_mailer.rb +1 -0
  33. data/lib/appsignal/hooks/active_job.rb +1 -1
  34. data/lib/appsignal/hooks/active_support_notifications.rb +1 -1
  35. data/lib/appsignal/hooks/at_exit.rb +1 -1
  36. data/lib/appsignal/hooks/celluloid.rb +1 -1
  37. data/lib/appsignal/hooks/data_mapper.rb +1 -1
  38. data/lib/appsignal/hooks/delayed_job.rb +1 -1
  39. data/lib/appsignal/hooks/dry_monitor.rb +1 -1
  40. data/lib/appsignal/hooks/excon.rb +1 -1
  41. data/lib/appsignal/hooks/gvl.rb +1 -1
  42. data/lib/appsignal/hooks/http.rb +1 -1
  43. data/lib/appsignal/hooks/mongo_ruby_driver.rb +1 -1
  44. data/lib/appsignal/hooks/mri.rb +1 -1
  45. data/lib/appsignal/hooks/net_http.rb +1 -1
  46. data/lib/appsignal/hooks/ownership.rb +1 -1
  47. data/lib/appsignal/hooks/passenger.rb +1 -1
  48. data/lib/appsignal/hooks/puma.rb +1 -1
  49. data/lib/appsignal/hooks/que.rb +1 -1
  50. data/lib/appsignal/hooks/rake.rb +1 -1
  51. data/lib/appsignal/hooks/redis.rb +1 -1
  52. data/lib/appsignal/hooks/redis_client.rb +1 -1
  53. data/lib/appsignal/hooks/resque.rb +1 -1
  54. data/lib/appsignal/hooks/sequel.rb +2 -1
  55. data/lib/appsignal/hooks/shoryuken.rb +1 -1
  56. data/lib/appsignal/hooks/sidekiq.rb +1 -0
  57. data/lib/appsignal/hooks/unicorn.rb +1 -1
  58. data/lib/appsignal/hooks/webmachine.rb +1 -1
  59. data/lib/appsignal/hooks.rb +5 -3
  60. data/lib/appsignal/integrations/action_cable.rb +1 -1
  61. data/lib/appsignal/integrations/active_support_notifications.rb +1 -1
  62. data/lib/appsignal/integrations/capistrano/capistrano_2_tasks.rb +1 -1
  63. data/lib/appsignal/integrations/data_mapper.rb +1 -1
  64. data/lib/appsignal/integrations/delayed_job_plugin.rb +1 -1
  65. data/lib/appsignal/integrations/dry_monitor.rb +1 -1
  66. data/lib/appsignal/integrations/excon.rb +1 -1
  67. data/lib/appsignal/integrations/http.rb +1 -1
  68. data/lib/appsignal/integrations/mongo_ruby_driver.rb +1 -1
  69. data/lib/appsignal/integrations/net_http.rb +1 -1
  70. data/lib/appsignal/integrations/object.rb +18 -2
  71. data/lib/appsignal/integrations/ownership.rb +1 -1
  72. data/lib/appsignal/integrations/puma.rb +1 -1
  73. data/lib/appsignal/integrations/que.rb +1 -1
  74. data/lib/appsignal/integrations/railtie.rb +17 -8
  75. data/lib/appsignal/integrations/rake.rb +2 -2
  76. data/lib/appsignal/integrations/redis.rb +1 -1
  77. data/lib/appsignal/integrations/redis_client.rb +1 -1
  78. data/lib/appsignal/integrations/resque.rb +2 -2
  79. data/lib/appsignal/integrations/shoryuken.rb +1 -1
  80. data/lib/appsignal/integrations/sidekiq.rb +15 -4
  81. data/lib/appsignal/integrations/unicorn.rb +1 -1
  82. data/lib/appsignal/integrations/webmachine.rb +1 -1
  83. data/lib/appsignal/internal_errors.rb +2 -2
  84. data/lib/appsignal/loaders/hanami.rb +1 -4
  85. data/lib/appsignal/loaders/padrino.rb +1 -1
  86. data/lib/appsignal/loaders/sinatra.rb +1 -1
  87. data/lib/appsignal/loaders.rb +1 -1
  88. data/lib/appsignal/logger.rb +36 -19
  89. data/lib/appsignal/marker.rb +1 -1
  90. data/lib/appsignal/probes/gvl.rb +4 -3
  91. data/lib/appsignal/probes/helpers.rb +1 -1
  92. data/lib/appsignal/probes/mri.rb +3 -3
  93. data/lib/appsignal/probes/sidekiq.rb +4 -3
  94. data/lib/appsignal/probes.rb +20 -15
  95. data/lib/appsignal/rack/event_handler.rb +21 -3
  96. data/lib/appsignal/rack/event_middleware.rb +111 -0
  97. data/lib/appsignal/rack/instrumentation_middleware.rb +2 -2
  98. data/lib/appsignal/rack.rb +1 -1
  99. data/lib/appsignal/sample_data.rb +31 -12
  100. data/lib/appsignal/span.rb +1 -1
  101. data/lib/appsignal/system.rb +9 -8
  102. data/lib/appsignal/transaction.rb +72 -52
  103. data/lib/appsignal/transmitter.rb +1 -1
  104. data/lib/appsignal/utils.rb +1 -1
  105. data/lib/appsignal/version.rb +2 -1
  106. data/lib/appsignal.rb +23 -14
  107. data/lib/puma/plugin/appsignal.rb +1 -1
  108. data/sig/appsignal.rbi +2599 -0
  109. data/sig/appsignal.rbs +2420 -0
  110. metadata +20 -2
@@ -13,6 +13,8 @@ module Appsignal
13
13
  # If called again, it will return the result of the first call.
14
14
  # This is useful for ensuring that a block is not executed multiple
15
15
  # times when it is broadcasted to multiple loggers.
16
+ #
17
+ # @!visibility private
16
18
  class BlockOnce
17
19
  def initialize(&block)
18
20
  @block = block
@@ -44,9 +46,13 @@ module Appsignal
44
46
  end
45
47
  end
46
48
 
49
+ # @!visibility private
47
50
  PLAINTEXT = 0
51
+ # @!visibility private
48
52
  LOGFMT = 1
53
+ # @!visibility private
49
54
  JSON = 2
55
+ # @!visibility private
50
56
  SEVERITY_MAP = {
51
57
  DEBUG => 2,
52
58
  INFO => 3,
@@ -55,14 +61,17 @@ module Appsignal
55
61
  FATAL => 7
56
62
  }.freeze
57
63
 
64
+ # Logging severity threshold
65
+ # @return [Integer]
58
66
  attr_reader :level
59
67
 
60
68
  # Create a new logger instance
61
69
  #
62
- # @param group Name of the group for this logger.
63
- # @param level Minimum log level to report. Log lines below this level will be ignored.
64
- # @param format Format to use to parse log line attributes.
65
- # @param attributes Default attributes for all log lines.
70
+ # @param group [String] Name of the group for this logger.
71
+ # @param level [Integer] Minimum log level to report. Log lines below this
72
+ # level will be ignored.
73
+ # @param format [Integer] Format to use to parse log line attributes.
74
+ # @param attributes [Hash<String, String>] Default attributes for all log lines.
66
75
  # @return [void]
67
76
  def initialize(group, level: INFO, format: PLAINTEXT, attributes: {})
68
77
  raise TypeError, "group must be a string" unless group.is_a? String
@@ -77,9 +86,9 @@ module Appsignal
77
86
  @loggers = []
78
87
  end
79
88
 
80
- # When a formatter is set on the logger (e.g. when wrapping the logger in
81
- # `ActiveSupport::TaggedLogging`) we want to set that formatter on all the
82
- # loggers that are being broadcasted to.
89
+ # Sets the formatter for this logger and all broadcasted loggers.
90
+ # @param formatter [Proc] The formatter to use for log messages.
91
+ # @return [Proc]
83
92
  def formatter=(formatter)
84
93
  super
85
94
  @loggers.each { |logger| logger.formatter = formatter }
@@ -87,7 +96,7 @@ module Appsignal
87
96
 
88
97
  # We support the various methods in the Ruby
89
98
  # logger class by supplying this method.
90
- # @api private
99
+ # @!visibility private
91
100
  def add(severity, message = nil, group = nil, &block) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
92
101
  # If we do not need to broadcast to any loggers and the severity is
93
102
  # below the log level, we can return early.
@@ -142,40 +151,40 @@ module Appsignal
142
151
  alias log add
143
152
 
144
153
  # Log a debug level message
145
- # @param message Message to log
146
- # @param attributes Attributes to tag the log with
154
+ # @param message [String] Message to log
155
+ # @param attributes [Hash<String, Object>] Attributes to tag the log with
147
156
  # @return [void]
148
157
  def debug(message = nil, attributes = {}, &block)
149
158
  add_with_attributes(DEBUG, message, @group, attributes, &block)
150
159
  end
151
160
 
152
161
  # Log an info level message
153
- # @param message Message to log
154
- # @param attributes Attributes to tag the log with
162
+ # @param message [String] Message to log
163
+ # @param attributes [Hash<String, Object>] Attributes to tag the log with
155
164
  # @return [void]
156
165
  def info(message = nil, attributes = {}, &block)
157
166
  add_with_attributes(INFO, message, @group, attributes, &block)
158
167
  end
159
168
 
160
169
  # Log a warn level message
161
- # @param message Message to log
162
- # @param attributes Attributes to tag the log with
170
+ # @param message [String] Message to log
171
+ # @param attributes [Hash<String, Object>] Attributes to tag the log with
163
172
  # @return [void]
164
173
  def warn(message = nil, attributes = {}, &block)
165
174
  add_with_attributes(WARN, message, @group, attributes, &block)
166
175
  end
167
176
 
168
177
  # Log an error level message
169
- # @param message Message to log
170
- # @param attributes Attributes to tag the log with
178
+ # @param message [String, Exception] Message to log
179
+ # @param attributes [Hash<String, Object>] Attributes to tag the log with
171
180
  # @return [void]
172
181
  def error(message = nil, attributes = {}, &block)
173
182
  add_with_attributes(ERROR, message, @group, attributes, &block)
174
183
  end
175
184
 
176
185
  # Log a fatal level message
177
- # @param message Message to log
178
- # @param attributes Attributes to tag the log with
186
+ # @param message [String, Exception] Message to log
187
+ # @param attributes [Hash<String, Object>] Attributes to tag the log with
179
188
  # @return [void]
180
189
  def fatal(message = nil, attributes = {}, &block)
181
190
  add_with_attributes(FATAL, message, @group, attributes, &block)
@@ -185,13 +194,15 @@ module Appsignal
185
194
  #
186
195
  # Returns the number of characters written.
187
196
  #
188
- # @param message Message to log
197
+ # @param message [String] Message to log
189
198
  # @return [Integer]
190
199
  def <<(message)
191
200
  info(message)
192
201
  message.length
193
202
  end
194
203
 
204
+ # Temporarily silences the logger to a specified level while executing a block.
205
+ #
195
206
  # When using ActiveSupport::TaggedLogging without the broadcast feature,
196
207
  # the passed logger is required to respond to the `silence` method.
197
208
  #
@@ -199,6 +210,9 @@ module Appsignal
199
210
  #
200
211
  # - https://github.com/rails/rails/blob/e11ebc04cfbe41c06cdfb70ee5a9fdbbd98bb263/activesupport/lib/active_support/logger.rb#L60-L76
201
212
  # - https://github.com/rails/rails/blob/e11ebc04cfbe41c06cdfb70ee5a9fdbbd98bb263/activesupport/lib/active_support/logger_silence.rb
213
+ #
214
+ # @param severity [Integer] The minimum severity level to log during the block.
215
+ # @return [Object] The return value of the block.
202
216
  def silence(severity = ERROR, &block)
203
217
  previous_level = @level
204
218
  @level = severity
@@ -209,6 +223,9 @@ module Appsignal
209
223
  @silenced = false
210
224
  end
211
225
 
226
+ # Adds a logger to broadcast log messages to.
227
+ # @param logger [Logger] The logger to add to the broadcast list.
228
+ # @return [Array<Logger>]
212
229
  def broadcast_to(logger)
213
230
  @loggers << logger
214
231
  end
@@ -20,7 +20,7 @@ module Appsignal
20
20
  # @see Appsignal::CLI::NotifyOfDeploy
21
21
  # @see https://docs.appsignal.com/appsignal/terminology.html#markers
22
22
  # Terminology: Deploy marker
23
- # @api private
23
+ # @!visibility private
24
24
  class Marker
25
25
  # Path used on the AppSignal Push API
26
26
  # https://push.appsignal.com/1/markers
@@ -2,21 +2,22 @@
2
2
 
3
3
  module Appsignal
4
4
  module Probes
5
+ # @!visibility private
5
6
  class GvlProbe
6
7
  include Helpers
7
8
 
8
- # @api private
9
+ # @!visibility private
9
10
  def self.dependencies_present?
10
11
  defined?(::GVLTools) && gvltools_0_2_or_newer? && ruby_3_2_or_newer? &&
11
12
  !Appsignal::System.jruby?
12
13
  end
13
14
 
14
- # @api private
15
+ # @!visibility private
15
16
  def self.gvltools_0_2_or_newer?
16
17
  Gem::Version.new(::GVLTools::VERSION) >= Gem::Version.new("0.2.0")
17
18
  end
18
19
 
19
- # @api private
20
+ # @!visibility private
20
21
  def self.ruby_3_2_or_newer?
21
22
  Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.2.0")
22
23
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Appsignal
4
4
  module Probes
5
- # @api private
5
+ # @!visibility private
6
6
  module Helpers
7
7
  private
8
8
 
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Appsignal
4
4
  module Probes
5
- # @api private
5
+ # @!visibility private
6
6
  class MriProbe
7
7
  include Helpers
8
8
 
9
- # @api private
9
+ # @!visibility private
10
10
  def self.dependencies_present?
11
11
  defined?(::RubyVM) && ::RubyVM.respond_to?(:stat)
12
12
  end
@@ -17,7 +17,7 @@ module Appsignal
17
17
  @gc_profiler = gc_profiler
18
18
  end
19
19
 
20
- # @api private
20
+ # @!visibility private
21
21
  def call
22
22
  stat = RubyVM.stat
23
23
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Appsignal
4
4
  module Probes
5
- # @api private
5
+ # @!visibility private
6
6
  class SidekiqProbe
7
7
  include Helpers
8
8
 
@@ -38,14 +38,15 @@ module Appsignal
38
38
  end
39
39
  end
40
40
 
41
- # @api private
41
+ # @!visibility private
42
42
  attr_reader :config
43
43
 
44
+ # @!visibility private
44
45
  def self.sidekiq7_and_greater?
45
46
  Gem::Version.new(::Sidekiq::VERSION) >= Gem::Version.new("7.0.0")
46
47
  end
47
48
 
48
- # @api private
49
+ # @!visibility private
49
50
  def self.dependencies_present?
50
51
  return true if sidekiq7_and_greater?
51
52
  return false unless defined?(::Redis::VERSION) # Sidekiq <= 6
@@ -2,10 +2,11 @@
2
2
 
3
3
  module Appsignal
4
4
  module Probes
5
- # @api private
5
+ # @return [Integer]
6
+ # @!visibility private
6
7
  ITERATION_IN_SECONDS = 60
7
8
 
8
- # @api private
9
+ # @!visibility private
9
10
  class ProbeCollection
10
11
  def initialize
11
12
  @probes = {}
@@ -23,13 +24,12 @@ module Appsignal
23
24
  end
24
25
 
25
26
  # Fetch a probe using its name.
26
- # @param key [Symbol/String] The name of the probe to fetch.
27
+ # @param key [Symbol, String] The name of the probe to fetch.
27
28
  # @return [Object] Returns the registered probe.
28
29
  def [](key)
29
30
  probes[key]
30
31
  end
31
32
 
32
- # @api private
33
33
  def internal_register(name, probe)
34
34
  if probes.key?(name)
35
35
  logger.debug "A probe with the name `#{name}` is already " \
@@ -38,12 +38,10 @@ module Appsignal
38
38
  probes[name] = probe
39
39
  end
40
40
 
41
- # @api private
42
41
  def unregister(name)
43
42
  probes.delete(name)
44
43
  end
45
44
 
46
- # @api private
47
45
  def each(&block)
48
46
  probes.each(&block)
49
47
  end
@@ -58,14 +56,14 @@ module Appsignal
58
56
  end
59
57
 
60
58
  class << self
61
- # @api private
59
+ # @!visibility private
62
60
  def mutex
63
61
  @mutex ||= Thread::Mutex.new
64
62
  end
65
63
 
66
64
  # @see ProbeCollection
67
65
  # @return [ProbeCollection] Returns list of probes.
68
- # @api private
66
+ # @!visibility private
69
67
  def probes
70
68
  @probes ||= ProbeCollection.new
71
69
  end
@@ -129,7 +127,7 @@ module Appsignal
129
127
  # # "started" # Printed on Appsignal::Probes.start
130
128
  # # "called" # Repeated every minute
131
129
  #
132
- # @param name [Symbol/String] Name of the probe. Can be used with
130
+ # @param name [Symbol, String] Name of the probe. Can be used with
133
131
  # {ProbeCollection#[]}. This name will be used in errors in the log and
134
132
  # allows overwriting of probes by registering new ones with the same
135
133
  # name.
@@ -153,7 +151,7 @@ module Appsignal
153
151
  # # Then unregister a probe if needed
154
152
  # Appsignal::Probes.unregister :my_probe
155
153
  #
156
- # @param name [Symbol/String] Name of the probe used to {register} the
154
+ # @param name [Symbol, String] Name of the probe used to {register} the
157
155
  # probe.
158
156
  # @return [void]
159
157
  def unregister(name)
@@ -162,6 +160,7 @@ module Appsignal
162
160
  uninitialize_probe(name)
163
161
  end
164
162
 
163
+ # @return [void]
165
164
  # @api private
166
165
  def start
167
166
  stop
@@ -184,8 +183,10 @@ module Appsignal
184
183
  logger.debug("Gathering minutely metrics with '#{name}' probe")
185
184
  probe.call
186
185
  rescue => ex
187
- logger.error "Error in minutely probe '#{name}': #{ex}"
188
- logger.debug ex.backtrace.join("\n")
186
+ logger.error(
187
+ "Error in minutely probe '#{name}': #{ex.class}: #{ex.message}\n" \
188
+ "#{ex.backtrace.join("\n")}"
189
+ )
189
190
  end
190
191
  end
191
192
  end_time = Time.now
@@ -212,13 +213,15 @@ module Appsignal
212
213
 
213
214
  # Stop the minutely probes mechanism. Stop the thread and clear all probe
214
215
  # instances.
216
+ #
217
+ # @return [void]
215
218
  def stop
216
219
  defined?(@thread) && @thread.kill
217
220
  @started = false
218
221
  probe_instances.clear
219
222
  end
220
223
 
221
- # @api private
224
+ # @!visibility private
222
225
  def wait_time
223
226
  ITERATION_IN_SECONDS - Time.now.sec
224
227
  end
@@ -256,8 +259,10 @@ module Appsignal
256
259
  end
257
260
  rescue => error
258
261
  logger = Appsignal.internal_logger
259
- logger.error "Error while initializing minutely probe '#{name}': #{error}"
260
- logger.debug error.backtrace.join("\n")
262
+ logger.error(
263
+ "Error while initializing minutely probe '#{name}': #{error.class}: #{error.message}\n" \
264
+ "#{error.backtrace.join("\n")}"
265
+ )
261
266
  end
262
267
 
263
268
  def uninitialize_probe(name)
@@ -2,10 +2,11 @@
2
2
 
3
3
  module Appsignal
4
4
  module Rack
5
- # Instrumentation middleware using Rack's Events module.
5
+ # Instrumentation event handler for `Rack::Events`.
6
6
  #
7
- # We recommend using this in combination with the
8
- # {InstrumentationMiddleware}.
7
+ # Using this middleware directly with `Rack::Events` is deprecated.
8
+ # We recommend using {EventMiddleware} instead, which is compatible with
9
+ # streaming bodies, in combination with the {InstrumentationMiddleware}.
9
10
  #
10
11
  # This middleware will report the response status code as the
11
12
  # `response_status` tag on the sample. It will also report the response
@@ -38,10 +39,12 @@ module Appsignal
38
39
 
39
40
  # @api private
40
41
  attr_reader :id
42
+ attr_writer :using_appsignal_rack_events_middleware
41
43
 
42
44
  # @api private
43
45
  def initialize
44
46
  @id = SecureRandom.uuid
47
+ @using_appsignal_rack_events_middleware = false
45
48
  end
46
49
 
47
50
  # @api private
@@ -51,6 +54,8 @@ module Appsignal
51
54
 
52
55
  # @api private
53
56
  def on_start(request, _response)
57
+ emit_warning_once
58
+
54
59
  return unless Appsignal.active?
55
60
 
56
61
  event_handler = self
@@ -152,6 +157,19 @@ module Appsignal
152
157
  namespace
153
158
  end
154
159
  end
160
+
161
+ def emit_warning_once
162
+ return if @using_appsignal_rack_events_middleware
163
+
164
+ Appsignal.internal_logger.warn <<~MSG
165
+ Rack::Events is not compatible with streaming bodies. Using `Appsignal::Rack::EventHandler`#{" "}
166
+ with `Rack::Events` will break streaming responses in your application and is deprecated.#{" "}
167
+ To silence this warning, use `Appsignal::Rack::EventMiddleware` instead, which is compatible#{" "}
168
+ with streaming bodies.#{" "}
169
+ See https://docs.appsignal.com/ruby/integrations/rack.html for more information.
170
+ MSG
171
+ @using_appsignal_rack_events_middleware = true
172
+ end
155
173
  end
156
174
  end
157
175
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Rack
5
+ # Modified version of the {::Rack::Events} instrumentation
6
+ # middleware.
7
+ #
8
+ # We recommend using this instead of {::Rack::Events}, as it
9
+ # is compatible with streaming bodies.
10
+ #
11
+ # We do not recommend using this middleware directly, instead
12
+ # recommending the use of {EventMiddleware}, which is a
13
+ # convenience wrapper around this middleware that includes
14
+ # AppSignal's {EventHandler}.
15
+ #
16
+ # See the original implementation at:
17
+ # https://github.com/rack/rack/blob/8d3d7857fcd9e5df057a6c22458bab35b3a19c12/lib/rack/events.rb
18
+ class Events < ::Rack::Events
19
+ # A stub for {::Rack::Events::EventedBodyProxy}. It
20
+ # allows the same initialization arguments, but
21
+ # otherwise behaves identically to {::Rack::BodyProxy}.
22
+ #
23
+ # It does not implement `#each`, fixing an issue
24
+ # where the evented body proxy would break
25
+ # streaming responses by always responding to `#each`
26
+ # even if the proxied body did not implement it.
27
+ #
28
+ # Because it ignores the handlers passed to it and
29
+ # behaves like a normal body proxy, the `on_send`
30
+ # event on the handlers is never called.
31
+ class EventedBodyProxy < ::Rack::BodyProxy
32
+ def initialize(body, _request, _response, _handlers, &block)
33
+ super(body, &block)
34
+ end
35
+ end
36
+
37
+ # Notifies the AppSignal event handler that it's being
38
+ # used with the modified middleware, suppressing a
39
+ # deprecation warning.
40
+ def initialize(app, handlers = [])
41
+ super
42
+ handlers.each do |handler|
43
+ if handler.respond_to?(:using_appsignal_rack_events_middleware=)
44
+ handler.using_appsignal_rack_events_middleware = true
45
+ end
46
+ end
47
+ end
48
+
49
+ # The `call` method, exactly as implemented by {::Rack::Events},
50
+ # but redefined here so that it uses our {EventedBodyProxy}
51
+ # instead of the original {::Rack::Events::EventedBodyProxy}.
52
+ #
53
+ # This fixes streaming bodies, but it also means that the
54
+ # `on_send` event on the handlers is never called.
55
+ #
56
+ # See the original implementation at:
57
+ # https://github.com/rack/rack/blob/8d3d7857fcd9e5df057a6c22458bab35b3a19c12/lib/rack/events.rb#L111-L129
58
+ def call(env)
59
+ request = make_request env
60
+ on_start request, nil
61
+
62
+ begin
63
+ status, headers, body = @app.call request.env
64
+ response = make_response status, headers, body
65
+ on_commit request, response
66
+ rescue StandardError => e
67
+ on_error request, response, e
68
+ on_finish request, response
69
+ raise
70
+ end
71
+
72
+ body = EventedBodyProxy.new(body, request, response, @handlers) do
73
+ on_finish request, response
74
+ end
75
+ [response.status, response.headers, body]
76
+ end
77
+ end
78
+
79
+ # Instrumentation middleware using Rack's Events module.
80
+ #
81
+ # A convenience wrapper around our {Events} middleware,
82
+ # modified to be compatible with streaming bodies,
83
+ # that automatically includes AppSignal's {EventHandler}.
84
+ #
85
+ # We recommend using this in combination with the
86
+ # {InstrumentationMiddleware}.
87
+ #
88
+ # This middleware will report the response status code as the
89
+ # `response_status` tag on the sample. It will also report the response
90
+ # status as the `response_status` metric.
91
+ #
92
+ # This middleware will ensure the AppSignal transaction is always completed
93
+ # for every request.
94
+ #
95
+ # @example Add EventMiddleware to a Rack app
96
+ # # Add this middleware as the first middleware of an app
97
+ # use Appsignal::Rack::EventMiddleware
98
+ #
99
+ # # Then add the InstrumentationMiddleware
100
+ # use Appsignal::Rack::InstrumentationMiddleware
101
+ #
102
+ # @see https://docs.appsignal.com/ruby/integrations/rack.html
103
+ # Rack integration documentation.
104
+ # @api public
105
+ class EventMiddleware < Events
106
+ def initialize(app)
107
+ super(app, [Appsignal::Rack::EventHandler.new])
108
+ end
109
+ end
110
+ end
111
+ end
@@ -27,8 +27,8 @@ module Appsignal
27
27
  # require "appsignal"
28
28
  # # Configure and start AppSignal
29
29
  #
30
- # # Add the EventHandler first
31
- # use ::Rack::Events, [Appsignal::Rack::EventHandler.new]
30
+ # # Add the EventMiddleware first
31
+ # use Appsignal::Rack::EventMiddleware
32
32
  # # Add the instrumentation middleware second
33
33
  # use Appsignal::Rack::InstrumentationMiddleware
34
34
  #
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- # @api private
4
+ # @!visibility private
5
5
  module Rack
6
6
  APPSIGNAL_TRANSACTION = "appsignal.transaction"
7
7
  APPSIGNAL_EVENT_HANDLER_ID = "appsignal.event_handler_id"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- # @api private
4
+ # @!visibility private
5
5
  class SampleData
6
6
  def initialize(key, accepted_type = nil)
7
7
  @key = key
@@ -21,7 +21,6 @@ module Appsignal
21
21
  end
22
22
  end
23
23
 
24
- # @api private
25
24
  def set_empty_value!
26
25
  @empty = true
27
26
  @blocks.clear
@@ -36,12 +35,18 @@ module Appsignal
36
35
  else
37
36
  block_or_value
38
37
  end
38
+
39
39
  unless accepted_type?(new_value)
40
40
  log_unsupported_data_type(new_value)
41
41
  next
42
42
  end
43
43
 
44
- value = merge_values(value, new_value)
44
+ # Before trying to merge values, convert them to Ruby classes
45
+ # This way we don't need to check if something is of a type often
46
+ value_new = convert_to_ruby_class(new_value)
47
+ value_original = convert_to_ruby_class(value)
48
+
49
+ value = merge_values(value_original, value_new)
45
50
  new_value
46
51
  end
47
52
 
@@ -52,7 +57,6 @@ module Appsignal
52
57
  @blocks.any?
53
58
  end
54
59
 
55
- # @api private
56
60
  def empty?
57
61
  @empty
58
62
  end
@@ -81,15 +85,19 @@ module Appsignal
81
85
  end
82
86
  end
83
87
 
88
+ def mergable?(value_original, value_new)
89
+ value_new.instance_of?(value_original.class)
90
+ end
91
+
84
92
  def merge_values(value_original, value_new)
85
- unless value_new.instance_of?(value_original.class)
86
- unless value_original == UNSET_VALUE
87
- Appsignal.internal_logger.warn(
88
- "The sample data '#{@key}' changed type from " \
89
- "'#{value_original.class}' to '#{value_new.class}'. " \
90
- "These types can not be merged. Using new '#{value_new.class}' type."
91
- )
92
- end
93
+ return value_new if value_original == UNSET_VALUE
94
+
95
+ unless mergable?(value_original, value_new)
96
+ Appsignal.internal_logger.warn(
97
+ "The sample data '#{@key}' changed type from " \
98
+ "'#{value_original.class}' to '#{value_new.class}'. " \
99
+ "These types can not be merged. Using new '#{value_new.class}' type."
100
+ )
93
101
  return value_new
94
102
  end
95
103
 
@@ -103,6 +111,17 @@ module Appsignal
103
111
  end
104
112
  end
105
113
 
114
+ # Convert any subclasses of Hash to a Ruby Hash class, so we don't have to
115
+ # account for the original value being a value that's not a pure Hash or
116
+ # Array object.
117
+ def convert_to_ruby_class(value)
118
+ if value.is_a? Hash
119
+ value.to_h
120
+ else
121
+ value
122
+ end
123
+ end
124
+
106
125
  def log_unsupported_data_type(value)
107
126
  Appsignal.internal_logger.error(
108
127
  "Sample data '#{@key}': Unsupported data type '#{value.class}' received: #{value.inspect}"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- # @api private
4
+ # @!visibility private
5
5
  class Span
6
6
  def initialize(namespace = nil, ext = nil)
7
7
  @ext = ext || Appsignal::Extension::Span.root(namespace || "")