statsd-instrument 3.11.2 → 4.0.0

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: 51853f9d4b70fc39649e251e7e93ff3d77b2bcca1d776182f82de8219ea86886
4
- data.tar.gz: fce22a816682f0bc25510b6a68248a64c93ba983e7640b57ec01dbb0b985c9d2
3
+ metadata.gz: 5ba68947d3fa5cec0f604135dbf0fdda92ba4d056262ae2864e3144f8ad395ba
4
+ data.tar.gz: 63200c83b270a8d840eb88ba3d58a8b615e3f1d4c20aaacb15946ce745f42a90
5
5
  SHA512:
6
- metadata.gz: 4a06bd60427001260e206243f4db74ecb3d8225c1c5e3f8f9764239bc40901457cf621d7f5fbd0936d39fa0765145499672a9fb0a9c22025ac8611f7051f7154
7
- data.tar.gz: f74279e49141844c564d896e1d04b88b6b9f66585e56401a4ee52f7f4cb8dcc5fbe6a99b8427fbfa45ca1be843fef5a8c9357fb629129372880ac5020aceab31
6
+ metadata.gz: cf8edd51de1582c7b90e9ae07d74277ff093a6ba6d9e896f502bdfab6f94abe1c5d9fb1ed019a8cdf0a700774606595182086dee310659512ac18c17ff8efd16
7
+ data.tar.gz: de8619df52375bf5aa0a544ee18f5849b892ce82f2ae3ebac77c30ca62c5cb9851ff267aff286e0f8eff2b2ce9f225af925d1d6d8a56cb5a7a8fd4ff9371e744
data/CHANGELOG.md CHANGED
@@ -6,6 +6,10 @@ section below.
6
6
 
7
7
  ## Unreleased changes
8
8
 
9
+ ## Version 4.0.0
10
+
11
+ - Make the internal aggregator interface positional-only and injectable through `Client`, with fixed-arity `increment`, `gauge`, and `aggregate_timing` entrypoints for native aggregation backends. Public Client metric methods retain their keyword API.
12
+
9
13
  ## Version 3.11.2
10
14
 
11
15
  - Widen the `CompiledMetric` tag-combination cache key from 32 to 57 bits, eliminating
@@ -148,7 +148,7 @@ module StatsD
148
148
  # @param tags [Hash{String, Symbol => String},Array<String>] The tags to attach to the counter.
149
149
  # @param no_prefix [Boolean] If true, the metric will not be prefixed.
150
150
  # @return [void]
151
- def increment(name, value = 1, tags: [], no_prefix: false, sample_rate: CONST_SAMPLE_RATE)
151
+ def increment(name, value, tags, no_prefix, sample_rate)
152
152
  unless thread_healthcheck
153
153
  @sink << datagram_builder(no_prefix: no_prefix).c(name, value, sample_rate, tags)
154
154
  return
@@ -224,7 +224,7 @@ module StatsD
224
224
  end
225
225
  end
226
226
 
227
- def aggregate_timing(name, value, tags: [], no_prefix: false, type: DISTRIBUTION, sample_rate: CONST_SAMPLE_RATE)
227
+ def aggregate_timing(name, value, tags, no_prefix, type, sample_rate)
228
228
  unless thread_healthcheck
229
229
  @sink << datagram_builder(no_prefix: no_prefix).timing_value_packed(
230
230
  name, type.to_s, [value], sample_rate, tags
@@ -249,7 +249,7 @@ module StatsD
249
249
  do_flush(aggregation_state) if aggregation_state
250
250
  end
251
251
 
252
- def gauge(name, value, tags: [], no_prefix: false)
252
+ def gauge(name, value, tags, no_prefix)
253
253
  unless thread_healthcheck
254
254
  @sink << datagram_builder(no_prefix: no_prefix).g(name, value, CONST_SAMPLE_RATE, tags)
255
255
  return
@@ -19,6 +19,9 @@ module StatsD
19
19
  # @see StatsD.singleton_client
20
20
  # @see #clone_with_options
21
21
  class Client
22
+ EMPTY_TAGS = [].freeze
23
+ private_constant :EMPTY_TAGS
24
+
22
25
  class << self
23
26
  # Instantiates a StatsD::Instrument::Client using configuration values provided in
24
27
  # environment variables.
@@ -31,7 +34,8 @@ module StatsD
31
34
  default_tags: env.statsd_default_tags,
32
35
  implementation: env.statsd_implementation,
33
36
  sink: env.default_sink_for_environment,
34
- datagram_builder_class: datagram_builder_class_for_implementation(implementation)
37
+ datagram_builder_class: datagram_builder_class_for_implementation(implementation),
38
+ aggregator: nil
35
39
  )
36
40
  new(
37
41
  prefix: prefix,
@@ -42,6 +46,7 @@ module StatsD
42
46
  datagram_builder_class: datagram_builder_class,
43
47
  enable_aggregation: env.experimental_aggregation_enabled?,
44
48
  aggregation_flush_interval: env.aggregation_interval,
49
+ aggregator: aggregator,
45
50
  )
46
51
  end
47
52
 
@@ -147,6 +152,16 @@ module StatsD
147
152
  end
148
153
 
149
154
  # Instantiates a new client.
155
+ #
156
+ # Aggregators use fixed-arity positional +increment+, +gauge+, and
157
+ # +aggregate_timing+ methods. This keeps the aggregation hot path simple
158
+ # and avoids keyword argument forwarding. An injected aggregator is also
159
+ # expected to implement the existing precompiled aggregation methods and
160
+ # +flush+. Calls without tags receive a shared frozen empty array. Other tag
161
+ # values are forwarded as-is; custom backends may require arrays.
162
+ #
163
+ # @param aggregator [#increment, #gauge, #aggregate_timing, nil]
164
+ # Optional external aggregation backend.
150
165
  # @see .from_env to instantiate a client using environment variables.
151
166
  def initialize(
152
167
  prefix: nil,
@@ -157,7 +172,8 @@ module StatsD
157
172
  datagram_builder_class: self.class.datagram_builder_class_for_implementation(implementation),
158
173
  enable_aggregation: false,
159
174
  aggregation_flush_interval: 2.0,
160
- aggregation_max_context_size: StatsD::Instrument::Aggregator::DEFAULT_MAX_CONTEXT_SIZE
175
+ aggregation_max_context_size: StatsD::Instrument::Aggregator::DEFAULT_MAX_CONTEXT_SIZE,
176
+ aggregator: nil
161
177
  )
162
178
  @sink = sink
163
179
  @datagram_builder_class = datagram_builder_class
@@ -167,10 +183,11 @@ module StatsD
167
183
  @default_sample_rate = default_sample_rate
168
184
 
169
185
  @datagram_builder = { false => nil, true => nil }
170
- @enable_aggregation = enable_aggregation
186
+ @injected_aggregator = aggregator
187
+ @enable_aggregation = enable_aggregation || !aggregator.nil?
171
188
  @aggregation_flush_interval = aggregation_flush_interval
172
189
  if @enable_aggregation
173
- @aggregator =
190
+ @aggregator = aggregator ||
174
191
  Aggregator.new(
175
192
  @sink,
176
193
  datagram_builder_class,
@@ -223,7 +240,7 @@ module StatsD
223
240
  return StatsD::Instrument::VOID if sample_rate && !sample?(sample_rate)
224
241
 
225
242
  if @enable_aggregation
226
- @aggregator.increment(name, value, tags: tags, no_prefix: no_prefix, sample_rate: sample_rate)
243
+ @aggregator.increment(name, value, tags || EMPTY_TAGS, no_prefix, sample_rate)
227
244
  else
228
245
  emit(datagram_builder(no_prefix: no_prefix).c(name, value, sample_rate, tags))
229
246
  end
@@ -357,7 +374,7 @@ module StatsD
357
374
  end
358
375
 
359
376
  if @enable_aggregation
360
- @aggregator.aggregate_timing(name, value, tags: tags, no_prefix: no_prefix, type: :ms, sample_rate: sample_rate)
377
+ @aggregator.aggregate_timing(name, value, tags || EMPTY_TAGS, no_prefix, :ms, sample_rate)
361
378
  return StatsD::Instrument::VOID
362
379
  end
363
380
  emit(datagram_builder(no_prefix: no_prefix).ms(name, value, sample_rate, tags))
@@ -379,7 +396,7 @@ module StatsD
379
396
  # @return [void]
380
397
  def gauge(name, value, sample_rate: nil, tags: nil, no_prefix: false)
381
398
  if @enable_aggregation
382
- @aggregator.gauge(name, value, tags: tags, no_prefix: no_prefix)
399
+ @aggregator.gauge(name, value, tags || EMPTY_TAGS, no_prefix)
383
400
  return StatsD::Instrument::VOID
384
401
  end
385
402
 
@@ -431,14 +448,7 @@ module StatsD
431
448
  end
432
449
 
433
450
  if @enable_aggregation
434
- @aggregator.aggregate_timing(
435
- name,
436
- value,
437
- tags: tags,
438
- no_prefix: no_prefix,
439
- type: :d,
440
- sample_rate: sample_rate,
441
- )
451
+ @aggregator.aggregate_timing(name, value, tags || EMPTY_TAGS, no_prefix, :d, sample_rate)
442
452
  return StatsD::Instrument::VOID
443
453
  end
444
454
 
@@ -467,7 +477,7 @@ module StatsD
467
477
  end
468
478
 
469
479
  if @enable_aggregation
470
- @aggregator.aggregate_timing(name, value, tags: tags, no_prefix: no_prefix, type: :h, sample_rate: sample_rate)
480
+ @aggregator.aggregate_timing(name, value, tags || EMPTY_TAGS, no_prefix, :h, sample_rate)
471
481
  return StatsD::Instrument::VOID
472
482
  end
473
483
 
@@ -508,10 +518,10 @@ module StatsD
508
518
  @aggregator.aggregate_timing(
509
519
  name,
510
520
  latency_in_ms,
511
- tags: tags,
512
- no_prefix: no_prefix,
513
- type: metric_type,
514
- sample_rate: sample_rate,
521
+ tags || EMPTY_TAGS,
522
+ no_prefix,
523
+ metric_type,
524
+ sample_rate,
515
525
  )
516
526
  else
517
527
  emit(datagram_builder(no_prefix: no_prefix).send(metric_type, name, latency_in_ms, sample_rate, tags))
@@ -598,7 +608,8 @@ module StatsD
598
608
  prefix: NO_CHANGE,
599
609
  default_sample_rate: NO_CHANGE,
600
610
  default_tags: NO_CHANGE,
601
- datagram_builder_class: NO_CHANGE
611
+ datagram_builder_class: NO_CHANGE,
612
+ aggregator: NO_CHANGE
602
613
  )
603
614
  client = clone_with_options(
604
615
  sink: sink,
@@ -606,6 +617,7 @@ module StatsD
606
617
  default_sample_rate: default_sample_rate,
607
618
  default_tags: default_tags,
608
619
  datagram_builder_class: datagram_builder_class,
620
+ aggregator: aggregator,
609
621
  )
610
622
 
611
623
  yield(client)
@@ -616,7 +628,8 @@ module StatsD
616
628
  prefix: NO_CHANGE,
617
629
  default_sample_rate: NO_CHANGE,
618
630
  default_tags: NO_CHANGE,
619
- datagram_builder_class: NO_CHANGE
631
+ datagram_builder_class: NO_CHANGE,
632
+ aggregator: NO_CHANGE
620
633
  )
621
634
  self.class.new(
622
635
  sink: sink == NO_CHANGE ? @sink : sink,
@@ -627,6 +640,7 @@ module StatsD
627
640
  datagram_builder_class == NO_CHANGE ? @datagram_builder_class : datagram_builder_class,
628
641
  enable_aggregation: @enable_aggregation,
629
642
  aggregation_flush_interval: @aggregation_flush_interval,
643
+ aggregator: aggregator == NO_CHANGE ? @injected_aggregator : aggregator,
630
644
  )
631
645
  end
632
646
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module StatsD
4
4
  module Instrument
5
- VERSION = "3.11.2"
5
+ VERSION = "4.0.0"
6
6
  end
7
7
  end
@@ -36,8 +36,8 @@ class AggregatorTest < Minitest::Test
36
36
  end
37
37
 
38
38
  def test_increment_simple
39
- @subject.increment("foo", 1, tags: { foo: "bar" })
40
- @subject.increment("foo", 1, tags: { foo: "bar" })
39
+ @subject.increment("foo", 1, ["foo:bar"], false, 1.0)
40
+ @subject.increment("foo", 1, ["foo:bar"], false, 1.0)
41
41
  @subject.flush
42
42
 
43
43
  datagram = @sink.datagrams.first
@@ -48,10 +48,9 @@ class AggregatorTest < Minitest::Test
48
48
  end
49
49
 
50
50
  def test_increment_with_sample_rate
51
- # Test that increment properly passes through sample_rate
52
- @subject.increment("counter.sampled", 1, tags: { foo: "bar" }, sample_rate: 0.5)
53
- @subject.increment("counter.sampled", 2, tags: { foo: "bar" }, sample_rate: 0.5)
54
- @subject.increment("counter.unsampled", 1, tags: { foo: "bar" })
51
+ @subject.increment("counter.sampled", 1, ["foo:bar"], false, 0.5)
52
+ @subject.increment("counter.sampled", 2, ["foo:bar"], false, 0.5)
53
+ @subject.increment("counter.unsampled", 1, ["foo:bar"], false, 1.0)
55
54
  @subject.flush
56
55
 
57
56
  assert_equal(2, @sink.datagrams.size)
@@ -68,11 +67,10 @@ class AggregatorTest < Minitest::Test
68
67
  end
69
68
 
70
69
  def test_increment_different_sample_rates_create_different_aggregation_keys
71
- # Counters with different sample rates should be aggregated separately
72
- @subject.increment("counter", 1, sample_rate: 0.5)
73
- @subject.increment("counter", 2, sample_rate: 0.5)
74
- @subject.increment("counter", 10, sample_rate: 0.1)
75
- @subject.increment("counter", 20, sample_rate: 0.1)
70
+ @subject.increment("counter", 1, [], false, 0.5)
71
+ @subject.increment("counter", 2, [], false, 0.5)
72
+ @subject.increment("counter", 10, [], false, 0.1)
73
+ @subject.increment("counter", 20, [], false, 0.1)
76
74
  @subject.flush
77
75
 
78
76
  assert_equal(2, @sink.datagrams.size)
@@ -85,8 +83,8 @@ class AggregatorTest < Minitest::Test
85
83
  end
86
84
 
87
85
  def test_distribution_simple
88
- @subject.aggregate_timing("foo", 1, tags: { foo: "bar" })
89
- @subject.aggregate_timing("foo", 100, tags: { foo: "bar" })
86
+ @subject.aggregate_timing("foo", 1, ["foo:bar"], false, :d, 1.0)
87
+ @subject.aggregate_timing("foo", 100, ["foo:bar"], false, :d, 1.0)
90
88
  @subject.flush
91
89
 
92
90
  datagram = @sink.datagrams.first
@@ -96,9 +94,9 @@ class AggregatorTest < Minitest::Test
96
94
  end
97
95
 
98
96
  def test_timing_sampling_scaling
99
- @subject.aggregate_timing("timing.sampled", 60.0, sample_rate: 0.01)
100
- @subject.aggregate_timing("timing.sampled", 80.0, sample_rate: 0.01)
101
- @subject.aggregate_timing("timing.unsampled", 60.0, sample_rate: 1.0)
97
+ @subject.aggregate_timing("timing.sampled", 60.0, [], false, :d, 0.01)
98
+ @subject.aggregate_timing("timing.sampled", 80.0, [], false, :d, 0.01)
99
+ @subject.aggregate_timing("timing.unsampled", 60.0, [], false, :d, 1.0)
102
100
 
103
101
  @subject.flush
104
102
 
@@ -114,11 +112,11 @@ class AggregatorTest < Minitest::Test
114
112
  end
115
113
 
116
114
  def test_mixed_type_timings
117
- @subject.aggregate_timing("foo_ms", 1, tags: { foo: "bar" }, type: :ms)
118
- @subject.aggregate_timing("foo_ms", 100, tags: { foo: "bar" }, type: :ms)
115
+ @subject.aggregate_timing("foo_ms", 1, ["foo:bar"], false, :ms, 1.0)
116
+ @subject.aggregate_timing("foo_ms", 100, ["foo:bar"], false, :ms, 1.0)
119
117
 
120
- @subject.aggregate_timing("foo_d", 100, tags: { foo: "bar" }, type: :d)
121
- @subject.aggregate_timing("foo_d", 120, tags: { foo: "bar" }, type: :d)
118
+ @subject.aggregate_timing("foo_d", 100, ["foo:bar"], false, :d, 1.0)
119
+ @subject.aggregate_timing("foo_d", 120, ["foo:bar"], false, :d, 1.0)
122
120
 
123
121
  @subject.flush
124
122
 
@@ -130,8 +128,8 @@ class AggregatorTest < Minitest::Test
130
128
  end
131
129
 
132
130
  def test_gauge_simple
133
- @subject.gauge("foo", 1, tags: { foo: "bar" })
134
- @subject.gauge("foo", 100, tags: { foo: "bar" })
131
+ @subject.gauge("foo", 1, ["foo:bar"], false)
132
+ @subject.gauge("foo", 100, ["foo:bar"], false)
135
133
  @subject.flush
136
134
 
137
135
  datagram = @sink.datagrams.first
@@ -141,18 +139,18 @@ class AggregatorTest < Minitest::Test
141
139
  end
142
140
 
143
141
  def test_increment_with_tags_in_different_orders
144
- @subject.increment("foo", 1, tags: ["tag1:val1", "tag2:val2"])
145
- @subject.increment("foo", 1, tags: ["tag2:val2", "tag1:val1"])
142
+ @subject.increment("foo", 1, ["tag1:val1", "tag2:val2"], false, 1.0)
143
+ @subject.increment("foo", 1, ["tag2:val2", "tag1:val1"], false, 1.0)
146
144
  @subject.flush
147
145
 
148
146
  assert_equal(2, @sink.datagrams.first.value)
149
147
  end
150
148
 
151
149
  def test_increment_with_different_tag_values
152
- @subject.increment("foo", 1, tags: ["tag1:val1", "tag2:val2"])
153
- @subject.increment("foo", 1, tags: { tag1: "val1", tag2: "val2" })
150
+ @subject.increment("foo", 1, ["tag1:val1", "tag2:val2"], false, 1.0)
151
+ @subject.increment("foo", 1, { tag1: "val1", tag2: "val2" }, false, 1.0)
154
152
 
155
- @subject.increment("bar")
153
+ @subject.increment("bar", 1, [], false, 1.0)
156
154
  @subject.flush
157
155
 
158
156
  assert_equal(2, @sink.datagrams.first.value)
@@ -161,8 +159,8 @@ class AggregatorTest < Minitest::Test
161
159
  end
162
160
 
163
161
  def test_increment_with_different_metric_names
164
- @subject.increment("foo", 1, tags: ["tag1:val1", "tag2:val2"])
165
- @subject.increment("bar", 1, tags: ["tag1:val1", "tag2:val2"])
162
+ @subject.increment("foo", 1, ["tag1:val1", "tag2:val2"], false, 1.0)
163
+ @subject.increment("bar", 1, ["tag1:val1", "tag2:val2"], false, 1.0)
166
164
  @subject.flush
167
165
 
168
166
  assert_equal(1, @sink.datagrams.find { |d| d.name == "foo" }.value)
@@ -170,22 +168,22 @@ class AggregatorTest < Minitest::Test
170
168
  end
171
169
 
172
170
  def test_increment_with_different_values
173
- @subject.increment("foo", 1, tags: ["tag1:val1", "tag2:val2"])
174
- @subject.increment("foo", 2, tags: ["tag1:val1", "tag2:val2"])
171
+ @subject.increment("foo", 1, ["tag1:val1", "tag2:val2"], false, 1.0)
172
+ @subject.increment("foo", 2, ["tag1:val1", "tag2:val2"], false, 1.0)
175
173
  @subject.flush
176
174
 
177
175
  assert_equal(3, @sink.datagrams.first.value)
178
176
  end
179
177
 
180
178
  def test_send_mixed_types_will_pass_through
181
- @subject.increment("test_counter", 1, tags: ["tag1:val1", "tag2:val2"])
182
- @subject.aggregate_timing("test_counter", 100, tags: ["tag1:val1", "tag2:val2"])
179
+ @subject.increment("test_counter", 1, ["tag1:val1", "tag2:val2"], false, 1.0)
180
+ @subject.aggregate_timing("test_counter", 100, ["tag1:val1", "tag2:val2"], false, :d, 1.0)
183
181
 
184
- @subject.gauge("test_gauge", 100, tags: ["tag1:val1", "tag2:val2"])
185
- @subject.increment("test_gauge", 1, tags: ["tag1:val1", "tag2:val2"])
182
+ @subject.gauge("test_gauge", 100, ["tag1:val1", "tag2:val2"], false)
183
+ @subject.increment("test_gauge", 1, ["tag1:val1", "tag2:val2"], false, 1.0)
186
184
 
187
- @subject.aggregate_timing("test_timing", 100, tags: ["tag1:val1", "tag2:val2"])
188
- @subject.gauge("test_timing", 100, tags: ["tag1:val1", "tag2:val2"])
185
+ @subject.aggregate_timing("test_timing", 100, ["tag1:val1", "tag2:val2"], false, :d, 1.0)
186
+ @subject.gauge("test_timing", 100, ["tag1:val1", "tag2:val2"], false)
189
187
  @subject.flush
190
188
 
191
189
  assert_equal(6, @sink.datagrams.size)
@@ -202,10 +200,10 @@ class AggregatorTest < Minitest::Test
202
200
  def test_with_prefix
203
201
  aggregator = StatsD::Instrument::Aggregator.new(@sink, StatsD::Instrument::DatagramBuilder, "MyApp", [])
204
202
 
205
- aggregator.increment("foo", 1, tags: ["tag1:val1", "tag2:val2"])
206
- aggregator.increment("foo", 1, tags: ["tag1:val1", "tag2:val2"])
203
+ aggregator.increment("foo", 1, ["tag1:val1", "tag2:val2"], false, 1.0)
204
+ aggregator.increment("foo", 1, ["tag1:val1", "tag2:val2"], false, 1.0)
207
205
 
208
- aggregator.increment("foo", 1, tags: ["tag1:val1", "tag2:val2"], no_prefix: true)
206
+ aggregator.increment("foo", 1, ["tag1:val1", "tag2:val2"], true, 1.0)
209
207
  aggregator.flush
210
208
 
211
209
  assert_equal(2, @sink.datagrams.size)
@@ -217,14 +215,11 @@ class AggregatorTest < Minitest::Test
217
215
  end
218
216
 
219
217
  def test_finalizer_with_prefix
220
- # Test that the finalizer correctly uses the prefix when flushing metrics
221
- # IMPORTANT: Use empty tags to ensure datagram_builder is not pre-created by tags_sorted
222
218
  aggregator = StatsD::Instrument::Aggregator.new(@sink, StatsD::Instrument::DatagramBuilder, "MyApp", [])
223
219
 
224
- aggregator.increment("foo", 1, tags: [])
225
- aggregator.increment("bar", 1, tags: [], no_prefix: true)
220
+ aggregator.increment("foo", 1, [], false, 1.0)
221
+ aggregator.increment("bar", 1, [], true, 1.0)
226
222
 
227
- # Manually trigger the finalizer (simulates GC cleanup)
228
223
  finalizer = StatsD::Instrument::Aggregator.finalize(
229
224
  aggregator.instance_variable_get(:@finalizer),
230
225
  )
@@ -242,18 +237,15 @@ class AggregatorTest < Minitest::Test
242
237
  end
243
238
 
244
239
  def test_synchronous_operation_on_thread_failure
245
- # Force thread_healthcheck to return false
246
240
  @subject.stubs(:thread_healthcheck).returns(false)
247
241
 
248
- # Stub methods on @aggregation_state to ensure they are not called
249
242
  aggregation_state = @subject.instance_variable_get(:@aggregation_state)
250
243
  aggregation_state.stubs(:[]=).never
251
244
 
252
- @subject.increment("foo", 1, tags: { foo: "bar" })
253
- @subject.aggregate_timing("bar", 100, tags: { foo: "bar" })
254
- @subject.gauge("baz", 100, tags: { foo: "bar" })
245
+ @subject.increment("foo", 1, ["foo:bar"], false, 1.0)
246
+ @subject.aggregate_timing("bar", 100, ["foo:bar"], false, :d, 1.0)
247
+ @subject.gauge("baz", 100, ["foo:bar"], false)
255
248
 
256
- # Verify metrics were sent immediately
257
249
  assert_equal(3, @sink.datagrams.size)
258
250
 
259
251
  counter_datagram = @sink.datagrams.find { |d| d.name == "foo" }
@@ -268,11 +260,9 @@ class AggregatorTest < Minitest::Test
268
260
  assert_equal(100, gauge_datagram.value)
269
261
  assert_equal(["foo:bar"], gauge_datagram.tags)
270
262
 
271
- # Additional metrics should also go through synchronously
272
- @subject.increment("foo", 1, tags: { foo: "bar" })
273
- @subject.aggregate_timing("bar", 200, tags: { foo: "bar" }, sample_rate: 0.5)
263
+ @subject.increment("foo", 1, ["foo:bar"], false, 1.0)
264
+ @subject.aggregate_timing("bar", 200, ["foo:bar"], false, :d, 0.5)
274
265
 
275
- # Verify new metrics were also sent immediately
276
266
  assert_equal(5, @sink.datagrams.size)
277
267
 
278
268
  counter_datagram = @sink.datagrams.select { |d| d.name == "foo" }.last
@@ -287,65 +277,52 @@ class AggregatorTest < Minitest::Test
287
277
  after_aggregation_state = @subject.instance_variable_get(:@aggregation_state)
288
278
  assert_same(after_aggregation_state, aggregation_state)
289
279
 
290
- # undo the stubbing
291
280
  @subject.unstub(:thread_healthcheck)
292
281
  end
293
282
 
294
283
  def test_recreate_thread_after_fork
295
284
  skip("#{RUBY_ENGINE} not supported for this test. Reason: fork()") if RUBY_ENGINE != "ruby"
296
- # Record initial metrics
297
- @subject.increment("foo", 1, tags: { foo: "bar" })
298
- @subject.aggregate_timing("bar", 100, tags: { foo: "bar" })
285
+ @subject.increment("foo", 1, ["foo:bar"], false, 1.0)
286
+ @subject.aggregate_timing("bar", 100, ["foo:bar"], false, :d, 1.0)
299
287
 
300
- # kill the flush thread
301
288
  @subject.instance_variable_get(:@flush_thread).kill
302
289
 
303
- # Fork the process
304
290
  pid = Process.fork do
305
- # In forked process, send more metrics
306
- @subject.increment("foo", 2, tags: { foo: "bar" })
307
- @subject.aggregate_timing("bar", 200, tags: { foo: "bar" })
291
+ @subject.increment("foo", 2, ["foo:bar"], false, 1.0)
292
+ @subject.aggregate_timing("bar", 200, ["foo:bar"], false, :d, 1.0)
308
293
  @subject.flush
309
294
 
310
295
  assert_equal(2, @sink.datagrams.size)
311
296
  exit!
312
297
  end
313
298
 
314
- # Wait for forked process to complete
315
299
  Process.wait(pid)
316
300
 
317
- # Send metrics in parent process
318
- @subject.increment("foo", 3, tags: { foo: "bar" })
319
- @subject.aggregate_timing("bar", 300, tags: { foo: "bar" })
301
+ @subject.increment("foo", 3, ["foo:bar"], false, 1.0)
302
+ @subject.aggregate_timing("bar", 300, ["foo:bar"], false, :d, 1.0)
320
303
  @subject.flush
321
304
 
322
305
  assert_equal(2, @sink.datagrams.size)
323
306
 
324
- # Verify metrics were properly aggregated in parent process
325
307
  counter_datagrams = @sink.datagrams.select { |d| d.name == "foo" }
326
308
  timing_datagrams = @sink.datagrams.select { |d| d.name == "bar" }
327
309
 
328
310
  assert_equal(1, counter_datagrams.size)
329
311
  assert_equal(1, timing_datagrams.size)
330
312
 
331
- # Aggregate despite fork
332
313
  assert_equal(4, counter_datagrams.last.value)
333
314
  assert_equal([100.0, 300.0], timing_datagrams.last.value)
334
315
  end
335
316
 
336
317
  def test_race_condition_during_forking
337
318
  skip("#{RUBY_ENGINE} not supported for this test. Reason: fork()") if RUBY_ENGINE != "ruby"
338
- # Record initial metrics
339
- @subject.increment("before_fork.count", 1, tags: { foo: "bar" })
340
- @subject.aggregate_timing("before_fork.timing", 100, tags: { foo: "bar" })
319
+ @subject.increment("before_fork.count", 1, ["foo:bar"], false, 1.0)
320
+ @subject.aggregate_timing("before_fork.timing", 100, ["foo:bar"], false, :d, 1.0)
341
321
 
342
- # Fork the process
343
322
  pid = Process.fork do
344
- # In forked process, send more metrics
345
- @subject.increment("in_child.count", 2, tags: { foo: "bar" })
346
- @subject.aggregate_timing("in_child.timing", 200, tags: { foo: "bar" })
323
+ @subject.increment("in_child.count", 2, ["foo:bar"], false, 1.0)
324
+ @subject.aggregate_timing("in_child.timing", 200, ["foo:bar"], false, :d, 1.0)
347
325
 
348
- # Simulate thread waiting for flush
349
326
  sleep(0.1)
350
327
  @subject.flush
351
328
 
@@ -353,20 +330,16 @@ class AggregatorTest < Minitest::Test
353
330
  exit!
354
331
  end
355
332
 
356
- # Call flush concurrently in parent process
357
333
  @subject.flush
358
334
 
359
- # Wait for forked process to complete
360
335
  Process.wait(pid)
361
336
 
362
- # Send metrics in parent process
363
- @subject.increment("after_fork.count", 3, tags: { foo: "bar" })
364
- @subject.aggregate_timing("after_fork.timing", 300, tags: { foo: "bar" })
337
+ @subject.increment("after_fork.count", 3, ["foo:bar"], false, 1.0)
338
+ @subject.aggregate_timing("after_fork.timing", 300, ["foo:bar"], false, :d, 1.0)
365
339
  @subject.flush
366
340
 
367
341
  assert_equal(4, @sink.datagrams.size)
368
342
 
369
- # Verify metrics were properly aggregated in parent process
370
343
  counter_datagrams = @sink.datagrams.select { |d| d.name == "before_fork.count" }
371
344
  timing_datagrams = @sink.datagrams.select { |d| d.name == "before_fork.count" }
372
345
  assert_equal(
@@ -376,7 +349,6 @@ class AggregatorTest < Minitest::Test
376
349
  )
377
350
  assert_equal(1, timing_datagrams.size)
378
351
 
379
- # After fork metrics
380
352
  counter_datagrams = @sink.datagrams.select { |d| d.name == "after_fork.count" }
381
353
  timing_datagrams = @sink.datagrams.select { |d| d.name == "after_fork.count" }
382
354
  assert_equal(1, counter_datagrams.size)
@@ -384,18 +356,16 @@ class AggregatorTest < Minitest::Test
384
356
  end
385
357
 
386
358
  def test_finalizer_flushes_pending_metrics
387
- @subject.increment("foo", 1, tags: { foo: "bar" })
388
- @subject.aggregate_timing("bar", 100, tags: { foo: "bar" })
389
- @subject.gauge("baz", 100, tags: { foo: "bar" })
390
- @subject.aggregate_timing("sampled_timing", 100, tags: { foo: "bar" }, sample_rate: 0.01)
359
+ @subject.increment("foo", 1, ["foo:bar"], false, 1.0)
360
+ @subject.aggregate_timing("bar", 100, ["foo:bar"], false, :d, 1.0)
361
+ @subject.gauge("baz", 100, ["foo:bar"], false)
362
+ @subject.aggregate_timing("sampled_timing", 100, ["foo:bar"], false, :d, 0.01)
391
363
 
392
- # Manually trigger the finalizer
393
364
  finalizer = StatsD::Instrument::Aggregator.finalize(
394
365
  @subject.instance_variable_get(:@finalizer),
395
366
  )
396
367
  finalizer.call
397
368
 
398
- # Verify that all pending metrics are sent
399
369
  assert_equal(4, @sink.datagrams.size)
400
370
 
401
371
  counter_datagram = @sink.datagrams.find { |d| d.name == "foo" }
@@ -418,13 +388,11 @@ class AggregatorTest < Minitest::Test
418
388
  def test_aggregator_finalizer_is_called_on_gc
419
389
  skip_on_jruby("JRuby's GC does not guarantee finalizers are called promptly")
420
390
 
421
- 5.times { @subject.increment("foo", 1, tags: { foo: "bar" }) }
391
+ 5.times { @subject.increment("foo", 1, ["foo:bar"], false, 1.0) }
422
392
 
423
393
  @subject.instance_variable_get(:@flush_thread)&.kill
424
- # Unbind the aggregator to allow GC to collect it.
425
394
  @subject = nil
426
395
 
427
- # We expect the finalizer to flush the aggregated metrics.
428
396
  assert_empty(@sink.datagrams)
429
397
 
430
398
  5.times do
@@ -441,10 +409,11 @@ class AggregatorTest < Minitest::Test
441
409
  end
442
410
 
443
411
  def test_aggregator_finalizer_is_called_on_gc_after_aggregation_state_move
444
- (StatsD::Instrument::Aggregator::DEFAULT_MAX_CONTEXT_SIZE + 5).times { @subject.aggregate_timing("foo", 1) }
412
+ (StatsD::Instrument::Aggregator::DEFAULT_MAX_CONTEXT_SIZE + 5).times do
413
+ @subject.aggregate_timing("foo", 1, [], false, :d, 1.0)
414
+ end
445
415
  assert_equal(1, @sink.datagrams.size)
446
416
 
447
- # Unbind the aggregator to allow GC to collect it.
448
417
  @subject = nil
449
418
  5.times do
450
419
  GC.start(full_mark: true, immediate_mark: true, immediate_sweep: true)
@@ -468,10 +437,9 @@ class AggregatorTest < Minitest::Test
468
437
 
469
438
  old_trap = Signal.trap("USR1") do
470
439
  signal_received = true
471
- # These operations should now fall back to direct writes
472
- @subject.increment("trap_counter", 1)
473
- @subject.gauge("trap_gauge", 42)
474
- @subject.aggregate_timing("trap_timing", 100)
440
+ @subject.increment("trap_counter", 1, [], false, 1.0)
441
+ @subject.gauge("trap_gauge", 42, [], false)
442
+ @subject.aggregate_timing("trap_timing", 100, [], false, :d, 1.0)
475
443
 
476
444
  metrics_sent_in_trap = @sink.datagrams.map(&:name)
477
445
  end
data/test/client_test.rb CHANGED
@@ -102,6 +102,70 @@ class ClientTest < Minitest::Test
102
102
  assert_equal(true, !datagram.nil?)
103
103
  end
104
104
 
105
+ def test_aggregation_preserves_block_timing_and_return_value
106
+ sink = StatsD::Instrument::CaptureSink.new(parent: StatsD::Instrument::NullSink.new)
107
+ client = StatsD::Instrument::Client.new(sink: sink, enable_aggregation: true)
108
+ Process.stubs(:clock_gettime).with(Process::CLOCK_MONOTONIC, :float_millisecond).returns(100.0, 125.0)
109
+
110
+ result = client.distribution("latency", tags: ["route:cart"]) { :result }
111
+
112
+ assert_equal(:result, result)
113
+ client.force_flush
114
+ datagram = sink.datagrams.find { |d| d.name == "latency" }
115
+ assert_equal(25.0, datagram.value)
116
+ ensure
117
+ client&.instance_variable_get(:@aggregator)&.instance_variable_get(:@flush_thread)&.kill
118
+ end
119
+
120
+ def test_injected_aggregator_receives_positional_calls
121
+ aggregator = mock("aggregator")
122
+ tags = ["route:cart"]
123
+ aggregator.expects(:increment).with("requests", 2, tags, false, nil)
124
+ aggregator.expects(:flush)
125
+
126
+ client = StatsD::Instrument::Client.new(
127
+ aggregator: aggregator,
128
+ sink: StatsD::Instrument::NullSink.new,
129
+ )
130
+
131
+ client.increment("requests", 2, tags: tags)
132
+ client.force_flush
133
+ end
134
+
135
+ def test_aggregation_supports_existing_compiled_metrics
136
+ sink = StatsD::Instrument::CaptureSink.new(parent: StatsD::Instrument::NullSink.new)
137
+ client = StatsD::Instrument::Client.new(sink: sink, enable_aggregation: true)
138
+ old_client = StatsD.singleton_client
139
+ StatsD.singleton_client = client
140
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
141
+ define(name: "compiled.requests")
142
+ end
143
+
144
+ metric.increment(2)
145
+ client.force_flush
146
+
147
+ datagram = sink.datagrams.find { |d| d.name == "compiled.requests" }
148
+ assert_equal(2, datagram.value)
149
+ ensure
150
+ StatsD.singleton_client = old_client
151
+ client&.instance_variable_get(:@aggregator)&.instance_variable_get(:@flush_thread)&.kill
152
+ end
153
+
154
+ def test_clone_with_options_preserves_aggregator
155
+ sink = StatsD::Instrument::CaptureSink.new(parent: StatsD::Instrument::NullSink.new)
156
+ client = StatsD::Instrument::Client.new(sink: sink, enable_aggregation: true)
157
+ clone = client.clone_with_options(prefix: "clone")
158
+
159
+ clone.increment("requests")
160
+ clone.force_flush
161
+
162
+ datagram = sink.datagrams.find { |d| d.name == "clone.requests" }
163
+ assert_equal(1, datagram.value)
164
+ ensure
165
+ client&.instance_variable_get(:@aggregator)&.instance_variable_get(:@flush_thread)&.kill
166
+ clone&.instance_variable_get(:@aggregator)&.instance_variable_get(:@flush_thread)&.kill
167
+ end
168
+
105
169
  def test_capture
106
170
  inner_datagrams = nil
107
171
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statsd-instrument
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.11.2
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesse Storimer