statsd-instrument 3.9.9 → 3.10.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.
data/test/client_test.rb CHANGED
@@ -8,6 +8,10 @@ class ClientTest < Minitest::Test
8
8
  @dogstatsd_client = StatsD::Instrument::Client.new(implementation: "datadog")
9
9
  end
10
10
 
11
+ def teardown
12
+ @client.instance_variable_get(:@aggregator).instance_variable_get(:@flush_thread)&.kill
13
+ end
14
+
11
15
  def test_client_from_env
12
16
  env = StatsD::Instrument::Environment.new(
13
17
  "STATSD_ENV" => "production",
@@ -255,11 +259,78 @@ class ClientTest < Minitest::Test
255
259
  client = StatsD::Instrument::Client.new(sink: mock_sink, default_sample_rate: 0.5, enable_aggregation: true)
256
260
  5.times { client.distribution("metric", 60) }
257
261
  client.force_flush
262
+ end
263
+
264
+ def test_increment_with_aggregation_respects_sample_rate
265
+ # Test that increment with aggregation properly samples before aggregation
266
+ # and preserves sample_rate in the datagram
267
+ sink = StatsD::Instrument::CaptureSink.new(parent: StatsD::Instrument::NullSink.new)
268
+ client = StatsD::Instrument::Client.new(sink: sink, enable_aggregation: true)
269
+
270
+ # With sample_rate=1.0, all increments should be counted
271
+ client.increment("counter", 1, sample_rate: 1.0)
272
+ client.increment("counter", 2, sample_rate: 1.0)
273
+ client.force_flush
274
+
275
+ assert_equal(1, sink.datagrams.size)
276
+ datagram = sink.datagrams.first
277
+ assert_equal("counter", datagram.name)
278
+ assert_equal(3, datagram.value)
279
+ assert_equal(1.0, datagram.sample_rate)
280
+ end
281
+
282
+ def test_increment_with_aggregation_applies_sampling_before_aggregation
283
+ # Test that sampling happens BEFORE aggregation, not after
284
+ # This is the key fix - previously sampling was bypassed when aggregation was enabled
285
+ mock_sink = mock("sink")
286
+ # First call samples out (false), second call samples in (true)
287
+ mock_sink.stubs(:sample?).returns(false, true)
288
+ mock_sink.expects(:<<).with("counter:3|c|@0.5")
289
+ mock_sink.stubs(:flush)
290
+
291
+ client = StatsD::Instrument::Client.new(sink: mock_sink, enable_aggregation: true)
292
+
293
+ # First increment should be sampled out
294
+ client.increment("counter", 5, sample_rate: 0.5)
295
+ # Second increment should be sampled in
296
+ client.increment("counter", 3, sample_rate: 0.5)
297
+ client.force_flush
298
+ end
299
+
300
+ def test_measure_with_aggregation_respects_sample_rate
301
+ # Test that measure (timing) with aggregation properly handles sample_rate
302
+ sink = StatsD::Instrument::CaptureSink.new(parent: StatsD::Instrument::NullSink.new)
303
+ client = StatsD::Instrument::Client.new(
304
+ sink: sink,
305
+ enable_aggregation: true,
306
+ datagram_builder_class: StatsD::Instrument::StatsDDatagramBuilder,
307
+ )
308
+
309
+ client.measure("timing", 100, sample_rate: 0.5)
310
+ client.measure("timing", 200, sample_rate: 0.5)
311
+ client.force_flush
312
+
313
+ assert_equal(1, sink.datagrams.size)
314
+ datagram = sink.datagrams.first
315
+ assert_equal("timing", datagram.name)
316
+ assert_equal(0.5, datagram.sample_rate)
317
+ assert_includes(datagram.source, "|@0.5")
318
+ end
319
+
320
+ def test_histogram_with_aggregation_respects_sample_rate
321
+ # Test that histogram with aggregation properly handles sample_rate
322
+ sink = StatsD::Instrument::CaptureSink.new(parent: StatsD::Instrument::NullSink.new)
323
+ client = StatsD::Instrument::Client.new(sink: sink, enable_aggregation: true)
324
+
325
+ client.histogram("hist", 100, sample_rate: 0.25)
326
+ client.histogram("hist", 200, sample_rate: 0.25)
327
+ client.force_flush
258
328
 
259
- # undo mock
260
- mock_sink.unstub(:sample?)
261
- mock_sink.unstub(:<<)
262
- mock_sink.unstub(:flush)
329
+ assert_equal(1, sink.datagrams.size)
330
+ datagram = sink.datagrams.first
331
+ assert_equal("hist", datagram.name)
332
+ assert_equal(0.25, datagram.sample_rate)
333
+ assert_includes(datagram.source, "|@0.25")
263
334
  end
264
335
 
265
336
  def test_clone_with_prefix_option
@@ -0,0 +1,396 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class CompiledMetricCounterTest < Minitest::Test
6
+ def setup
7
+ super
8
+ @old_client = StatsD.singleton_client
9
+ @sink = StatsD::Instrument::CaptureSink.new(parent: StatsD::Instrument::NullSink.new)
10
+ StatsD.singleton_client = StatsD::Instrument::Client.new(
11
+ sink: @sink,
12
+ prefix: "test",
13
+ default_tags: [],
14
+ enable_aggregation: false,
15
+ )
16
+ end
17
+
18
+ def teardown
19
+ super
20
+ @sink.clear
21
+ StatsD.singleton_client = @old_client
22
+ end
23
+
24
+ def test_counter_without_define
25
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter)
26
+
27
+ error = assert_raises(ArgumentError) do
28
+ metric.increment(5)
29
+ end
30
+ assert_equal("Every CompiledMetric subclass needs to call `define` before first invocation of increment.", error.message)
31
+ end
32
+
33
+ def test_define_counter_without_tags
34
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
35
+ define(name: "foo.bar")
36
+ end
37
+
38
+ metric.increment(5)
39
+
40
+ datagram = @sink.datagrams.first
41
+ assert_equal("test.foo.bar", datagram.name)
42
+ assert_equal(5, datagram.value)
43
+ assert_equal(:c, datagram.type)
44
+ assert_nil(datagram.tags)
45
+ end
46
+
47
+ def test_define_counter_with_static_tags
48
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
49
+ define(
50
+ name: "foo.bar",
51
+ static_tags: { service: "web", env: "prod" },
52
+ )
53
+ end
54
+
55
+ metric.increment(5)
56
+
57
+ datagram = @sink.datagrams.first
58
+ assert_equal("test.foo.bar", datagram.name)
59
+ assert_equal(5, datagram.value)
60
+ assert_equal(:c, datagram.type)
61
+ assert_equal(["env:prod", "service:web"], datagram.tags.sort)
62
+ end
63
+
64
+ def test_define_counter_with_dynamic_tags
65
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
66
+ define(
67
+ name: "foo.bar",
68
+ tags: { shop_id: Integer, user_id: Integer },
69
+ )
70
+ end
71
+
72
+ metric.increment(1, shop_id: 123, user_id: 456)
73
+
74
+ datagram = @sink.datagrams.first
75
+ assert_equal("test.foo.bar", datagram.name)
76
+ assert_equal(1, datagram.value)
77
+ assert_equal(:c, datagram.type)
78
+ assert_equal(["shop_id:123", "user_id:456"], datagram.tags.sort)
79
+ end
80
+
81
+ def test_define_counter_with_mixed_tags
82
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
83
+ define(
84
+ name: "foo.bar",
85
+ static_tags: { service: "web" },
86
+ tags: { shop_id: Integer },
87
+ )
88
+ end
89
+
90
+ metric.increment(3, shop_id: 999)
91
+
92
+ datagram = @sink.datagrams.first
93
+ assert_equal("test.foo.bar", datagram.name)
94
+ assert_equal(3, datagram.value)
95
+ assert_equal(["service:web", "shop_id:999"], datagram.tags.sort)
96
+ end
97
+
98
+ def test_define_counter_with_string_tags
99
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
100
+ define(
101
+ name: "foo.bar",
102
+ tags: { country: String, region: String },
103
+ )
104
+ end
105
+
106
+ metric.increment(2, country: "US", region: "West")
107
+
108
+ datagram = @sink.datagrams.first
109
+ assert_equal("test.foo.bar", datagram.name)
110
+ assert_equal(2, datagram.value)
111
+ assert_equal(["country:US", "region:West"], datagram.tags.sort)
112
+ end
113
+
114
+ def test_define_counter_with_float_tags
115
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
116
+ define(
117
+ name: "foo.bar",
118
+ tags: { rate: Float },
119
+ )
120
+ end
121
+
122
+ metric.increment(1, rate: 1.5)
123
+
124
+ datagram = @sink.datagrams.first
125
+ assert_equal("test.foo.bar", datagram.name)
126
+ assert_equal(1, datagram.value)
127
+ # Float formatting uses %f which outputs full precision
128
+ assert_equal(1, datagram.tags.size)
129
+ assert_match(/^rate:1\.5/, datagram.tags.first)
130
+ end
131
+
132
+ def test_define_counter_no_prefix
133
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
134
+ define(
135
+ name: "foo.bar",
136
+ no_prefix: true,
137
+ )
138
+ end
139
+
140
+ metric.increment(1)
141
+
142
+ datagram = @sink.datagrams.first
143
+ assert_equal("foo.bar", datagram.name) # No "test." prefix
144
+ end
145
+
146
+ def test_multiple_increments_same_tags
147
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
148
+ define(
149
+ name: "foo.bar",
150
+ tags: { shop_id: Integer },
151
+ )
152
+ end
153
+
154
+ metric.increment(1, shop_id: 123)
155
+ metric.increment(2, shop_id: 123)
156
+ metric.increment(3, shop_id: 123)
157
+
158
+ assert_equal(3, @sink.datagrams.size)
159
+ @sink.datagrams.each do |datagram|
160
+ assert_equal(["shop_id:123"], datagram.tags)
161
+ end
162
+ end
163
+
164
+ def test_multiple_increments_different_tags
165
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
166
+ define(
167
+ name: "foo.bar",
168
+ tags: { shop_id: Integer },
169
+ )
170
+ end
171
+
172
+ metric.increment(1, shop_id: 123)
173
+ metric.increment(1, shop_id: 456)
174
+ metric.increment(1, shop_id: 789)
175
+
176
+ assert_equal(3, @sink.datagrams.size)
177
+ assert_equal(["shop_id:123"], @sink.datagrams[0].tags)
178
+ assert_equal(["shop_id:456"], @sink.datagrams[1].tags)
179
+ assert_equal(["shop_id:789"], @sink.datagrams[2].tags)
180
+ end
181
+
182
+ def test_counter_includes_default_tags_from_client
183
+ # Create a client with default tags
184
+ client = StatsD::Instrument::Client.new(
185
+ sink: @sink,
186
+ prefix: "test",
187
+ default_tags: ["env:production", "region:us-east"],
188
+ enable_aggregation: false,
189
+ )
190
+ StatsD.singleton_client = client
191
+
192
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
193
+ define(
194
+ name: "foo.bar",
195
+ static_tags: { service: "web" },
196
+ )
197
+ end
198
+
199
+ metric.increment(1)
200
+
201
+ datagram = @sink.datagrams.first
202
+ assert_equal("test.foo.bar", datagram.name)
203
+ # Should include default tags from client + static tags
204
+ assert_equal(["env:production", "region:us-east", "service:web"], datagram.tags.sort)
205
+ end
206
+
207
+ def test_counter_includes_default_tags_with_no_prefix
208
+ # Create a client with default tags
209
+ client = StatsD::Instrument::Client.new(
210
+ sink: @sink,
211
+ prefix: "test",
212
+ default_tags: ["env:production", "region:us-east"],
213
+ enable_aggregation: false,
214
+ )
215
+ StatsD.singleton_client = client
216
+
217
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
218
+ define(
219
+ name: "foo.bar",
220
+ static_tags: { service: "web" },
221
+ no_prefix: true,
222
+ )
223
+ end
224
+
225
+ metric.increment(1)
226
+
227
+ datagram = @sink.datagrams.first
228
+ assert_equal("foo.bar", datagram.name) # No prefix
229
+ # Should include default tags even when no_prefix is true
230
+ assert_equal(["env:production", "region:us-east", "service:web"], datagram.tags.sort)
231
+ end
232
+
233
+ def test_counter_does_not_support_blocks
234
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
235
+ define(
236
+ name: "foo.bar",
237
+ static_tags: { service: "web" },
238
+ tags: { shop_id: Integer },
239
+ )
240
+ end
241
+
242
+ block_called = false
243
+ metric.increment(shop_id: 999) do
244
+ block_called = true
245
+ end
246
+
247
+ datagram = @sink.datagrams.first
248
+ assert_equal("test.foo.bar", datagram.name)
249
+ # Default value
250
+ assert_equal(1, datagram.value)
251
+ refute(block_called)
252
+ assert_equal(["service:web", "shop_id:999"], datagram.tags.sort)
253
+ end
254
+ end
255
+
256
+ class CompiledMetricCounterWithAggregationTest < Minitest::Test
257
+ def setup
258
+ super
259
+ @old_client = StatsD.singleton_client
260
+ @sink = StatsD::Instrument::CaptureSink.new(parent: StatsD::Instrument::NullSink.new)
261
+ @aggregator = StatsD::Instrument::Aggregator.new(
262
+ @sink,
263
+ StatsD::Instrument::DatagramBuilder,
264
+ "test",
265
+ [],
266
+ flush_interval: 0.1,
267
+ )
268
+ client = StatsD::Instrument::Client.new(
269
+ sink: @sink,
270
+ prefix: "test",
271
+ default_tags: [],
272
+ enable_aggregation: true,
273
+ )
274
+ client.instance_variable_set(:@aggregator, @aggregator)
275
+ StatsD.singleton_client = client
276
+ end
277
+
278
+ def teardown
279
+ super
280
+ @sink.clear
281
+ StatsD.singleton_client = @old_client
282
+ end
283
+
284
+ def test_aggregates_precompiled_metrics
285
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
286
+ define(
287
+ name: "foo.bar",
288
+ tags: { shop_id: Integer },
289
+ )
290
+ end
291
+
292
+ metric.increment(1, shop_id: 123)
293
+ metric.increment(2, shop_id: 123)
294
+ metric.increment(3, shop_id: 123)
295
+
296
+ @aggregator.flush
297
+
298
+ assert_equal(1, @sink.datagrams.size)
299
+ datagram = @sink.datagrams.first
300
+ assert_equal(6, datagram.value) # 1 + 2 + 3
301
+ assert_equal(["shop_id:123"], datagram.tags)
302
+ end
303
+
304
+ def test_aggregates_different_tag_combinations_separately
305
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
306
+ define(
307
+ name: "foo.bar",
308
+ tags: { shop_id: Integer },
309
+ )
310
+ end
311
+
312
+ metric.increment(1, shop_id: 123)
313
+ metric.increment(2, shop_id: 456)
314
+ metric.increment(3, shop_id: 123)
315
+
316
+ @aggregator.flush
317
+
318
+ assert_equal(2, @sink.datagrams.size)
319
+
320
+ shop_123_datagram = @sink.datagrams.find { |d| d.tags.include?("shop_id:123") }
321
+ shop_456_datagram = @sink.datagrams.find { |d| d.tags.include?("shop_id:456") }
322
+
323
+ assert_equal(4, shop_123_datagram.value) # 1 + 3
324
+ assert_equal(2, shop_456_datagram.value)
325
+ end
326
+
327
+ def test_aggregates_static_tag_metrics
328
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
329
+ define(
330
+ name: "foo.bar",
331
+ static_tags: { service: "web" },
332
+ )
333
+ end
334
+
335
+ metric.increment(1)
336
+ metric.increment(2)
337
+ metric.increment(5)
338
+
339
+ @aggregator.flush
340
+
341
+ assert_equal(1, @sink.datagrams.size)
342
+ datagram = @sink.datagrams.first
343
+ assert_equal(8, datagram.value) # 1 + 2 + 5
344
+ end
345
+
346
+ def test_sample_rate_equal_to_1_with_aggregation
347
+ # When aggregating with sample_rate, sampling happens before aggregation
348
+ # This test verifies that with sample_rate=1.0, all increments are aggregated
349
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
350
+ define(
351
+ name: "foo.bar",
352
+ static_tags: { service: "web" },
353
+ sample_rate: 1.0,
354
+ )
355
+ end
356
+
357
+ # With sample_rate=1.0, all increments should be aggregated
358
+ metric.increment(5)
359
+ metric.increment(3)
360
+
361
+ @aggregator.flush
362
+
363
+ assert_equal(1, @sink.datagrams.size)
364
+ datagram = @sink.datagrams.first
365
+ assert_equal("test.foo.bar", datagram.name)
366
+ assert_equal(8, datagram.value) # 5 + 3
367
+ # Sample rate should be 1.0 when aggregating
368
+ assert_equal(1.0, datagram.sample_rate)
369
+ refute_includes(datagram.source, "|@")
370
+ end
371
+
372
+ def test_sample_rate_applied_with_aggregation
373
+ # When aggregating with sample_rate, sampling happens before aggregation
374
+ # This test verifies that with sample_rate=0.5, all increments are aggregated
375
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
376
+ define(
377
+ name: "foo.bar",
378
+ static_tags: { service: "web" },
379
+ sample_rate: 0.5,
380
+ )
381
+ end
382
+
383
+ metric.increment(5)
384
+ metric.increment(3)
385
+
386
+ @aggregator.flush
387
+
388
+ assert_equal(1, @sink.datagrams.size)
389
+ datagram = @sink.datagrams.first
390
+ assert_equal("test.foo.bar", datagram.name)
391
+ assert_equal(8, datagram.value) # 5 + 3
392
+ # Sample rate should be 1.0 when aggregating
393
+ assert_equal(0.5, datagram.sample_rate)
394
+ assert_includes(datagram.source, "|@")
395
+ end
396
+ end