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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 898cc61dd09079372dd341fcc77815283720d773a249c951383ac4be7ce031d8
4
- data.tar.gz: 2d3d8fa4dfe45a3a42c89955bd243cd2c7871b3f2ac2978e7bef8333b6518b5e
3
+ metadata.gz: 91e5aad04da2524d3d2f4bd983ecd76cf1a33380a1bc2a6bf8aab1bd4d91db5b
4
+ data.tar.gz: c7d0582debd5c6d9ed29f2239a4baeed92ebdad46606c423e7537495b4dea3ab
5
5
  SHA512:
6
- metadata.gz: 492ff15286cd7d5ce8064e57ca959dc9b308fc452237463a35b0d64d978bc1b6bff834b078af6a6a19fa0881e88ccb21d24c9f25d7f7a4c721435423d3b81d75
7
- data.tar.gz: 52e9a83f45fe345d895453e38833047c305f0b474918d8510148959a87a8e1ae797773213082152a03a19f8fd9cd7adfc37b5b0b6c33572f97fd9fdf3d67b5e1
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
- transmit_event("start")
15
+ CheckIn.scheduler.schedule(event("start"))
25
16
  end
26
17
 
27
18
  def finish
28
- transmit_event("finish")
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
@@ -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 " \
@@ -225,7 +225,7 @@ module Appsignal
225
225
  # How to integrate AppSignal manually
226
226
  def initialize(
227
227
  root_path,
228
- initial_env,
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
- @initial_env = initial_env
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] = @initial_env.to_s
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
@@ -35,7 +35,8 @@ module Appsignal
35
35
 
36
36
  IGNORED_ERRORS = [
37
37
  # Normal exits from the application we do not need to report
38
- SystemExit
38
+ SystemExit,
39
+ SignalException
39
40
  ].freeze
40
41
 
41
42
  def self.ignored_error?(error)
@@ -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
- IGNORED_ERRORS.include?(error.class.name)
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
- CONTENT_TYPE = "application/json; charset=UTF-8"
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
- config.logger.debug "Transmitting payload to #{uri}"
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"] = CONTENT_TYPE
66
- request.body = Appsignal::Utils::JSON.generate(payload)
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
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Utils
5
+ class NDJSON
6
+ class << self
7
+ def generate(body)
8
+ body.map do |element|
9
+ Appsignal::Utils::JSON.generate(element)
10
+ end.join("\n")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -12,4 +12,5 @@ require "appsignal/utils/data"
12
12
  require "appsignal/utils/hash_sanitizer"
13
13
  require "appsignal/utils/integration_logger"
14
14
  require "appsignal/utils/json"
15
+ require "appsignal/utils/ndjson"
15
16
  require "appsignal/utils/query_params_sanitizer"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "4.0.2"
4
+ VERSION = "4.0.4"
5
5
  end
data/lib/appsignal.rb CHANGED
@@ -164,6 +164,7 @@ module Appsignal
164
164
  end
165
165
  Appsignal::Extension.stop
166
166
  Appsignal::Probes.stop
167
+ Appsignal::CheckIn.stop
167
168
  end
168
169
 
169
170
  # Configure the AppSignal Ruby gem using a DSL.