statsd-instrument 3.10.0 → 3.11.1

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: 4768c067e6be6c254db4557916f0cfecb4b86cb318bc8c220acfaf67e9930029
4
- data.tar.gz: 756ea941dc116e8c4ed95f1d6af1060ca1d524271b13cb5a418772fc7952519b
3
+ metadata.gz: 3da8fd9b1f80c7cf776c41d335ab14858dd625299bbec536c7f8994dfe93baf6
4
+ data.tar.gz: b1ca1c4049a5f3956ea510074027bb577d26ab8a2033cf96d4af5fcc39fc90c9
5
5
  SHA512:
6
- metadata.gz: aab58b7242348378b11869da05bcf548c434b63a5f9155a904532b186e5a420e44891c30b1e551d0627359da05fad9c50414876d6eb57baba5883b7990f1d127
7
- data.tar.gz: aece18a4af6ef4a14fbb437a91b3dcbd84b7a876a815210d3fb1e207b4851bc8803531589f1b985563da5a31e93e2724d1cc18d2d063742d7dc91ccbeb5abf60
6
+ metadata.gz: f8774b020de05ec35687a82aca947a19b095ea34867fdcd73518748c9ccddfe28e41cdc41c2dfe1b9c2e4f657b4416218d67fe3b0e259a8bf10a810580f8ea60
7
+ data.tar.gz: 420ea1eb853426b83d2d5f546dde091d402aaff31ef243b912df3869dd02882e8ef24bde4b124f860beccb0e9528dcfbcd7ab6b25b36b3d2fc3e02798fa209e1
data/CHANGELOG.md CHANGED
@@ -6,6 +6,15 @@ section below.
6
6
 
7
7
  ## Unreleased changes
8
8
 
9
+ ## Version 3.11.1
10
+
11
+ - Fix `CompiledMetric::Distribution` applying `sample_rate` twice.
12
+
13
+ ## Version 3.11.0
14
+
15
+ - [#418](https://github.com/Shopify/statsd-instrument/pull/418) - Prevent misuse of `CompiledMetric` definitions by requiring a subclass when defining one.
16
+ - [#417](https://github.com/Shopify/statsd-instrument/pull/417) - Add a `metric_name` method to `CompiledMetric`.
17
+
9
18
  ## Version 3.10.0
10
19
 
11
20
  - [#416](https://github.com/Shopify/statsd-instrument/pull/416) - Fix missing `metric_prefix` in Aggregator finalizer, causing metrics to lose their prefix when flushed during GC.
@@ -285,6 +285,25 @@ module StatsD
285
285
  StatsD::Instrument::VOID
286
286
  end
287
287
 
288
+ # Emits a pre-sampled compiled distribution metric.
289
+ # This method is used when the generated compiled metric method already made
290
+ # the sampling decision before measuring block latency.
291
+ #
292
+ # @param precompiled_datagram [StatsD::Instrument::CompiledMetric::PrecompiledDatagram]
293
+ # The precompiled metric datagram
294
+ # @param value [Numeric] The metric value
295
+ # @return [void]
296
+ # @api private
297
+ def emit_presampled_precompiled_distribution_metric(precompiled_datagram, value)
298
+ if @enable_aggregation
299
+ @aggregator.aggregate_precompiled_timing_metric(precompiled_datagram, value)
300
+ return StatsD::Instrument::VOID
301
+ end
302
+
303
+ emit(precompiled_datagram.to_datagram(value))
304
+ StatsD::Instrument::VOID
305
+ end
306
+
288
307
  # Emits a precompiled metric for high-performance use cases.
289
308
  # This method is used by {StatsD::Instrument::CompiledMetric::Gauge} to emit metrics
290
309
  # with minimal allocations.
@@ -18,6 +18,12 @@ module StatsD
18
18
  # # Later, emit with minimal allocations:
19
19
  # CheckoutMetric.increment(shop_id: 123, user_id: 456, value: 1)
20
20
  class CompiledMetric
21
+ # Raised when a CompiledMetric subclass is defined incorrectly.
22
+ # This includes calling `define` on a base type class directly,
23
+ # calling `define` more than once, or using a metric before `define`
24
+ # has been called.
25
+ class DefinitionError < StandardError; end
26
+
21
27
  # Default maximum number of unique tag combinations to cache before clearing
22
28
  # the cache to prevent unbounded memory growth
23
29
  DEFAULT_MAX_TAG_COMBINATION_CACHE_SIZE = 5000
@@ -33,6 +39,19 @@ module StatsD
33
39
  # @param max_cache_size [Integer] Maximum tag combinations this metric supports, and will be retained in-memory. Cardinality beyond this number will fall back to the slow path and should be avoided.
34
40
  # @return [Class] A new CompiledMetric subclass configured for this metric
35
41
  def define(name:, static_tags: {}, tags: {}, no_prefix: false, sample_rate: nil, max_cache_size: DEFAULT_MAX_TAG_COMBINATION_CACHE_SIZE)
42
+ if equal?(CompiledMetric) || superclass.equal?(CompiledMetric)
43
+ raise DefinitionError,
44
+ "`define` must be called on a subclass, not on #{self.name} directly. " \
45
+ "Use `Class.new(#{self.name}) { define(...) }` or " \
46
+ "`class MyMetric < #{self.name}; define(...); end` instead."
47
+ end
48
+
49
+ if defined?(@datagram_blueprint)
50
+ raise DefinitionError,
51
+ "`define` has already been called on #{self.name}. " \
52
+ "Each CompiledMetric subclass can only be defined once."
53
+ end
54
+
36
55
  client = StatsD.singleton_client
37
56
 
38
57
  # Build the datagram blueprint using the builder
@@ -51,7 +70,7 @@ module StatsD
51
70
  # Create a new class for this specific metric
52
71
  # Using classes instead of instances for better YJIT optimization
53
72
  metric_class = tap do
54
- @name = DatagramBlueprintBuilder.normalize_name(name)
73
+ @name = DatagramBlueprintBuilder.normalize_name(name).freeze
55
74
  @datagram_blueprint = datagram_blueprint
56
75
  @tag_combination_cache = {}
57
76
  @max_cache_size = max_cache_size
@@ -101,10 +120,15 @@ module StatsD
101
120
  @singleton_client.sink.sample?(sample_rate)
102
121
  end
103
122
 
123
+ # @return [String, nil] The normalized metric name for this compiled metric class (`nil` if not yet defined).
124
+ def metric_name
125
+ @name
126
+ end
127
+
104
128
  # @return [Float] The defined sample rate for a metric class.
105
129
  # Will raise when `define` has not yet been called on the class.
106
130
  def sample_rate
107
- raise ArgumentError, "Every CompiledMetric subclass needs to call `define` before accessing its sample_rate." unless defined?(@sample_rate)
131
+ raise DefinitionError, "Every CompiledMetric subclass needs to call `define` before accessing its sample_rate." unless defined?(@sample_rate)
108
132
 
109
133
  @sample_rate
110
134
  end
@@ -115,7 +139,7 @@ module StatsD
115
139
  # Once `define` was called during the class creation, it will override the
116
140
  # method implementation to emit the actual metric datagrams.
117
141
  def require_define_to_be_called
118
- raise ArgumentError, "Every CompiledMetric subclass needs to call `define` before first invocation of #{method_name}."
142
+ raise DefinitionError, "Every CompiledMetric subclass needs to call `define` before first invocation of #{method_name}."
119
143
  end
120
144
 
121
145
  def generate_block_handler
@@ -153,6 +177,10 @@ module StatsD
153
177
  default_val = default_value
154
178
  default_val_assignment = default_val.nil? ? "" : " = #{default_val.inspect}"
155
179
  allow_block = allow_measuring_latency
180
+ # Block-enabled compiled metrics run through generate_block_handler first,
181
+ # which applies sample_rate before emitting. Use the presampled client helper
182
+ # here so distributions don't make a second sampling decision.
183
+ client_emit_method = allow_block ? "emit_presampled_precompiled_#{method}_metric" : "emit_precompiled_#{method}_metric"
156
184
 
157
185
  method_code = <<~RUBY
158
186
  def self.#{method}(__value__#{default_val_assignment}, #{tag_names.map { |name| "#{name}:" }.join(", ")})
@@ -194,7 +222,7 @@ module StatsD
194
222
 
195
223
  __datagram__ ||= PrecompiledDatagram.new([#{tag_names.join(", ")}], @datagram_blueprint, @sample_rate)
196
224
 
197
- @singleton_client.emit_precompiled_#{method}_metric(__datagram__, __value__)
225
+ @singleton_client.#{client_emit_method}(__datagram__, __value__)
198
226
  __return_value__
199
227
  end
200
228
  RUBY
@@ -209,12 +237,16 @@ module StatsD
209
237
  method = method_name
210
238
  default_val = default_value
211
239
  allow_block = allow_measuring_latency
240
+ # Block-enabled compiled metrics run through generate_block_handler first,
241
+ # which applies sample_rate before emitting. Use the presampled client helper
242
+ # here so distributions don't make a second sampling decision.
243
+ client_emit_method = allow_block ? "emit_presampled_precompiled_#{method}_metric" : "emit_precompiled_#{method}_metric"
212
244
 
213
245
  instance_eval(<<~RUBY, __FILE__, __LINE__ + 1)
214
246
  def self.#{method}(__value__ = #{default_val.inspect})
215
247
  __return_value__ = StatsD::Instrument::VOID
216
248
  #{generate_block_handler if allow_block}
217
- @singleton_client.emit_precompiled_#{method}_metric(@static_datagram, __value__)
249
+ @singleton_client.#{client_emit_method}(@static_datagram, __value__)
218
250
  __return_value__
219
251
  end
220
252
  RUBY
@@ -2,6 +2,6 @@
2
2
 
3
3
  module StatsD
4
4
  module Instrument
5
- VERSION = "3.10.0"
5
+ VERSION = "3.11.1"
6
6
  end
7
7
  end
@@ -24,7 +24,7 @@ class CompiledMetricCounterTest < Minitest::Test
24
24
  def test_counter_without_define
25
25
  metric = Class.new(StatsD::Instrument::CompiledMetric::Counter)
26
26
 
27
- error = assert_raises(ArgumentError) do
27
+ error = assert_raises(StatsD::Instrument::CompiledMetric::DefinitionError) do
28
28
  metric.increment(5)
29
29
  end
30
30
  assert_equal("Every CompiledMetric subclass needs to call `define` before first invocation of increment.", error.message)
@@ -24,7 +24,7 @@ class CompiledMetricDistributionTest < Minitest::Test
24
24
  def test_distribution_without_define
25
25
  metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution)
26
26
 
27
- error = assert_raises(ArgumentError) do
27
+ error = assert_raises(StatsD::Instrument::CompiledMetric::DefinitionError) do
28
28
  metric.distribution(5)
29
29
  end
30
30
  assert_equal("Every CompiledMetric subclass needs to call `define` before first invocation of distribution.", error.message)
@@ -230,6 +230,43 @@ class CompiledMetricDistributionTest < Minitest::Test
230
230
  assert_equal(["env:production", "region:us-east", "service:web"], datagram.tags.sort)
231
231
  end
232
232
 
233
+ def test_sampled_distribution_value_uses_one_sampling_decision
234
+ sample_rate = 0.5
235
+ @sink.expects(:sample?).with(sample_rate).once.returns(true)
236
+
237
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
238
+ define(
239
+ name: "foo.bar",
240
+ tags: { shop_id: Integer },
241
+ sample_rate: sample_rate,
242
+ )
243
+ end
244
+
245
+ metric.distribution(5, shop_id: 123)
246
+
247
+ assert_equal(1, @sink.datagrams.size)
248
+ assert_equal("test.foo.bar:5|d|@0.5|#shop_id:123", @sink.datagrams.first.source)
249
+ end
250
+
251
+ def test_sampled_distribution_block_uses_one_sampling_decision
252
+ sample_rate = 0.5
253
+ @sink.expects(:sample?).with(sample_rate).once.returns(true)
254
+ Process.stubs(:clock_gettime).with(Process::CLOCK_MONOTONIC, :float_millisecond).returns(100.0, 125.0)
255
+
256
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
257
+ define(
258
+ name: "foo.bar",
259
+ sample_rate: sample_rate,
260
+ )
261
+ end
262
+
263
+ returned_value = metric.distribution { :result }
264
+
265
+ assert_equal(:result, returned_value)
266
+ assert_equal(1, @sink.datagrams.size)
267
+ assert_equal("test.foo.bar:25.0|d|@0.5", @sink.datagrams.first.source)
268
+ end
269
+
233
270
  def test_latency_as_value_when_block_provided
234
271
  metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
235
272
  define(
@@ -415,18 +452,17 @@ class CompiledMetricDistributionWithAggregationTest < Minitest::Test
415
452
  end
416
453
 
417
454
  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
455
+ sample_rate = 0.5
456
+ @sink.expects(:sample?).with(sample_rate).times(5).returns(false, true, false, false, true)
457
+
420
458
  metric = Class.new(StatsD::Instrument::CompiledMetric::Distribution) do
421
459
  define(
422
460
  name: "foo.bar",
423
461
  static_tags: { service: "web" },
424
- sample_rate: 0.5,
462
+ sample_rate: sample_rate,
425
463
  )
426
464
  end
427
465
 
428
- metric.stubs(:sample?).returns(false, true, false, false, true)
429
-
430
466
  metric.distribution(1)
431
467
  metric.distribution(2)
432
468
  metric.distribution(3)
@@ -24,7 +24,7 @@ class CompiledMetricGaugeTest < Minitest::Test
24
24
  def test_gauge_without_define
25
25
  metric = Class.new(StatsD::Instrument::CompiledMetric::Gauge)
26
26
 
27
- error = assert_raises(ArgumentError) do
27
+ error = assert_raises(StatsD::Instrument::CompiledMetric::DefinitionError) do
28
28
  metric.gauge(5)
29
29
  end
30
30
  assert_equal("Every CompiledMetric subclass needs to call `define` before first invocation of gauge.", error.message)
@@ -439,9 +439,78 @@ class CompiledMetricDefinitionTest < Minitest::Test
439
439
  def test_sample_rate_without_define
440
440
  metric = Class.new(StatsD::Instrument::CompiledMetric::Counter)
441
441
 
442
- error = assert_raises(ArgumentError) do
442
+ error = assert_raises(StatsD::Instrument::CompiledMetric::DefinitionError) do
443
443
  metric.sample_rate
444
444
  end
445
445
  assert_equal("Every CompiledMetric subclass needs to call `define` before accessing its sample_rate.", error.message)
446
446
  end
447
+
448
+ def test_metric_name
449
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
450
+ define(
451
+ name: "foo.bar",
452
+ )
453
+ end
454
+
455
+ assert_equal("foo.bar", metric.metric_name)
456
+ assert_predicate(metric.metric_name, :frozen?)
457
+ end
458
+
459
+ def test_metric_name_is_normalized
460
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
461
+ define(
462
+ name: "foo:bar|baz@qux",
463
+ )
464
+ end
465
+
466
+ assert_equal("foo_bar_baz_qux", metric.metric_name)
467
+ assert_predicate(metric.metric_name, :frozen?)
468
+ end
469
+
470
+ def test_metric_name_without_define
471
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter)
472
+
473
+ assert_nil(metric.metric_name)
474
+ end
475
+
476
+ def test_define_directly_on_counter_raises
477
+ error = assert_raises(StatsD::Instrument::CompiledMetric::DefinitionError) do
478
+ StatsD::Instrument::CompiledMetric::Counter.define(name: "bad_metric")
479
+ end
480
+ assert_includes(error.message, "`define` must be called on a subclass")
481
+ assert_includes(error.message, "Counter")
482
+ end
483
+
484
+ def test_define_directly_on_gauge_raises
485
+ error = assert_raises(StatsD::Instrument::CompiledMetric::DefinitionError) do
486
+ StatsD::Instrument::CompiledMetric::Gauge.define(name: "bad_metric")
487
+ end
488
+ assert_includes(error.message, "`define` must be called on a subclass")
489
+ assert_includes(error.message, "Gauge")
490
+ end
491
+
492
+ def test_define_directly_on_distribution_raises
493
+ error = assert_raises(StatsD::Instrument::CompiledMetric::DefinitionError) do
494
+ StatsD::Instrument::CompiledMetric::Distribution.define(name: "bad_metric")
495
+ end
496
+ assert_includes(error.message, "`define` must be called on a subclass")
497
+ assert_includes(error.message, "Distribution")
498
+ end
499
+
500
+ def test_define_directly_on_compiled_metric_raises
501
+ error = assert_raises(StatsD::Instrument::CompiledMetric::DefinitionError) do
502
+ StatsD::Instrument::CompiledMetric.define(name: "bad_metric")
503
+ end
504
+ assert_includes(error.message, "`define` must be called on a subclass")
505
+ end
506
+
507
+ def test_double_define_raises
508
+ error = assert_raises(StatsD::Instrument::CompiledMetric::DefinitionError) do
509
+ Class.new(StatsD::Instrument::CompiledMetric::Counter) do
510
+ define(name: "first_metric")
511
+ define(name: "second_metric")
512
+ end
513
+ end
514
+ assert_includes(error.message, "`define` has already been called")
515
+ end
447
516
  end
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.10.0
4
+ version: 3.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesse Storimer
@@ -135,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
135
  - !ruby/object:Gem::Version
136
136
  version: '0'
137
137
  requirements: []
138
- rubygems_version: 4.0.8
138
+ rubygems_version: 4.0.14
139
139
  specification_version: 4
140
140
  summary: A StatsD client for Ruby apps
141
141
  test_files: