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.
@@ -0,0 +1,503 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class CompiledMetricDistributionTest < 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_distribution_without_define
25
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution)
26
+
27
+ error = assert_raises(ArgumentError) do
28
+ metric.distribution(5)
29
+ end
30
+ assert_equal("Every CompiledMetric subclass needs to call `define` before first invocation of distribution.", error.message)
31
+ end
32
+
33
+ def test_define_distribution_without_tags
34
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
35
+ define(name: "foo.bar")
36
+ end
37
+
38
+ metric.distribution(5)
39
+
40
+ datagram = @sink.datagrams.first
41
+ assert_equal("test.foo.bar", datagram.name)
42
+ assert_equal(5, datagram.value)
43
+ assert_equal(:d, datagram.type)
44
+ assert_nil(datagram.tags)
45
+ end
46
+
47
+ def test_define_distribution_with_static_tags
48
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
49
+ define(
50
+ name: "foo.bar",
51
+ static_tags: { service: "web", env: "prod" },
52
+ )
53
+ end
54
+
55
+ metric.distribution(5)
56
+
57
+ datagram = @sink.datagrams.first
58
+ assert_equal("test.foo.bar", datagram.name)
59
+ assert_equal(5, datagram.value)
60
+ assert_equal(:d, datagram.type)
61
+ assert_equal(["env:prod", "service:web"], datagram.tags.sort)
62
+ end
63
+
64
+ def test_define_distribution_with_dynamic_tags
65
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
66
+ define(
67
+ name: "foo.bar",
68
+ tags: { shop_id: Integer, user_id: Integer },
69
+ )
70
+ end
71
+
72
+ metric.distribution(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(:d, datagram.type)
78
+ assert_equal(["shop_id:123", "user_id:456"], datagram.tags.sort)
79
+ end
80
+
81
+ def test_define_distribution_with_mixed_tags
82
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
83
+ define(
84
+ name: "foo.bar",
85
+ static_tags: { service: "web" },
86
+ tags: { shop_id: Integer },
87
+ )
88
+ end
89
+
90
+ metric.distribution(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_distribution_with_string_tags
99
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
100
+ define(
101
+ name: "foo.bar",
102
+ tags: { country: String, region: String },
103
+ )
104
+ end
105
+
106
+ metric.distribution(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_distribution_with_float_tags
115
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
116
+ define(
117
+ name: "foo.bar",
118
+ tags: { rate: Float },
119
+ )
120
+ end
121
+
122
+ metric.distribution(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_distribution_no_prefix
133
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
134
+ define(
135
+ name: "foo.bar",
136
+ no_prefix: true,
137
+ )
138
+ end
139
+
140
+ metric.distribution(1)
141
+
142
+ datagram = @sink.datagrams.first
143
+ assert_equal("foo.bar", datagram.name) # No "test." prefix
144
+ end
145
+
146
+ def test_multiple_distributions_same_tags
147
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
148
+ define(
149
+ name: "foo.bar",
150
+ tags: { shop_id: Integer },
151
+ )
152
+ end
153
+
154
+ metric.distribution(1, shop_id: 123)
155
+ metric.distribution(2, shop_id: 123)
156
+ metric.distribution(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_distributions_different_tags
165
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
166
+ define(
167
+ name: "foo.bar",
168
+ tags: { shop_id: Integer },
169
+ )
170
+ end
171
+
172
+ metric.distribution(1, shop_id: 123)
173
+ metric.distribution(1, shop_id: 456)
174
+ metric.distribution(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_distribution_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::Distribution) do
193
+ define(
194
+ name: "foo.bar",
195
+ static_tags: { service: "web" },
196
+ )
197
+ end
198
+
199
+ metric.distribution(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_distribution_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::Distribution) do
218
+ define(
219
+ name: "foo.bar",
220
+ static_tags: { service: "web" },
221
+ no_prefix: true,
222
+ )
223
+ end
224
+
225
+ metric.distribution(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_latency_as_value_when_block_provided
234
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
235
+ define(
236
+ name: "foo.bar",
237
+ static_tags: { service: "web" },
238
+ tags: { shop_id: Integer, user_id: Integer },
239
+ )
240
+ end
241
+
242
+ Process.stubs(:clock_gettime).with(Process::CLOCK_MONOTONIC, :float_millisecond).returns(100.0, 200.0)
243
+
244
+ returned_value = metric.distribution(shop_id: 123, user_id: 456) do
245
+ 4
246
+ end
247
+
248
+ datagram = @sink.datagrams.first
249
+ assert_equal("test.foo.bar", datagram.name)
250
+ assert_equal(4, returned_value)
251
+ assert_equal(100, datagram.value)
252
+ assert_equal(:d, datagram.type)
253
+ assert_equal(["service:web", "shop_id:123", "user_id:456"], datagram.tags.sort)
254
+ end
255
+
256
+ def test_latency_as_value_when_block_provided_with_only_static_tags
257
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
258
+ define(
259
+ name: "foo.bar",
260
+ static_tags: { service: "web" },
261
+ )
262
+ end
263
+
264
+ Process.stubs(:clock_gettime).with(Process::CLOCK_MONOTONIC, :float_millisecond).returns(100.0, 200.0)
265
+
266
+ returned_value = metric.distribution do
267
+ 4
268
+ end
269
+
270
+ datagram = @sink.datagrams.first
271
+ assert_equal("test.foo.bar", datagram.name)
272
+ assert_equal(4, returned_value)
273
+ assert_equal(100, datagram.value)
274
+ assert_equal(:d, datagram.type)
275
+ assert_equal(["service:web"], datagram.tags.sort)
276
+ end
277
+
278
+ def test_ignores_explicit_value_when_block_provided
279
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
280
+ define(
281
+ name: "foo.bar",
282
+ static_tags: { service: "web" },
283
+ tags: { shop_id: Integer, user_id: Integer },
284
+ )
285
+ end
286
+
287
+ Process.stubs(:clock_gettime).with(Process::CLOCK_MONOTONIC, :float_millisecond).returns(100.0, 200.0)
288
+
289
+ returned_value = metric.distribution(42, shop_id: 123, user_id: 456) {}
290
+
291
+ datagram = @sink.datagrams.first
292
+ assert_equal("test.foo.bar", datagram.name)
293
+ assert_nil(returned_value)
294
+ # Time of block is used and overrides the passed in `value: 42`
295
+ assert_equal(100, datagram.value)
296
+ assert_equal(:d, datagram.type)
297
+ assert_equal(["service:web", "shop_id:123", "user_id:456"], datagram.tags.sort)
298
+ end
299
+ end
300
+
301
+ class CompiledMetricDistributionWithAggregationTest < Minitest::Test
302
+ def setup
303
+ super
304
+ @old_client = StatsD.singleton_client
305
+ @sink = StatsD::Instrument::CaptureSink.new(parent: StatsD::Instrument::NullSink.new)
306
+ @aggregator = StatsD::Instrument::Aggregator.new(
307
+ @sink,
308
+ StatsD::Instrument::DatagramBuilder,
309
+ "test",
310
+ [],
311
+ flush_interval: 5.0,
312
+ )
313
+ client = StatsD::Instrument::Client.new(
314
+ sink: @sink,
315
+ prefix: "test",
316
+ default_tags: [],
317
+ enable_aggregation: true,
318
+ )
319
+ client.instance_variable_set(:@aggregator, @aggregator)
320
+ StatsD.singleton_client = client
321
+ end
322
+
323
+ def teardown
324
+ super
325
+ @sink.clear
326
+ StatsD.singleton_client = @old_client
327
+ end
328
+
329
+ def test_aggregates_precompiled_metrics
330
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
331
+ define(
332
+ name: "foo.bar",
333
+ tags: { shop_id: Integer },
334
+ )
335
+ end
336
+
337
+ metric.distribution(1, shop_id: 123)
338
+ metric.distribution(2, shop_id: 123)
339
+ metric.distribution(3, shop_id: 123)
340
+
341
+ @aggregator.flush
342
+
343
+ assert_equal(1, @sink.datagrams.size)
344
+ datagram = @sink.datagrams.first
345
+ assert_equal([1, 2, 3], datagram.value)
346
+ assert_equal(["shop_id:123"], datagram.tags)
347
+ end
348
+
349
+ def test_aggregates_different_tag_combinations_separately
350
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
351
+ define(
352
+ name: "foo.bar",
353
+ tags: { shop_id: Integer },
354
+ )
355
+ end
356
+
357
+ metric.distribution(1, shop_id: 123)
358
+ metric.distribution(2, shop_id: 456)
359
+ metric.distribution(3, shop_id: 123)
360
+
361
+ @aggregator.flush
362
+
363
+ assert_equal(2, @sink.datagrams.size)
364
+
365
+ shop_123_datagram = @sink.datagrams.find { |d| d.tags.include?("shop_id:123") }
366
+ shop_456_datagram = @sink.datagrams.find { |d| d.tags.include?("shop_id:456") }
367
+
368
+ assert_equal([1, 3], shop_123_datagram.value)
369
+ assert_equal(2, shop_456_datagram.value)
370
+ end
371
+
372
+ def test_aggregates_static_tag_metrics
373
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
374
+ define(
375
+ name: "foo.bar",
376
+ static_tags: { service: "web" },
377
+ )
378
+ end
379
+
380
+ metric.distribution(1)
381
+ metric.distribution(2)
382
+ metric.distribution(5)
383
+
384
+ @aggregator.flush
385
+
386
+ assert_equal(1, @sink.datagrams.size)
387
+ datagram = @sink.datagrams.first
388
+ assert_equal([1, 2, 5], datagram.value)
389
+ end
390
+
391
+ def test_sample_rate_equal_to_1_with_aggregation
392
+ # When aggregating with sample_rate, sampling happens before aggregation
393
+ # This test verifies that with sample_rate=1.0, all distributions are aggregated
394
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
395
+ define(
396
+ name: "foo.bar",
397
+ static_tags: { service: "web" },
398
+ sample_rate: 1.0,
399
+ )
400
+ end
401
+
402
+ # With sample_rate=1.0, all distributions should be aggregated
403
+ metric.distribution(5)
404
+ metric.distribution(3)
405
+
406
+ @aggregator.flush
407
+
408
+ assert_equal(1, @sink.datagrams.size)
409
+ datagram = @sink.datagrams.first
410
+ assert_equal("test.foo.bar", datagram.name)
411
+ assert_equal([5, 3], datagram.value)
412
+ # Sample rate should be 1.0 when aggregating
413
+ assert_equal(1.0, datagram.sample_rate)
414
+ refute_includes(datagram.source, "|@")
415
+ end
416
+
417
+ def test_sample_rate_with_aggregation
418
+ # When aggregating with sample_rate, sampling happens before aggregation
419
+ # This test verifies that with a sample_rate >0, a subset of distributions are aggregated
420
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
421
+ define(
422
+ name: "foo.bar",
423
+ static_tags: { service: "web" },
424
+ sample_rate: 0.5,
425
+ )
426
+ end
427
+
428
+ metric.stubs(:sample?).returns(false, true, false, false, true)
429
+
430
+ metric.distribution(1)
431
+ metric.distribution(2)
432
+ metric.distribution(3)
433
+ metric.distribution(4)
434
+ metric.distribution(5)
435
+
436
+ @aggregator.flush
437
+
438
+ assert_equal(1, @sink.datagrams.size)
439
+ datagram = @sink.datagrams.first
440
+ assert_equal("test.foo.bar", datagram.name)
441
+ assert_equal([2, 5], datagram.value)
442
+ assert_equal(0.5, datagram.sample_rate)
443
+ assert_includes(datagram.source, "|@")
444
+ end
445
+
446
+ def test_aggregates_values_with_blocks
447
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
448
+ define(
449
+ name: "foo.bar",
450
+ static_tags: { service: "web" },
451
+ tags: { shop_id: Integer, user_id: Integer },
452
+ )
453
+ end
454
+
455
+ Process.stubs(:clock_gettime).with(Process::CLOCK_MONOTONIC, :float_millisecond).returns(100.0, 200.0, 300.0, 350.0)
456
+
457
+ first_returned_value = metric.distribution(shop_id: 123, user_id: 456) do
458
+ 1
459
+ end
460
+
461
+ second_returned_value = metric.distribution(shop_id: 123, user_id: 456) do
462
+ 2
463
+ end
464
+
465
+ @aggregator.flush
466
+
467
+ datagram = @sink.datagrams.first
468
+ assert_equal("test.foo.bar", datagram.name)
469
+ assert_equal(1, first_returned_value)
470
+ assert_equal(2, second_returned_value)
471
+ # First block 100ms, second block 50ms
472
+ assert_equal([100, 50], datagram.value)
473
+ assert_equal(:d, datagram.type)
474
+ assert_equal(["service:web", "shop_id:123", "user_id:456"], datagram.tags.sort)
475
+ end
476
+
477
+ def test_aggregates_with_values_and_blocks_mixed
478
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
479
+ define(
480
+ name: "foo.bar",
481
+ static_tags: { service: "web" },
482
+ tags: { shop_id: Integer, user_id: Integer },
483
+ )
484
+ end
485
+
486
+ Process.stubs(:clock_gettime).with(Process::CLOCK_MONOTONIC, :float_millisecond).returns(300.0, 350.0)
487
+
488
+ metric.distribution(42, shop_id: 123, user_id: 456)
489
+ second_returned_value = metric.distribution(shop_id: 123, user_id: 456) do
490
+ 2
491
+ end
492
+
493
+ @aggregator.flush
494
+
495
+ datagram = @sink.datagrams.first
496
+ assert_equal("test.foo.bar", datagram.name)
497
+ assert_equal(2, second_returned_value)
498
+ # First value 42, second block 50ms
499
+ assert_equal([42, 50], datagram.value)
500
+ assert_equal(:d, datagram.type)
501
+ assert_equal(["service:web", "shop_id:123", "user_id:456"], datagram.tags.sort)
502
+ end
503
+ end