appsignal 4.0.2-java → 4.0.4-java

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a43d932132633873f343b1a180772e9949115f85fcce3d9e13d9af6a23eb824
4
- data.tar.gz: 2d3d8fa4dfe45a3a42c89955bd243cd2c7871b3f2ac2978e7bef8333b6518b5e
3
+ metadata.gz: 4d56061a26addb877656ebb516f8cdd174feb64651cee8816f67e9eaa1ac946f
4
+ data.tar.gz: c7d0582debd5c6d9ed29f2239a4baeed92ebdad46606c423e7537495b4dea3ab
5
5
  SHA512:
6
- metadata.gz: a7f97a072db897dc6433d059067ad0bd0db054ec6d2b7a2f6c16ea9e8876e7b7f1b5ec3abdfba554fda60c6a211c8b3455a39221a9c7ebe8beb336a129fa0eec
7
- data.tar.gz: 52e9a83f45fe345d895453e38833047c305f0b474918d8510148959a87a8e1ae797773213082152a03a19f8fd9cd7adfc37b5b0b6c33572f97fd9fdf3d67b5e1
6
+ metadata.gz: f6710e1277f99b861d0c1981e300c21203c8ef91c3c49b23d39453b3970872e968f1eac140b9561684f1b569b7ab09406d352f06d37d852fa7c985ed858eb5d2
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.