statsd-instrument 3.9.8 → 3.9.10

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: 6aa9b8412d646528092adba3cbfcf173e80c4c9f6a69ce2a49db6c74d74fded6
4
- data.tar.gz: 38aaac366290965b067e2e167bffa030aa90d185e595e7c6ed1d687683607bb0
3
+ metadata.gz: d6bb36fa3a5254a64999335275c5515579febf9c2d92d35522fee1d883d0f56d
4
+ data.tar.gz: 69c96c4f5abaa8cae674a85e562a41ab3bbcf05cde0ef523c934d86846103658
5
5
  SHA512:
6
- metadata.gz: 32cbfe82af236e100778e9367ed1c7cf5a1b4c2ed4bed6969d1e319738eddfe192c0e521c2917ac4b4af5ac1e4e36f3f551828fa9fc69a06e9009d015726f465
7
- data.tar.gz: 0ac5714732d77dcda2ac89ea92dfd51ae99ed1cc021766eaed26e7b28310181c51c5a095ed545862be513a8b17348811715dff2760f6a9ae58dd431a5337080a
6
+ metadata.gz: 321874ee863b3f66e1eb7abbace4e734af55dc86fbd11aa8e009e398c54414f47d91447cdcbf08fb93bd4f7b1d88d58a16217093e9f8978b400187ed513dbd74
7
+ data.tar.gz: 9f34eed89dee5efc7d9a3d800c1d6824b0396054782694d708dab40180e0c3b423f6a0c2337b995bf6f58d72ae750bed564cf02bb5e13fc08518540e443be400
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.3.3
1
+ 3.4.4
data/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ section below.
6
6
 
7
7
  ## Unreleased changes
8
8
 
9
+ ## Version 3.9.10
10
+
11
+ - [#398](https://github.com/Shopify/statsd-instrument/pull/398) - Fix metrics not being sent from signal trap contexts when aggregation is enabled.
12
+ When the aggregator is enabled and metrics are emitted from within a signal handler (e.g., SIGTERM, SIGINT),
13
+ the thread health check would fail with `ThreadError: can't be called from trap context` due to mutex
14
+ synchronization. The aggregator now gracefully falls back to direct writes when called from a trap context,
15
+ ensuring metrics are not lost during signal handling such as graceful shutdowns.
16
+
17
+ ## Version 3.9.9
18
+
19
+ - [#392](https://github.com/Shopify/statsd-instrument/pull/392) - Prevent ENOBUFS errors when using UDP, by skipping setting socket buffer size.
20
+
9
21
  ## Version 3.9.8
10
22
 
11
23
  - [#390](https://github.com/Shopify/statsd-instrument/pull/391) - Fixing bug in Environment when using UDS. The max packet size option was not being passed to the
data/Gemfile CHANGED
@@ -19,4 +19,9 @@ platform :mri do
19
19
  if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.2")
20
20
  gem "vernier", require: false
21
21
  end
22
+
23
+ # From Ruby >= 3.5, logger is not part of the stdlib anymore
24
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.5")
25
+ gem "logger"
26
+ end
22
27
  end
@@ -278,6 +278,16 @@ module StatsD
278
278
  end
279
279
  true
280
280
  end
281
+ rescue ThreadError => e
282
+ # If we're in a trap context, we can't use mutex synchronization
283
+ # Fall back to direct writes to avoid losing metrics
284
+ if e.message.include?("can't be called from trap context")
285
+ StatsD.logger.debug { "[#{self.class.name}] In trap context, falling back to direct writes" }
286
+ false
287
+ else
288
+ # Re-raise other ThreadErrors
289
+ raise
290
+ end
281
291
  end
282
292
  end
283
293
  end
@@ -42,7 +42,7 @@ module StatsD
42
42
  original_socket
43
43
  rescue IOError => e
44
44
  StatsD.logger.debug do
45
- "[#{self.class.name}] Failed to create socket: #{e.class}: #{e.message}"
45
+ "[#{self.class.name}] Failed to setup socket: #{e.class}: #{e.message}"
46
46
  end
47
47
  nil
48
48
  end
@@ -25,11 +25,16 @@ module StatsD
25
25
 
26
26
  private
27
27
 
28
+ def setup_socket(original_socket)
29
+ original_socket
30
+ end
31
+
28
32
  def socket
29
33
  @socket ||= begin
30
- udp_socket = UDPSocket.new
34
+ family = Addrinfo.udp(host, port).afamily
35
+ udp_socket = UDPSocket.new(family)
31
36
  setup_socket(udp_socket)&.tap do |s|
32
- s.connect(@host, @port)
37
+ s.connect(host, port)
33
38
  end
34
39
  end
35
40
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module StatsD
4
4
  module Instrument
5
- VERSION = "3.9.8"
5
+ VERSION = "3.9.10"
6
6
  end
7
7
  end
@@ -362,7 +362,7 @@ module StatsD
362
362
  # @!method distribution(name, value = nil, sample_rate: nil, tags: nil, &block)
363
363
  # (see StatsD::Instrument::Client#distribution)
364
364
  #
365
- # @!method event(title, text, tags: nil, hostname: nil, timestamp: nil, aggregation_key: nil, priority: nil, source_type_name: nil, alert_type: nil) # rubocop:disable Layout/LineLength
365
+ # @!method event(title, text, tags: nil, hostname: nil, timestamp: nil, aggregation_key: nil, priority: nil, source_type_name: nil, alert_type: nil)
366
366
  # (see StatsD::Instrument::Client#event)
367
367
  #
368
368
  # @!method service_check(name, status, tags: nil, hostname: nil, timestamp: nil, message: nil)
@@ -353,4 +353,50 @@ class AggregatorTest < Minitest::Test
353
353
  assert_equal(100.0, sampled_timing_datagram.value)
354
354
  assert_equal(0.01, sampled_timing_datagram.sample_rate)
355
355
  end
356
+
357
+ def test_signal_trap_context_fallback_to_direct_writes
358
+ skip("#{RUBY_ENGINE} not supported for this test. Reason: signal handling") if RUBY_ENGINE != "ruby"
359
+
360
+ signal_received = false
361
+ metrics_sent_in_trap = []
362
+
363
+ old_trap = Signal.trap("USR1") do
364
+ signal_received = true
365
+ # These operations should now fall back to direct writes
366
+ @subject.increment("trap_counter", 1)
367
+ @subject.gauge("trap_gauge", 42)
368
+ @subject.aggregate_timing("trap_timing", 100)
369
+
370
+ metrics_sent_in_trap = @sink.datagrams.map(&:name)
371
+ end
372
+
373
+ @sink.clear
374
+
375
+ Process.kill("USR1", Process.pid)
376
+
377
+ sleep(0.1)
378
+
379
+ assert(signal_received, "Signal should have been received")
380
+
381
+ assert_includes(metrics_sent_in_trap, "trap_counter")
382
+ assert_includes(metrics_sent_in_trap, "trap_gauge")
383
+ assert_includes(metrics_sent_in_trap, "trap_timing")
384
+
385
+ counter_datagram = @sink.datagrams.find { |d| d.name == "trap_counter" }
386
+ assert_equal(1, counter_datagram.value)
387
+
388
+ gauge_datagram = @sink.datagrams.find { |d| d.name == "trap_gauge" }
389
+ assert_equal(42, gauge_datagram.value)
390
+
391
+ timing_datagram = @sink.datagrams.find { |d| d.name == "trap_timing" }
392
+ assert_equal([100.0], [timing_datagram.value].flatten)
393
+
394
+ debug_messages = @logger.messages.select { |m| m[:severity] == :debug }
395
+ assert(
396
+ debug_messages.any? { |m| m[:message].include?("In trap context, falling back to direct writes") },
397
+ "Expected debug message about trap context fallback",
398
+ )
399
+ ensure
400
+ Signal.trap("USR1", old_trap || "DEFAULT")
401
+ end
356
402
  end
@@ -48,21 +48,21 @@ class DispatcherStatsTest < Minitest::Test
48
48
  end
49
49
  assert_equal(batches.length, stats.instance_variable_get(:@batched_sends))
50
50
  assert_equal(
51
- batches.map { |b|
51
+ batches.map do |b|
52
52
  b[:buffer_len]
53
- }.sum / batches.length,
53
+ end.sum / batches.length,
54
54
  stats.instance_variable_get(:@avg_buffer_length),
55
55
  )
56
56
  assert_equal(
57
- batches.map { |b|
57
+ batches.map do |b|
58
58
  b[:packet_size]
59
- }.sum / batches.length,
59
+ end.sum / batches.length,
60
60
  stats.instance_variable_get(:@avg_batched_packet_size),
61
61
  )
62
62
  assert_equal(
63
- batches.map { |b|
63
+ batches.map do |b|
64
64
  b[:batch_len]
65
- }.sum / batches.length,
65
+ end.sum / batches.length,
66
66
  stats.instance_variable_get(:@avg_batch_length),
67
67
  )
68
68
  end
@@ -103,4 +103,56 @@ class IntegrationTest < Minitest::Test
103
103
  assert_match(/counter:\d+|c/, packets.find { |packet| packet.start_with?("counter:") })
104
104
  assert_match(/test_distribution:\d+:3|d/, packets.find { |packet| packet.start_with?("test_distribution:") })
105
105
  end
106
+
107
+ def test_signal_trap_with_aggregation_fallback
108
+ skip("#{RUBY_ENGINE} not supported for this test. Reason: signal handling") if RUBY_ENGINE != "ruby"
109
+
110
+ client = StatsD::Instrument::Environment.new(
111
+ "STATSD_ADDR" => "#{@server.addr[2]}:#{@server.addr[1]}",
112
+ "STATSD_IMPLEMENTATION" => "dogstatsd",
113
+ "STATSD_ENV" => "production",
114
+ "STATSD_ENABLE_AGGREGATION" => "true",
115
+ "STATSD_AGGREGATION_INTERVAL" => "5.0",
116
+ ).client
117
+
118
+ signal_received = false
119
+
120
+ old_trap = Signal.trap("USR1") do
121
+ signal_received = true
122
+ # These should fall back to direct writes
123
+ client.increment("trap_metric", 5)
124
+ client.gauge("trap_gauge", 42)
125
+ client.distribution("trap_distribution", 100)
126
+ end
127
+
128
+ Process.kill("USR1", Process.pid)
129
+
130
+ sleep(0.1)
131
+
132
+ assert(signal_received, "Signal should have been received")
133
+
134
+ packets = []
135
+ while IO.select([@server], nil, nil, 0.1)
136
+ packet = @server.recvfrom(300).first
137
+ packets.concat(packet.split("\n"))
138
+ end
139
+
140
+ # When aggregation is disabled due to trap context, metrics might be batched
141
+ assert(packets.size >= 3, "Expected at least 3 metrics, got #{packets.size}: #{packets.inspect}")
142
+
143
+ assert(
144
+ packets.any? { |p| p == "trap_metric:5|c" },
145
+ "Expected counter metric, got: #{packets.inspect}",
146
+ )
147
+ assert(
148
+ packets.any? { |p| p == "trap_gauge:42|g" },
149
+ "Expected gauge metric, got: #{packets.inspect}",
150
+ )
151
+ assert(
152
+ packets.any? { |p| p == "trap_distribution:100|d" },
153
+ "Expected distribution metric, got: #{packets.inspect}",
154
+ )
155
+ ensure
156
+ Signal.trap("USR1", old_trap || "DEFAULT")
157
+ end
106
158
  end
@@ -151,25 +151,11 @@ class UDPSinkTest < Minitest::Test
151
151
  seq = sequence("connect_fail_connect_succeed")
152
152
 
153
153
  # First attempt
154
- socket.expects(:setsockopt)
155
- .with(Socket::SOL_SOCKET, Socket::SO_SNDBUF, StatsD::Instrument::UdpConnection::DEFAULT_MAX_PACKET_SIZE)
156
- .in_sequence(seq)
157
- socket.expects(:getsockopt)
158
- .with(Socket::SOL_SOCKET, Socket::SO_SNDBUF)
159
- .returns(mock(int: StatsD::Instrument::UdpConnection::DEFAULT_MAX_PACKET_SIZE))
160
- .in_sequence(seq)
161
154
  socket.expects(:connect).with("localhost", 8125).in_sequence(seq)
162
155
  socket.expects(:send).raises(Errno::EDESTADDRREQ).in_sequence(seq)
163
156
  socket.expects(:close).in_sequence(seq)
164
157
 
165
158
  # Second attempt after error
166
- socket.expects(:setsockopt)
167
- .with(Socket::SOL_SOCKET, Socket::SO_SNDBUF, StatsD::Instrument::UdpConnection::DEFAULT_MAX_PACKET_SIZE)
168
- .in_sequence(seq)
169
- socket.expects(:getsockopt)
170
- .with(Socket::SOL_SOCKET, Socket::SO_SNDBUF)
171
- .returns(mock(int: StatsD::Instrument::UdpConnection::DEFAULT_MAX_PACKET_SIZE))
172
- .in_sequence(seq)
173
159
  socket.expects(:connect).with("localhost", 8125).in_sequence(seq)
174
160
  socket.expects(:send).twice.returns(1).in_sequence(seq)
175
161
  socket.expects(:close).in_sequence(seq)
metadata CHANGED
@@ -1,16 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statsd-instrument
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.9.8
4
+ version: 3.9.10
5
5
  platform: ruby
6
- original_platform: ''
7
6
  authors:
8
7
  - Jesse Storimer
9
8
  - Tobias Lutke
10
9
  - Willem van Bergen
11
10
  bindir: bin
12
11
  cert_chain: []
13
- date: 2024-12-19 00:00:00.000000000 Z
12
+ date: 1980-01-02 00:00:00.000000000 Z
14
13
  dependencies: []
15
14
  description: A StatsD client for Ruby apps. Provides metaprogramming methods to inject
16
15
  StatsD instrumentation into your code.
@@ -131,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
130
  - !ruby/object:Gem::Version
132
131
  version: '0'
133
132
  requirements: []
134
- rubygems_version: 3.6.1
133
+ rubygems_version: 3.6.9
135
134
  specification_version: 4
136
135
  summary: A StatsD client for Ruby apps
137
136
  test_files: