statsd-instrument 3.11.0 → 3.11.2
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 +4 -4
- data/.github/workflows/tests.yml +1 -1
- data/CHANGELOG.md +10 -0
- data/Gemfile +2 -5
- data/lib/statsd/instrument/client.rb +19 -0
- data/lib/statsd/instrument/compiled_metric.rb +17 -6
- data/lib/statsd/instrument/datagram.rb +1 -1
- data/lib/statsd/instrument/dogstatsd_datagram.rb +2 -2
- data/lib/statsd/instrument/rubocop/metric_return_value.rb +1 -1
- data/lib/statsd/instrument/rubocop.rb +3 -3
- data/lib/statsd/instrument/version.rb +1 -1
- data/test/compiled_metric/distribution_test.rb +41 -5
- data/test/compiled_metric_test.rb +75 -4
- data/test/rubocop/singleton_configuration_test.rb +3 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51853f9d4b70fc39649e251e7e93ff3d77b2bcca1d776182f82de8219ea86886
|
|
4
|
+
data.tar.gz: fce22a816682f0bc25510b6a68248a64c93ba983e7640b57ec01dbb0b985c9d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a06bd60427001260e206243f4db74ecb3d8225c1c5e3f8f9764239bc40901457cf621d7f5fbd0936d39fa0765145499672a9fb0a9c22025ac8611f7051f7154
|
|
7
|
+
data.tar.gz: f74279e49141844c564d896e1d04b88b6b9f66585e56401a4ee52f7f4cb8dcc5fbe6a99b8427fbfa45ca1be843fef5a8c9357fb629129372880ac5020aceab31
|
data/.github/workflows/tests.yml
CHANGED
|
@@ -9,7 +9,7 @@ jobs:
|
|
|
9
9
|
strategy:
|
|
10
10
|
fail-fast: false
|
|
11
11
|
matrix:
|
|
12
|
-
ruby: ['
|
|
12
|
+
ruby: ['3.0', '3.1', '3.2', '3.3', '3.4', 'ruby-head', 'jruby-10.1.1.0', 'truffleruby-34.0.1']
|
|
13
13
|
|
|
14
14
|
steps:
|
|
15
15
|
- uses: actions/checkout@v4
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ section below.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased changes
|
|
8
8
|
|
|
9
|
+
## Version 3.11.2
|
|
10
|
+
|
|
11
|
+
- Widen the `CompiledMetric` tag-combination cache key from 32 to 57 bits, eliminating
|
|
12
|
+
recurring hash collisions on hot metrics while keeping all intermediates within Fixnum
|
|
13
|
+
range (no new allocations on the emit path).
|
|
14
|
+
|
|
15
|
+
## Version 3.11.1
|
|
16
|
+
|
|
17
|
+
- Fix `CompiledMetric::Distribution` applying `sample_rate` twice.
|
|
18
|
+
|
|
9
19
|
## Version 3.11.0
|
|
10
20
|
|
|
11
21
|
- [#418](https://github.com/Shopify/statsd-instrument/pull/418) - Prevent misuse of `CompiledMetric` definitions by requiring a subclass when defining one.
|
data/Gemfile
CHANGED
|
@@ -13,15 +13,12 @@ gem "rubocop-shopify", require: false
|
|
|
13
13
|
gem "benchmark-ips"
|
|
14
14
|
gem "dogstatsd-ruby", "~> 5.0", require: false
|
|
15
15
|
gem "simplecov", require: false, group: :test
|
|
16
|
+
# logger is no longer bundled with newer Ruby and JRuby releases.
|
|
17
|
+
gem "logger"
|
|
16
18
|
|
|
17
19
|
platform :mri do
|
|
18
20
|
# only if Ruby is MRI && >= 3.2
|
|
19
21
|
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.2")
|
|
20
22
|
gem "vernier", require: false
|
|
21
23
|
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
|
|
27
24
|
end
|
|
@@ -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.
|
|
@@ -177,6 +177,10 @@ module StatsD
|
|
|
177
177
|
default_val = default_value
|
|
178
178
|
default_val_assignment = default_val.nil? ? "" : " = #{default_val.inspect}"
|
|
179
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"
|
|
180
184
|
|
|
181
185
|
method_code = <<~RUBY
|
|
182
186
|
def self.#{method}(__value__#{default_val_assignment}, #{tag_names.map { |name| "#{name}:" }.join(", ")})
|
|
@@ -185,10 +189,13 @@ module StatsD
|
|
|
185
189
|
|
|
186
190
|
# Compute hash of tag values for cache lookup using rotate-left + XOR.
|
|
187
191
|
# Rotation makes it order-dependent (unlike plain XOR), preventing collisions
|
|
188
|
-
# when tag values are swapped.
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
#
|
|
192
|
+
# when tag values are swapped. The mask keeps 57 bits of entropy, wide enough
|
|
193
|
+
# that birthday collisions are negligible at real tag-combination volumes.
|
|
194
|
+
# 57 bits is the widest key for which the intermediate (__cache_key__ << 5)
|
|
195
|
+
# stays within Fixnum range (2**62 - 1 on 64-bit CRuby) and never allocates
|
|
196
|
+
# a Bignum.
|
|
197
|
+
__cache_key__ = #{tag_names.first}.hash & 0x01FFFFFFFFFFFFFF
|
|
198
|
+
#{tag_names.drop(1).map { |name| "__cache_key__ = (((__cache_key__ << 5) | (__cache_key__ >> 52)) ^ #{name}.hash) & 0x01FFFFFFFFFFFFFF" }.join("\n")}
|
|
192
199
|
|
|
193
200
|
# Look up or create a PrecompiledDatagram
|
|
194
201
|
__datagram__ =
|
|
@@ -218,7 +225,7 @@ module StatsD
|
|
|
218
225
|
|
|
219
226
|
__datagram__ ||= PrecompiledDatagram.new([#{tag_names.join(", ")}], @datagram_blueprint, @sample_rate)
|
|
220
227
|
|
|
221
|
-
@singleton_client
|
|
228
|
+
@singleton_client.#{client_emit_method}(__datagram__, __value__)
|
|
222
229
|
__return_value__
|
|
223
230
|
end
|
|
224
231
|
RUBY
|
|
@@ -233,12 +240,16 @@ module StatsD
|
|
|
233
240
|
method = method_name
|
|
234
241
|
default_val = default_value
|
|
235
242
|
allow_block = allow_measuring_latency
|
|
243
|
+
# Block-enabled compiled metrics run through generate_block_handler first,
|
|
244
|
+
# which applies sample_rate before emitting. Use the presampled client helper
|
|
245
|
+
# here so distributions don't make a second sampling decision.
|
|
246
|
+
client_emit_method = allow_block ? "emit_presampled_precompiled_#{method}_metric" : "emit_precompiled_#{method}_metric"
|
|
236
247
|
|
|
237
248
|
instance_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
|
238
249
|
def self.#{method}(__value__ = #{default_val.inspect})
|
|
239
250
|
__return_value__ = StatsD::Instrument::VOID
|
|
240
251
|
#{generate_block_handler if allow_block}
|
|
241
|
-
@singleton_client
|
|
252
|
+
@singleton_client.#{client_emit_method}(@static_datagram, __value__)
|
|
242
253
|
__return_value__
|
|
243
254
|
end
|
|
244
255
|
RUBY
|
|
@@ -69,7 +69,7 @@ module StatsD
|
|
|
69
69
|
(?:\|m:(?<message>[^\|]+))?
|
|
70
70
|
\n? # In some implementations, the datagram may include a trailing newline.
|
|
71
71
|
\z
|
|
72
|
-
}x
|
|
72
|
+
}x.freeze
|
|
73
73
|
|
|
74
74
|
# |k:my-key|p:low|s:source|t:success|
|
|
75
75
|
EVENT_PARSER = %r{
|
|
@@ -84,7 +84,7 @@ module StatsD
|
|
|
84
84
|
(?:\|\#(?<tags>(?:[^\|,]+(?:,[^\|,]+)*)))?
|
|
85
85
|
\n? # In some implementations, the datagram may include a trailing newline.
|
|
86
86
|
\z
|
|
87
|
-
}x
|
|
87
|
+
}x.freeze
|
|
88
88
|
|
|
89
89
|
PARSER = Regexp.union(StatsD::Instrument::Datagram::PARSER, SERVICE_CHECK_PARSER, EVENT_PARSER)
|
|
90
90
|
end
|
|
@@ -19,7 +19,7 @@ module RuboCop
|
|
|
19
19
|
|
|
20
20
|
MSG = "Do not use the return value of StatsD metric methods"
|
|
21
21
|
|
|
22
|
-
INVALID_PARENTS = [:lvasgn, :array, :pair, :send, :return, :yield]
|
|
22
|
+
INVALID_PARENTS = [:lvasgn, :array, :pair, :send, :return, :yield].freeze
|
|
23
23
|
|
|
24
24
|
def on_send(node)
|
|
25
25
|
if metric_method?(node) && node.arguments.last&.type != :block_pass
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module RuboCop
|
|
4
4
|
module Cop
|
|
5
5
|
module StatsD
|
|
6
|
-
METRIC_METHODS = [:increment, :gauge, :measure, :set, :histogram, :distribution, :key_value]
|
|
6
|
+
METRIC_METHODS = [:increment, :gauge, :measure, :set, :histogram, :distribution, :key_value].freeze
|
|
7
7
|
|
|
8
8
|
METAPROGRAMMING_METHODS = [
|
|
9
9
|
:statsd_measure,
|
|
@@ -11,7 +11,7 @@ module RuboCop
|
|
|
11
11
|
:statsd_count_success,
|
|
12
12
|
:statsd_count_if,
|
|
13
13
|
:statsd_count,
|
|
14
|
-
]
|
|
14
|
+
].freeze
|
|
15
15
|
|
|
16
16
|
SINGLETON_CONFIGURATION_METHODS = [
|
|
17
17
|
:backend,
|
|
@@ -22,7 +22,7 @@ module RuboCop
|
|
|
22
22
|
:"default_tags=",
|
|
23
23
|
:default_sample_rate,
|
|
24
24
|
:"default_sample_rate=",
|
|
25
|
-
]
|
|
25
|
+
].freeze
|
|
26
26
|
|
|
27
27
|
private
|
|
28
28
|
|
|
@@ -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
|
-
|
|
419
|
-
|
|
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:
|
|
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)
|
|
@@ -285,10 +285,10 @@ class CompiledMetricDefinitionTest < Minitest::Test
|
|
|
285
285
|
|
|
286
286
|
cache = metric.instance_variable_get(:@tag_combination_cache)
|
|
287
287
|
|
|
288
|
-
# Compute cache keys using the rotate-left + XOR formula (
|
|
289
|
-
# For single tag, it's the hash value masked to
|
|
290
|
-
cache_key_for_1 = 1.hash &
|
|
291
|
-
cache_key_for_2 = 2.hash &
|
|
288
|
+
# Compute cache keys using the rotate-left + XOR formula (57-bit bounded)
|
|
289
|
+
# For single tag, it's the hash value masked to 57 bits
|
|
290
|
+
cache_key_for_1 = 1.hash & 0x01FFFFFFFFFFFFFF
|
|
291
|
+
cache_key_for_2 = 2.hash & 0x01FFFFFFFFFFFFFF
|
|
292
292
|
|
|
293
293
|
# Store the cached datagram under the collision key
|
|
294
294
|
cached_datagram = cache[cache_key_for_1]
|
|
@@ -312,6 +312,77 @@ class CompiledMetricDefinitionTest < Minitest::Test
|
|
|
312
312
|
assert_includes(hash_collision_metric.tags, "metric_name:foo.bar")
|
|
313
313
|
end
|
|
314
314
|
|
|
315
|
+
def test_cache_key_space_is_wider_than_32_bits
|
|
316
|
+
metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
|
|
317
|
+
define(
|
|
318
|
+
name: "foo.bar",
|
|
319
|
+
tags: { shop_id: Integer },
|
|
320
|
+
)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Hash seeds are process-local, so find a pair of tag values at runtime that
|
|
324
|
+
# shares its low 32 hash bits (collided under the previous 32-bit cache key)
|
|
325
|
+
# while still differing across the full 57-bit key space.
|
|
326
|
+
seen = {}
|
|
327
|
+
pair = nil
|
|
328
|
+
i = 0
|
|
329
|
+
until pair
|
|
330
|
+
key = i.hash & 0xFFFFFFFF
|
|
331
|
+
other = seen[key]
|
|
332
|
+
if other && other != i && (other.hash & 0x01FFFFFFFFFFFFFF) != (i.hash & 0x01FFFFFFFFFFFFFF)
|
|
333
|
+
pair = [other, i]
|
|
334
|
+
end
|
|
335
|
+
seen[key] ||= i
|
|
336
|
+
i += 1
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
metric.increment(1, shop_id: pair[0])
|
|
340
|
+
metric.increment(1, shop_id: pair[1])
|
|
341
|
+
|
|
342
|
+
cache = metric.instance_variable_get(:@tag_combination_cache)
|
|
343
|
+
assert_equal(2, cache.size)
|
|
344
|
+
|
|
345
|
+
collision_metric = @sink.datagrams.find do |datagram|
|
|
346
|
+
datagram.name == "test.statsd_instrument.compiled_metric.hash_collision_detected"
|
|
347
|
+
end
|
|
348
|
+
assert_nil(collision_metric, "Expected no hash collision metric for 57-bit-distinct keys")
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def test_cache_key_computation_does_not_allocate
|
|
352
|
+
skip_on_jruby("JRuby does not support GC.stat(:total_allocated_objects)")
|
|
353
|
+
|
|
354
|
+
single_tag_metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
|
|
355
|
+
define(name: "foo.single", tags: { a: String })
|
|
356
|
+
end
|
|
357
|
+
multi_tag_metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
|
|
358
|
+
define(name: "foo.multi", tags: { a: String, b: String, c: String, d: String, e: String, f: String, g: String, h: String })
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
single_call = -> { single_tag_metric.increment(1, a: "a") }
|
|
362
|
+
multi_call = -> { multi_tag_metric.increment(1, a: "a", b: "b", c: "c", d: "d", e: "e", f: "f", g: "g", h: "h") }
|
|
363
|
+
measure = ->(callable) { count_allocations { 10.times { callable.call } } }
|
|
364
|
+
[single_call, multi_call].each { |c| measure.call(c) }
|
|
365
|
+
|
|
366
|
+
# With frozen string literals, the rotate-left + XOR chain is the only per-tag
|
|
367
|
+
# work on a cache hit. A cache key wide enough to escape Fixnum range would
|
|
368
|
+
# allocate one heap integer per chain step and show up as extra allocations
|
|
369
|
+
# for the multi-tag metric.
|
|
370
|
+
assert_equal(
|
|
371
|
+
measure.call(single_call),
|
|
372
|
+
measure.call(multi_call),
|
|
373
|
+
"Expected per-tag cache-key computation to allocate nothing",
|
|
374
|
+
)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
private
|
|
378
|
+
|
|
379
|
+
def count_allocations
|
|
380
|
+
GC.start
|
|
381
|
+
before = GC.stat(:total_allocated_objects)
|
|
382
|
+
yield
|
|
383
|
+
GC.stat(:total_allocated_objects) - before
|
|
384
|
+
end
|
|
385
|
+
|
|
315
386
|
def test_handles_default_tags_as_array
|
|
316
387
|
StatsD.singleton_client = StatsD::Instrument::Client.new(
|
|
317
388
|
sink: @sink,
|
|
@@ -18,7 +18,10 @@ module Rubocop
|
|
|
18
18
|
|
|
19
19
|
def test_offense_statsd_prefix
|
|
20
20
|
assert_offense('StatsD.prefix = "foo"')
|
|
21
|
+
# The interpolation is intentionally literal source passed to the cop under test.
|
|
22
|
+
# rubocop:disable Lint/InterpolationCheck
|
|
21
23
|
assert_offense('"#{StatsD.prefix}.foo"')
|
|
24
|
+
# rubocop:enable Lint/InterpolationCheck
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def test_offense_statsd_default_tags
|
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.
|
|
4
|
+
version: 3.11.2
|
|
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.
|
|
138
|
+
rubygems_version: 4.0.18
|
|
139
139
|
specification_version: 4
|
|
140
140
|
summary: A StatsD client for Ruby apps
|
|
141
141
|
test_files:
|