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.
- checksums.yaml +4 -4
- data/.semaphore/semaphore.yml +180 -14
- data/CHANGELOG.md +164 -0
- data/README.md +2 -0
- data/Rakefile +3 -1
- data/build_matrix.yml +7 -13
- data/ext/Rakefile +8 -1
- data/ext/agent.rb +27 -27
- data/ext/appsignal_extension.c +0 -24
- data/ext/base.rb +4 -1
- data/gemfiles/redis-4.gemfile +5 -0
- data/gemfiles/redis-5.gemfile +6 -0
- data/lib/appsignal/cli/diagnose/paths.rb +33 -10
- data/lib/appsignal/cli/diagnose.rb +6 -1
- data/lib/appsignal/config.rb +19 -5
- data/lib/appsignal/demo.rb +1 -1
- data/lib/appsignal/environment.rb +24 -13
- data/lib/appsignal/event_formatter.rb +1 -1
- data/lib/appsignal/extension/jruby.rb +4 -17
- data/lib/appsignal/extension.rb +1 -1
- data/lib/appsignal/helpers/instrumentation.rb +10 -10
- data/lib/appsignal/helpers/metrics.rb +15 -13
- data/lib/appsignal/hooks/active_job.rb +9 -1
- data/lib/appsignal/hooks/redis.rb +1 -0
- data/lib/appsignal/hooks/redis_client.rb +27 -0
- data/lib/appsignal/hooks.rb +3 -2
- data/lib/appsignal/integrations/hanami.rb +1 -1
- data/lib/appsignal/integrations/padrino.rb +1 -1
- data/lib/appsignal/integrations/railtie.rb +1 -1
- data/lib/appsignal/integrations/redis_client.rb +20 -0
- data/lib/appsignal/integrations/sidekiq.rb +2 -2
- data/lib/appsignal/integrations/sinatra.rb +1 -1
- data/lib/appsignal/logger.rb +2 -0
- data/lib/appsignal/minutely.rb +4 -4
- data/lib/appsignal/probes/gvl.rb +1 -1
- data/lib/appsignal/probes/helpers.rb +1 -1
- data/lib/appsignal/probes/mri.rb +1 -1
- data/lib/appsignal/probes/sidekiq.rb +10 -8
- data/lib/appsignal/rack/body_wrapper.rb +161 -0
- data/lib/appsignal/rack/generic_instrumentation.rb +18 -5
- data/lib/appsignal/rack/rails_instrumentation.rb +17 -5
- data/lib/appsignal/rack/sinatra_instrumentation.rb +17 -5
- data/lib/appsignal/rack/streaming_listener.rb +27 -36
- data/lib/appsignal/span.rb +2 -2
- data/lib/appsignal/transaction.rb +46 -10
- data/lib/appsignal/utils/deprecation_message.rb +2 -2
- data/lib/appsignal/version.rb +1 -1
- data/lib/appsignal.rb +38 -31
- data/resources/cacert.pem +321 -159
- data/spec/lib/appsignal/cli/diagnose/utils_spec.rb +11 -0
- data/spec/lib/appsignal/cli/diagnose_spec.rb +38 -12
- data/spec/lib/appsignal/config_spec.rb +3 -2
- data/spec/lib/appsignal/hooks/activejob_spec.rb +26 -1
- data/spec/lib/appsignal/hooks/redis_client_spec.rb +222 -0
- data/spec/lib/appsignal/hooks/redis_spec.rb +98 -76
- data/spec/lib/appsignal/hooks_spec.rb +4 -4
- data/spec/lib/appsignal/integrations/railtie_spec.rb +2 -2
- data/spec/lib/appsignal/integrations/sidekiq_spec.rb +3 -3
- data/spec/lib/appsignal/integrations/sinatra_spec.rb +2 -2
- data/spec/lib/appsignal/minutely_spec.rb +2 -2
- data/spec/lib/appsignal/probes/sidekiq_spec.rb +29 -6
- data/spec/lib/appsignal/rack/body_wrapper_spec.rb +220 -0
- data/spec/lib/appsignal/rack/generic_instrumentation_spec.rb +3 -2
- data/spec/lib/appsignal/rack/rails_instrumentation_spec.rb +5 -3
- data/spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb +3 -1
- data/spec/lib/appsignal/rack/streaming_listener_spec.rb +9 -53
- data/spec/lib/appsignal/transaction_spec.rb +95 -2
- data/spec/lib/appsignal_spec.rb +62 -60
- data/spec/spec_helper.rb +1 -1
- data/spec/support/fixtures/projects/valid/config/appsignal.yml +3 -3
- data/spec/support/helpers/config_helpers.rb +6 -2
- data/spec/support/helpers/dependency_helper.rb +9 -1
- data/spec/support/helpers/log_helpers.rb +2 -2
- metadata +9 -2
@@ -10,22 +10,24 @@ module Appsignal
|
|
10
10
|
Appsignal::Utils::Data.generate(tags)
|
11
11
|
)
|
12
12
|
rescue RangeError
|
13
|
-
Appsignal.
|
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(
|
18
|
-
Appsignal::
|
19
|
-
|
20
|
-
|
21
|
-
|
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(
|
25
|
-
Appsignal::
|
26
|
-
|
27
|
-
|
28
|
-
|
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.
|
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.
|
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
|
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
|
@@ -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
|
data/lib/appsignal/hooks.rb
CHANGED
@@ -32,12 +32,12 @@ module Appsignal
|
|
32
32
|
return unless dependencies_present?
|
33
33
|
return if installed?
|
34
34
|
|
35
|
-
Appsignal.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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(
|
data/lib/appsignal/logger.rb
CHANGED
data/lib/appsignal/minutely.rb
CHANGED
@@ -108,7 +108,7 @@ module Appsignal
|
|
108
108
|
attr_reader :probes
|
109
109
|
|
110
110
|
def logger
|
111
|
-
Appsignal.
|
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.
|
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.
|
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.
|
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
|
data/lib/appsignal/probes/gvl.rb
CHANGED
@@ -22,7 +22,7 @@ module Appsignal
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def initialize(appsignal: Appsignal, gvl_tools: ::GVLTools)
|
25
|
-
Appsignal.
|
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.
|
50
|
+
Appsignal.internal_logger.debug "Probe helper: Using hostname config " \
|
51
51
|
"option '#{@hostname.inspect}' as hostname"
|
52
52
|
|
53
53
|
@hostname
|
data/lib/appsignal/probes/mri.rb
CHANGED
@@ -11,7 +11,7 @@ module Appsignal
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def initialize(appsignal: Appsignal, gc_profiler: Appsignal::GarbageCollection.profiler)
|
14
|
-
Appsignal.
|
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.
|
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
|
82
|
-
gauge "memory_usage", redis_info
|
83
|
-
gauge "memory_usage_rss", redis_info
|
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.
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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.
|
55
|
+
Appsignal.internal_logger.error("Unable to report HTTP request method: '#{error}'")
|
47
56
|
end
|
48
|
-
|
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
|
|