appsignal 4.0.2 → 4.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/lib/appsignal/check_in/cron.rb +2 -34
- data/lib/appsignal/check_in/scheduler.rb +192 -0
- data/lib/appsignal/check_in.rb +18 -0
- data/lib/appsignal/cli/diagnose.rb +1 -1
- data/lib/appsignal/config.rb +3 -12
- data/lib/appsignal/hooks/at_exit.rb +2 -1
- data/lib/appsignal/integrations/railtie.rb +2 -7
- data/lib/appsignal/rack/body_wrapper.rb +15 -0
- data/lib/appsignal/transmitter.rb +30 -7
- data/lib/appsignal/utils/ndjson.rb +15 -0
- data/lib/appsignal/utils.rb +1 -0
- data/lib/appsignal/version.rb +1 -1
- data/lib/appsignal.rb +1 -0
- data/spec/lib/appsignal/check_in/cron_spec.rb +210 -0
- data/spec/lib/appsignal/check_in/scheduler_spec.rb +484 -0
- data/spec/lib/appsignal/config_spec.rb +0 -25
- data/spec/lib/appsignal/environment_spec.rb +23 -2
- data/spec/lib/appsignal/hooks/at_exit_spec.rb +11 -0
- data/spec/lib/appsignal/integrations/railtie_spec.rb +27 -6
- data/spec/lib/appsignal/rack/body_wrapper_spec.rb +29 -21
- data/spec/lib/appsignal/transmitter_spec.rb +48 -2
- data/spec/lib/appsignal_spec.rb +5 -0
- data/spec/support/helpers/take_at_most_helper.rb +21 -0
- metadata +7 -3
- data/spec/lib/appsignal/check_in_spec.rb +0 -136
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 91e5aad04da2524d3d2f4bd983ecd76cf1a33380a1bc2a6bf8aab1bd4d91db5b
|
4
|
+
data.tar.gz: c7d0582debd5c6d9ed29f2239a4baeed92ebdad46606c423e7537495b4dea3ab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a482b32ffa5d9ddc805a65507ab4d526f9c299969c86123743e8f0c4830af6f8abd3eecd805cb7c4eec6abcd474a2f97704ac9f5129825b7958245cc973ff6db
|
7
|
+
data.tar.gz: c0bf6a6ba6454fee105c6890db3154e03c0ad64c6b5ab325fb0186edf99066e923d7b6b4043ea4d77428155fbc58371ece1309517109437edc8cc265ffda554a
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,38 @@
|
|
1
1
|
# AppSignal for Ruby gem Changelog
|
2
2
|
|
3
|
+
## 4.0.4
|
4
|
+
|
5
|
+
_Published on 2024-08-29._
|
6
|
+
|
7
|
+
### Changed
|
8
|
+
|
9
|
+
- Send check-ins concurrently. When calling `Appsignal::CheckIn.cron`, instead of blocking the current thread while the check-in events are sent, schedule them to be sent in a separate thread.
|
10
|
+
|
11
|
+
When shutting down your application manually, call `Appsignal.stop` to block until all scheduled check-ins have been sent.
|
12
|
+
|
13
|
+
(patch [46d4ca74](https://github.com/appsignal/appsignal-ruby/commit/46d4ca74f4c188cc011653ed23969ad7ec770812))
|
14
|
+
|
15
|
+
### Fixed
|
16
|
+
|
17
|
+
- Make our Rack BodyWrapper behave like a Rack BodyProxy. If a method doesn't exist on our BodyWrapper class, but it does exist on the body, behave like the Rack BodyProxy and call the method on the wrapped body. (patch [e2376305](https://github.com/appsignal/appsignal-ruby/commit/e23763058a3fb980f1054e9c1eaf7e0f25f75666))
|
18
|
+
- Do not report `SignalException` errors from our `at_exit` error reporter. (patch [3ba3ce31](https://github.com/appsignal/appsignal-ruby/commit/3ba3ce31ee3f3e84665c9f2f18d488c689cff6c2))
|
19
|
+
|
20
|
+
## 4.0.3
|
21
|
+
|
22
|
+
_Published on 2024-08-26._
|
23
|
+
|
24
|
+
### Changed
|
25
|
+
|
26
|
+
- Do not report Sidekiq `Sidekiq::JobRetry::Handled` and `Sidekiq::JobRetry::Skip` errors. These errors would be reported by our Rails error subscriber. These are an internal Sidekiq errors we do not need to report. (patch [e385ee2c](https://github.com/appsignal/appsignal-ruby/commit/e385ee2c4da13063e6f1a7a207286dda74113fc4))
|
27
|
+
|
28
|
+
### Removed
|
29
|
+
|
30
|
+
- Remove the `app_path` writer in the `Appsignal.configure` helper. This was deprecated in version 3.x. It is removed now in the next major version.
|
31
|
+
|
32
|
+
Use the `root_path` keyword argument in the `Appsignal.configure` helper (`Appsignal.configure(:root_path => "...")`) to change the AppSignal root path if necessary.
|
33
|
+
|
34
|
+
(patch [6335da6d](https://github.com/appsignal/appsignal-ruby/commit/6335da6d99a5ba7687fb5885eee27b9633d80474))
|
35
|
+
|
3
36
|
## 4.0.2
|
4
37
|
|
5
38
|
_Published on 2024-08-23._
|
@@ -3,15 +3,6 @@
|
|
3
3
|
module Appsignal
|
4
4
|
module CheckIn
|
5
5
|
class Cron
|
6
|
-
class << self
|
7
|
-
# @api private
|
8
|
-
def transmitter
|
9
|
-
@transmitter ||= Appsignal::Transmitter.new(
|
10
|
-
"#{Appsignal.config[:logging_endpoint]}/check_ins/json"
|
11
|
-
)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
6
|
# @api private
|
16
7
|
attr_reader :identifier, :digest
|
17
8
|
|
@@ -21,11 +12,11 @@ module Appsignal
|
|
21
12
|
end
|
22
13
|
|
23
14
|
def start
|
24
|
-
|
15
|
+
CheckIn.scheduler.schedule(event("start"))
|
25
16
|
end
|
26
17
|
|
27
18
|
def finish
|
28
|
-
|
19
|
+
CheckIn.scheduler.schedule(event("finish"))
|
29
20
|
end
|
30
21
|
|
31
22
|
private
|
@@ -39,29 +30,6 @@ module Appsignal
|
|
39
30
|
:check_in_type => "cron"
|
40
31
|
}
|
41
32
|
end
|
42
|
-
|
43
|
-
def transmit_event(kind)
|
44
|
-
unless Appsignal.active?
|
45
|
-
Appsignal.internal_logger.debug(
|
46
|
-
"AppSignal not active, not transmitting cron check-in event"
|
47
|
-
)
|
48
|
-
return
|
49
|
-
end
|
50
|
-
|
51
|
-
response = self.class.transmitter.transmit(event(kind))
|
52
|
-
|
53
|
-
if response.code.to_i >= 200 && response.code.to_i < 300
|
54
|
-
Appsignal.internal_logger.debug(
|
55
|
-
"Transmitted cron check-in `#{identifier}` (#{digest}) #{kind} event"
|
56
|
-
)
|
57
|
-
else
|
58
|
-
Appsignal.internal_logger.error(
|
59
|
-
"Failed to transmit cron check-in #{kind} event: status code was #{response.code}"
|
60
|
-
)
|
61
|
-
end
|
62
|
-
rescue => e
|
63
|
-
Appsignal.internal_logger.error("Failed to transmit cron check-in #{kind} event: #{e}")
|
64
|
-
end
|
65
33
|
end
|
66
34
|
end
|
67
35
|
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Appsignal
|
4
|
+
module CheckIn
|
5
|
+
class Scheduler
|
6
|
+
INITIAL_DEBOUNCE_SECONDS = 0.1
|
7
|
+
BETWEEN_TRANSMISSIONS_DEBOUNCE_SECONDS = 10
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
# The mutex is used to synchronize access to the events array, the
|
11
|
+
# waker thread and the main thread, as well as queue writes
|
12
|
+
# (which depend on the events array) and closes (so they do not
|
13
|
+
# happen at the same time that an event is added to the scheduler)
|
14
|
+
@mutex = Mutex.new
|
15
|
+
# The transmitter thread will be started when an event is first added.
|
16
|
+
@thread = nil
|
17
|
+
@queue = Thread::Queue.new
|
18
|
+
# Scheduled events that have not been sent to the transmitter thread
|
19
|
+
# yet. A copy of this array is pushed to the queue by the waker thread
|
20
|
+
# after it has awaited the debounce period.
|
21
|
+
@events = []
|
22
|
+
# The waker thread is used to schedule debounces. It will be started
|
23
|
+
# when an event is first added.
|
24
|
+
@waker = nil
|
25
|
+
# For internal testing purposes.
|
26
|
+
@transmitted = 0
|
27
|
+
end
|
28
|
+
|
29
|
+
def schedule(event)
|
30
|
+
unless Appsignal.active?
|
31
|
+
Appsignal.internal_logger.debug(
|
32
|
+
"Cannot transmit #{describe([event])}: AppSignal is not active"
|
33
|
+
)
|
34
|
+
return
|
35
|
+
end
|
36
|
+
|
37
|
+
@mutex.synchronize do
|
38
|
+
if @queue.closed?
|
39
|
+
Appsignal.internal_logger.debug(
|
40
|
+
"Cannot transmit #{describe([event])}: AppSignal is stopped"
|
41
|
+
)
|
42
|
+
return
|
43
|
+
end
|
44
|
+
add_event(event)
|
45
|
+
# If we're not already waiting to be awakened from a scheduled
|
46
|
+
# debounce, schedule a short debounce, which will push the events
|
47
|
+
# to the queue and schedule a long debounce.
|
48
|
+
start_waker(INITIAL_DEBOUNCE_SECONDS) if @waker.nil?
|
49
|
+
|
50
|
+
Appsignal.internal_logger.debug(
|
51
|
+
"Scheduling #{describe([event])} to be transmitted"
|
52
|
+
)
|
53
|
+
|
54
|
+
# Make sure to start the thread after an event has been added.
|
55
|
+
@thread ||= Thread.new(&method(:run))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def stop
|
60
|
+
@mutex.synchronize do
|
61
|
+
# Flush all events before closing the queue.
|
62
|
+
push_events
|
63
|
+
rescue ClosedQueueError
|
64
|
+
# The queue is already closed (by a previous call to `#stop`)
|
65
|
+
# so it is not possible to push events to it anymore.
|
66
|
+
ensure
|
67
|
+
# Ensure calling `#stop` closes the queue and kills
|
68
|
+
# the waker thread, disallowing any further events from being
|
69
|
+
# scheduled with `#schedule`.
|
70
|
+
stop_waker
|
71
|
+
@queue.close
|
72
|
+
|
73
|
+
# Block until the thread has finished.
|
74
|
+
@thread&.join
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# @api private
|
79
|
+
# For internal testing purposes.
|
80
|
+
attr_reader :thread, :waker, :queue, :events, :transmitted
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def run
|
85
|
+
loop do
|
86
|
+
events = @queue.pop
|
87
|
+
break if events.nil?
|
88
|
+
|
89
|
+
transmit(events)
|
90
|
+
@transmitted += 1
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def transmit(events)
|
95
|
+
description = describe(events)
|
96
|
+
|
97
|
+
begin
|
98
|
+
response = CheckIn.transmitter.transmit(events, :format => :ndjson)
|
99
|
+
|
100
|
+
if (200...300).include?(response.code.to_i)
|
101
|
+
Appsignal.internal_logger.debug(
|
102
|
+
"Transmitted #{description}"
|
103
|
+
)
|
104
|
+
else
|
105
|
+
Appsignal.internal_logger.error(
|
106
|
+
"Failed to transmit #{description}: #{response.code} status code"
|
107
|
+
)
|
108
|
+
end
|
109
|
+
rescue => e
|
110
|
+
Appsignal.internal_logger.error("Failed to transmit #{description}: #{e.message}")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def describe(events)
|
115
|
+
if events.empty?
|
116
|
+
# This shouldn't happen.
|
117
|
+
"no check-in events"
|
118
|
+
elsif events.length > 1
|
119
|
+
"#{events.length} check-in events"
|
120
|
+
else
|
121
|
+
event = events.first
|
122
|
+
if event[:check_in_type] == "cron"
|
123
|
+
"cron check-in `#{event[:identifier] || "unknown"}` " \
|
124
|
+
"#{event[:kind] || "unknown"} event (digest #{event[:digest] || "unknown"})" \
|
125
|
+
else
|
126
|
+
"unknown check-in event"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Must be called from within a `@mutex.synchronize` block.
|
132
|
+
def add_event(event)
|
133
|
+
# Remove redundant events, keeping the newly added one, which
|
134
|
+
# should be the one with the most recent timestamp.
|
135
|
+
if event[:check_in_type] == "cron"
|
136
|
+
# Remove any existing cron check-in event with the same identifier,
|
137
|
+
# digest and kind as the one we're adding.
|
138
|
+
@events.reject! do |existing_event|
|
139
|
+
next unless existing_event[:identifier] == event[:identifier] &&
|
140
|
+
existing_event[:digest] == event[:digest] &&
|
141
|
+
existing_event[:kind] == event[:kind] &&
|
142
|
+
existing_event[:check_in_type] == "cron"
|
143
|
+
|
144
|
+
Appsignal.internal_logger.debug(
|
145
|
+
"Replacing previously scheduled #{describe([existing_event])}"
|
146
|
+
)
|
147
|
+
|
148
|
+
true
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
@events << event
|
153
|
+
end
|
154
|
+
|
155
|
+
# Must be called from within a `@mutex.synchronize` block.
|
156
|
+
def start_waker(debounce)
|
157
|
+
stop_waker
|
158
|
+
|
159
|
+
@waker = Thread.new do
|
160
|
+
sleep(debounce)
|
161
|
+
|
162
|
+
@mutex.synchronize do
|
163
|
+
# Make sure this waker doesn't get killed, so it can push
|
164
|
+
# events and schedule a new waker.
|
165
|
+
@waker = nil
|
166
|
+
push_events
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Must be called from within a `@mutex.synchronize` block.
|
172
|
+
def stop_waker
|
173
|
+
@waker&.kill
|
174
|
+
@waker&.join
|
175
|
+
@waker = nil
|
176
|
+
end
|
177
|
+
|
178
|
+
# Must be called from within a `@mutex.synchronize` block.
|
179
|
+
def push_events
|
180
|
+
return if @events.empty?
|
181
|
+
|
182
|
+
# Push a copy of the events to the queue, and clear the events array.
|
183
|
+
# This ensures that `@events` always contains events that have not
|
184
|
+
# yet been pushed to the queue.
|
185
|
+
@queue.push(@events.dup)
|
186
|
+
@events.clear
|
187
|
+
|
188
|
+
start_waker(BETWEEN_TRANSMISSIONS_DEBOUNCE_SECONDS)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
data/lib/appsignal/check_in.rb
CHANGED
@@ -39,8 +39,26 @@ module Appsignal
|
|
39
39
|
cron.finish
|
40
40
|
output
|
41
41
|
end
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
def transmitter
|
45
|
+
@transmitter ||= Transmitter.new(
|
46
|
+
"#{Appsignal.config[:logging_endpoint]}/check_ins/json"
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @api private
|
51
|
+
def scheduler
|
52
|
+
@scheduler ||= Scheduler.new
|
53
|
+
end
|
54
|
+
|
55
|
+
# @api private
|
56
|
+
def stop
|
57
|
+
scheduler&.stop
|
58
|
+
end
|
42
59
|
end
|
43
60
|
end
|
44
61
|
end
|
45
62
|
|
63
|
+
require "appsignal/check_in/scheduler"
|
46
64
|
require "appsignal/check_in/cron"
|
@@ -150,7 +150,7 @@ module Appsignal
|
|
150
150
|
ENV.fetch("APPSIGNAL_DIAGNOSE_ENDPOINT", DIAGNOSE_ENDPOINT),
|
151
151
|
Appsignal.config
|
152
152
|
)
|
153
|
-
response = transmitter.transmit(:diagnose => data)
|
153
|
+
response = transmitter.transmit({ :diagnose => data })
|
154
154
|
|
155
155
|
unless response.code == "200"
|
156
156
|
puts " Error: Something went wrong while submitting the report " \
|
data/lib/appsignal/config.rb
CHANGED
@@ -225,7 +225,7 @@ module Appsignal
|
|
225
225
|
# How to integrate AppSignal manually
|
226
226
|
def initialize(
|
227
227
|
root_path,
|
228
|
-
|
228
|
+
env,
|
229
229
|
logger = Appsignal.internal_logger
|
230
230
|
)
|
231
231
|
@root_path = root_path
|
@@ -234,8 +234,7 @@ module Appsignal
|
|
234
234
|
@logger = logger
|
235
235
|
@valid = false
|
236
236
|
|
237
|
-
@
|
238
|
-
@env = initial_env.to_s
|
237
|
+
@env = env.to_s
|
239
238
|
@config_hash = {}
|
240
239
|
@system_config = {}
|
241
240
|
@loaders_config = {}
|
@@ -270,7 +269,7 @@ module Appsignal
|
|
270
269
|
end
|
271
270
|
|
272
271
|
# Track origin of env
|
273
|
-
@initial_config[:env] = @
|
272
|
+
@initial_config[:env] = @env
|
274
273
|
|
275
274
|
# Load the config file if it exists
|
276
275
|
@file_config = load_from_disk || {}
|
@@ -560,14 +559,6 @@ module Appsignal
|
|
560
559
|
@config.root_path
|
561
560
|
end
|
562
561
|
|
563
|
-
def app_path=(_path)
|
564
|
-
Appsignal::Utils::StdoutAndLoggerMessage.warning \
|
565
|
-
"The `Appsignal.configure`'s `app_path=` writer is deprecated " \
|
566
|
-
"and can no longer be used to set the root path. " \
|
567
|
-
"Use the `Appsignal.configure`'s method `root_path` keyword argument " \
|
568
|
-
"to set the root path."
|
569
|
-
end
|
570
|
-
|
571
562
|
def env
|
572
563
|
@config.env
|
573
564
|
end
|
@@ -102,14 +102,9 @@ module Appsignal
|
|
102
102
|
|
103
103
|
private
|
104
104
|
|
105
|
-
IGNORED_ERRORS = [
|
106
|
-
# We don't need to alert Sidekiq job skip errors.
|
107
|
-
# This is an internal Sidekiq error.
|
108
|
-
"Sidekiq::JobRetry::Skip"
|
109
|
-
].freeze
|
110
|
-
|
111
105
|
def ignored_error?(error)
|
112
|
-
|
106
|
+
# We don't need to alert about Sidekiq job internal errors.
|
107
|
+
defined?(Sidekiq::JobRetry::Handled) && error.is_a?(Sidekiq::JobRetry::Handled)
|
113
108
|
end
|
114
109
|
|
115
110
|
def context_for(context)
|
@@ -57,6 +57,21 @@ module Appsignal
|
|
57
57
|
@transaction.set_error(error)
|
58
58
|
raise error
|
59
59
|
end
|
60
|
+
|
61
|
+
# Return whether the wrapped body responds to the method if this class does not.
|
62
|
+
# Based on:
|
63
|
+
# https://github.com/rack/rack/blob/0ed580bbe3858ffe5d530adf1bdad9ef9c03407c/lib/rack/body_proxy.rb#L16-L24
|
64
|
+
def respond_to_missing?(method_name, include_all = false)
|
65
|
+
super || @body.respond_to?(method_name, include_all)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Delegate missing methods to the wrapped body.
|
69
|
+
# Based on:
|
70
|
+
# https://github.com/rack/rack/blob/0ed580bbe3858ffe5d530adf1bdad9ef9c03407c/lib/rack/body_proxy.rb#L44-L61
|
71
|
+
def method_missing(method_name, *args, &block)
|
72
|
+
@body.__send__(method_name, *args, &block)
|
73
|
+
end
|
74
|
+
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
|
60
75
|
end
|
61
76
|
|
62
77
|
# The standard Rack body wrapper which exposes "each" for iterating
|
@@ -9,7 +9,8 @@ require "json"
|
|
9
9
|
module Appsignal
|
10
10
|
# @api private
|
11
11
|
class Transmitter
|
12
|
-
|
12
|
+
JSON_CONTENT_TYPE = "application/json; charset=UTF-8"
|
13
|
+
NDJSON_CONTENT_TYPE = "application/x-ndjson; charset=UTF-8"
|
13
14
|
|
14
15
|
HTTP_ERRORS = [
|
15
16
|
EOFError,
|
@@ -53,17 +54,39 @@ module Appsignal
|
|
53
54
|
end
|
54
55
|
end
|
55
56
|
|
56
|
-
def transmit(payload)
|
57
|
-
|
58
|
-
http_client.request(http_post(payload))
|
57
|
+
def transmit(payload, format: :json)
|
58
|
+
Appsignal.internal_logger.debug "Transmitting payload to #{uri}"
|
59
|
+
http_client.request(http_post(payload, :format => format))
|
59
60
|
end
|
60
61
|
|
61
62
|
private
|
62
63
|
|
63
|
-
def http_post(payload)
|
64
|
+
def http_post(payload, format: :json)
|
64
65
|
Net::HTTP::Post.new(uri.request_uri).tap do |request|
|
65
|
-
request["Content-Type"] =
|
66
|
-
request.body =
|
66
|
+
request["Content-Type"] = content_type_for(format)
|
67
|
+
request.body = generate_body_for(format, payload)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def content_type_for(format)
|
72
|
+
case format
|
73
|
+
when :json
|
74
|
+
JSON_CONTENT_TYPE
|
75
|
+
when :ndjson
|
76
|
+
NDJSON_CONTENT_TYPE
|
77
|
+
else
|
78
|
+
raise ArgumentError, "Unknown Content-Type header for format: #{format}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def generate_body_for(format, payload)
|
83
|
+
case format
|
84
|
+
when :json
|
85
|
+
Appsignal::Utils::JSON.generate(payload)
|
86
|
+
when :ndjson
|
87
|
+
Appsignal::Utils::NDJSON.generate(payload)
|
88
|
+
else
|
89
|
+
raise ArgumentError, "Unknown body generator for format: #{format}"
|
67
90
|
end
|
68
91
|
end
|
69
92
|
|
data/lib/appsignal/utils.rb
CHANGED
data/lib/appsignal/version.rb
CHANGED