statsd-instrument 3.9.10 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6bb36fa3a5254a64999335275c5515579febf9c2d92d35522fee1d883d0f56d
4
- data.tar.gz: 69c96c4f5abaa8cae674a85e562a41ab3bbcf05cde0ef523c934d86846103658
3
+ metadata.gz: 4768c067e6be6c254db4557916f0cfecb4b86cb318bc8c220acfaf67e9930029
4
+ data.tar.gz: 756ea941dc116e8c4ed95f1d6af1060ca1d524271b13cb5a418772fc7952519b
5
5
  SHA512:
6
- metadata.gz: 321874ee863b3f66e1eb7abbace4e734af55dc86fbd11aa8e009e398c54414f47d91447cdcbf08fb93bd4f7b1d88d58a16217093e9f8978b400187ed513dbd74
7
- data.tar.gz: 9f34eed89dee5efc7d9a3d800c1d6824b0396054782694d708dab40180e0c3b423f6a0c2337b995bf6f58d72ae750bed564cf02bb5e13fc08518540e443be400
6
+ metadata.gz: aab58b7242348378b11869da05bcf548c434b63a5f9155a904532b186e5a420e44891c30b1e551d0627359da05fad9c50414876d6eb57baba5883b7990f1d127
7
+ data.tar.gz: aece18a4af6ef4a14fbb437a91b3dcbd84b7a876a815210d3fb1e207b4851bc8803531589f1b985563da5a31e93e2724d1cc18d2d063742d7dc91ccbeb5abf60
@@ -9,7 +9,7 @@ jobs:
9
9
  strategy:
10
10
  fail-fast: false
11
11
  matrix:
12
- ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', 'ruby-head', 'jruby-9.4.9.0', 'truffleruby-22.3.1']
12
+ ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', 'ruby-head', 'jruby-9.4.9.0', 'truffleruby-25.0.0']
13
13
 
14
14
  steps:
15
15
  - uses: actions/checkout@v4
data/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ section below.
6
6
 
7
7
  ## Unreleased changes
8
8
 
9
+ ## Version 3.10.0
10
+
11
+ - [#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.
12
+ - [#415](https://github.com/Shopify/statsd-instrument/pull/415) - Fix `sample_rate` being ignored when aggregation is enabled. Previously, `increment`, `measure`, and `histogram` calls would bypass sampling entirely when aggregation was enabled, causing metrics to be emitted at 100% rate regardless of the configured sample rate.
13
+ - [#411](https://github.com/Shopify/statsd-instrument/pull/403) - Add `CompiledMetric::Gauge` as the second metric type to support pre-compiled metric datagrams. It can be used as a replacement over standard `StatsD.gauge`.
14
+ - Add support for `nil` tag values in `CompiledMetric` dynamic tags (converted to empty string).
15
+ - [#407](https://github.com/Shopify/statsd-instrument/pull/407) - Add support for `Symbol` and `:Boolean` types in `CompiledMetric` dynamic tags, allowing symbol and boolean values to be used as tag values.
16
+ - [#403](https://github.com/Shopify/statsd-instrument/pull/403) - Add `CompiledMetric::Distribution` as the second metric type to support pre-compiled metric datagrams. It can be used as a replacement over standard `StatsD.distribution`.
17
+ - [#404](https://github.com/Shopify/statsd-instrument/pull/404) - Fix bug when using aggregation; finalizer was called with the wrong values.
18
+ - [#401](https://github.com/Shopify/statsd-instrument/pull/401), [#402](https://github.com/Shopify/statsd-instrument/pull/402) - Add `CompiledMetric` pattern for high-performance metric emission. Pre-compiles metric datagrams at definition time with tag caching for 2-7x performance improvement over standard `StatsD.increment`. Currently supports Counter metrics only.
19
+ - [#399](https://github.com/Shopify/statsd-instrument/pull/399) - Release the mutex protecting the Aggregator's internal
20
+ state when flushing metrics to avoid stalling other threads trying to emit metrics.
21
+
9
22
  ## Version 3.9.10
10
23
 
11
24
  - [#398](https://github.com/Shopify/statsd-instrument/pull/398) - Fix metrics not being sent from signal trap contexts when aggregation is enabled.
@@ -11,7 +11,7 @@ module StatsD
11
11
  @no_prefix = no_prefix
12
12
  @type = type
13
13
  @sample_rate = sample_rate
14
- @hash = [@name, @tags, @no_prefix, @type].hash
14
+ @hash = [@name, @tags, @no_prefix, @type, @sample_rate].hash
15
15
  end
16
16
 
17
17
  def ==(other)
@@ -19,7 +19,8 @@ module StatsD
19
19
  @name == other.name &&
20
20
  @tags == other.tags &&
21
21
  @no_prefix == other.no_prefix &&
22
- @type == other.type
22
+ @type == other.type &&
23
+ @sample_rate == other.sample_rate
23
24
  end
24
25
  alias_method :eql?, :==
25
26
  end
@@ -33,15 +34,31 @@ module StatsD
33
34
  MEASURE = :ms
34
35
  HISTOGRAM = :h
35
36
  GAUGE = :g
37
+
38
+ Finalizer = Struct.new(:aggregation_state, :sink, :datagram_builders, :datagram_builder_class, :default_tags, :metric_prefix)
39
+
36
40
  private_constant :COUNT, :DISTRIBUTION, :MEASURE, :HISTOGRAM, :GAUGE, :CONST_SAMPLE_RATE
37
41
 
38
42
  class << self
39
- def finalize(aggregation_state, sink, datagram_builders, datagram_builder_class, default_tags)
43
+ def finalize(finalizer)
44
+ # The finalizer can be called in trap context, which means we cannot use mutexes here.
40
45
  proc do
46
+ aggregation_state = finalizer.aggregation_state
47
+ sink = finalizer.sink
48
+ datagram_builders = finalizer.datagram_builders
49
+ datagram_builder_class = finalizer.datagram_builder_class
50
+ default_tags = finalizer.default_tags
51
+ metric_prefix = finalizer.metric_prefix
52
+
41
53
  aggregation_state.each do |key, agg_value|
54
+ if key.is_a?(StatsD::Instrument::CompiledMetric::PrecompiledDatagram)
55
+ sink << key.to_datagram(agg_value)
56
+ next
57
+ end
58
+
42
59
  no_prefix = key.no_prefix
43
60
  datagram_builders[no_prefix] ||= datagram_builder_class.new(
44
- prefix: no_prefix ? nil : @metric_prefix,
61
+ prefix: no_prefix ? nil : metric_prefix,
45
62
  default_tags: default_tags,
46
63
  )
47
64
  case key.type
@@ -49,7 +66,7 @@ module StatsD
49
66
  sink << datagram_builders[no_prefix].c(
50
67
  key.name,
51
68
  agg_value,
52
- CONST_SAMPLE_RATE,
69
+ key.sample_rate,
53
70
  key.tags,
54
71
  )
55
72
  when DISTRIBUTION, MEASURE, HISTOGRAM
@@ -71,7 +88,6 @@ module StatsD
71
88
  StatsD.logger.error { "[#{self.class.name}] Unknown aggregation type: #{key.type}" }
72
89
  end
73
90
  end
74
- aggregation_state.clear
75
91
  end
76
92
  end
77
93
  end
@@ -101,16 +117,28 @@ module StatsD
101
117
  @max_values = max_values
102
118
 
103
119
  # Mutex protects the aggregation_state and flush_thread from concurrent access
104
- @mutex = Mutex.new
120
+ @aggregation_state_mutex = Mutex.new
121
+ # Mutex protects do_flush to prevent flushing to the sink concurrently
122
+ @flush_mutex = Mutex.new
123
+
105
124
  @aggregation_state = {}
106
125
 
107
126
  @pid = Process.pid
108
127
  @flush_interval = flush_interval
109
128
  start_flush_thread
110
129
 
130
+ @finalizer = Finalizer.new(
131
+ @aggregation_state,
132
+ @sink,
133
+ @datagram_builders,
134
+ @datagram_builder_class,
135
+ @default_tags,
136
+ @metric_prefix,
137
+ )
138
+
111
139
  ObjectSpace.define_finalizer(
112
140
  self,
113
- self.class.finalize(@aggregation_state, @sink, @datagram_builders, @datagram_builder_class, @default_tags),
141
+ self.class.finalize(@finalizer),
114
142
  )
115
143
  end
116
144
 
@@ -120,21 +148,82 @@ module StatsD
120
148
  # @param tags [Hash{String, Symbol => String},Array<String>] The tags to attach to the counter.
121
149
  # @param no_prefix [Boolean] If true, the metric will not be prefixed.
122
150
  # @return [void]
123
- def increment(name, value = 1, tags: [], no_prefix: false)
151
+ def increment(name, value = 1, tags: [], no_prefix: false, sample_rate: CONST_SAMPLE_RATE)
124
152
  unless thread_healthcheck
125
- @sink << datagram_builder(no_prefix: no_prefix).c(name, value, CONST_SAMPLE_RATE, tags)
153
+ @sink << datagram_builder(no_prefix: no_prefix).c(name, value, sample_rate, tags)
126
154
  return
127
155
  end
128
156
 
129
157
  tags = tags_sorted(tags)
130
- key = packet_key(name, tags, no_prefix, COUNT)
158
+ key = packet_key(name, tags, no_prefix, COUNT, sample_rate: sample_rate)
131
159
 
132
- @mutex.synchronize do
160
+ @aggregation_state_mutex.synchronize do
133
161
  @aggregation_state[key] ||= 0
134
162
  @aggregation_state[key] += value
135
163
  end
136
164
  end
137
165
 
166
+ # Aggregates a precompiled metric that can be combined into a single scalar for later flushing.
167
+ # @param precompiled_datagram [StatsD::Instrument::CompiledMetric::PrecompiledDatagram]
168
+ # The precompiled metric datagram, with the tag values already "filled-in".
169
+ # @param value [Numeric] The value to aggregate
170
+ # @return [void]
171
+ def aggregate_precompiled_increment_metric(precompiled_datagram, value = 1)
172
+ unless thread_healthcheck
173
+ # Fallback: emit directly if thread is unhealthy
174
+ @sink << precompiled_datagram.to_datagram(value)
175
+ return
176
+ end
177
+
178
+ @aggregation_state_mutex.synchronize do
179
+ @aggregation_state[precompiled_datagram] ||= 0
180
+ @aggregation_state[precompiled_datagram] += value
181
+ end
182
+ end
183
+
184
+ # Aggregates a precompiled metric that can be packed into a single datagram for later flushing.
185
+ # @param precompiled_datagram [StatsD::Instrument::CompiledMetric::PrecompiledDatagram]
186
+ # The precompiled metric datagram, with the tag values already "filled-in".
187
+ # @param value [Numeric] The value to aggregate
188
+ # @return [void]
189
+ def aggregate_precompiled_timing_metric(precompiled_datagram, value = 1)
190
+ unless thread_healthcheck
191
+ # Fallback: emit directly if thread is unhealthy
192
+ @sink << precompiled_datagram.to_datagram(value)
193
+ return
194
+ end
195
+
196
+ aggregation_state = nil
197
+
198
+ @aggregation_state_mutex.synchronize do
199
+ values = @aggregation_state[precompiled_datagram] ||= []
200
+ if values.size + 1 >= @max_values
201
+ aggregation_state = @aggregation_state
202
+ new_aggregation_state
203
+ end
204
+ values << value
205
+ end
206
+
207
+ do_flush(aggregation_state) if aggregation_state
208
+ end
209
+
210
+ # Aggregates a precompiled metric that can be combined into a single scalar for later flushing.
211
+ # @param precompiled_datagram [StatsD::Instrument::CompiledMetric::PrecompiledDatagram]
212
+ # The precompiled metric datagram, with the tag values already "filled-in".
213
+ # @param value [Numeric] The value to aggregate
214
+ # @return [void]
215
+ def aggregate_precompiled_gauge_metric(precompiled_datagram, value)
216
+ unless thread_healthcheck
217
+ # Fallback: emit directly if thread is unhealthy
218
+ @sink << precompiled_datagram.to_datagram(value)
219
+ return
220
+ end
221
+
222
+ @aggregation_state_mutex.synchronize do
223
+ @aggregation_state[precompiled_datagram] = value
224
+ end
225
+ end
226
+
138
227
  def aggregate_timing(name, value, tags: [], no_prefix: false, type: DISTRIBUTION, sample_rate: CONST_SAMPLE_RATE)
139
228
  unless thread_healthcheck
140
229
  @sink << datagram_builder(no_prefix: no_prefix).timing_value_packed(
@@ -146,13 +235,18 @@ module StatsD
146
235
  tags = tags_sorted(tags)
147
236
  key = packet_key(name, tags, no_prefix, type, sample_rate: sample_rate)
148
237
 
149
- @mutex.synchronize do
238
+ aggregation_state = nil
239
+
240
+ @aggregation_state_mutex.synchronize do
150
241
  values = @aggregation_state[key] ||= []
151
242
  if values.size + 1 >= @max_values
152
- do_flush
243
+ aggregation_state = @aggregation_state
244
+ new_aggregation_state
153
245
  end
154
246
  values << value
155
247
  end
248
+
249
+ do_flush(aggregation_state) if aggregation_state
156
250
  end
157
251
 
158
252
  def gauge(name, value, tags: [], no_prefix: false)
@@ -164,52 +258,69 @@ module StatsD
164
258
  tags = tags_sorted(tags)
165
259
  key = packet_key(name, tags, no_prefix, GAUGE)
166
260
 
167
- @mutex.synchronize do
261
+ @aggregation_state_mutex.synchronize do
168
262
  @aggregation_state[key] = value
169
263
  end
170
264
  end
171
265
 
172
266
  def flush
173
- @mutex.synchronize { do_flush }
267
+ state = nil
268
+ @aggregation_state_mutex.synchronize do
269
+ state = @aggregation_state
270
+ new_aggregation_state
271
+ end
272
+
273
+ do_flush(state)
174
274
  end
175
275
 
176
276
  private
177
277
 
178
278
  EMPTY_ARRAY = [].freeze
179
279
 
280
+ def new_aggregation_state
281
+ @aggregation_state = {}
282
+ @finalizer.aggregation_state = @aggregation_state
283
+ end
284
+
180
285
  # Flushes the aggregated metrics to the sink.
181
286
  # Iterates over the aggregation state and sends each metric to the sink.
182
287
  # If you change this function, you need to update the logic in the finalizer as well.
183
- def do_flush
184
- @aggregation_state.each do |key, value|
185
- case key.type
186
- when COUNT
187
- @sink << datagram_builder(no_prefix: key.no_prefix).c(
188
- key.name,
189
- value,
190
- CONST_SAMPLE_RATE,
191
- key.tags,
192
- )
193
- when DISTRIBUTION, MEASURE, HISTOGRAM
194
- @sink << datagram_builder(no_prefix: key.no_prefix).timing_value_packed(
195
- key.name,
196
- key.type.to_s,
197
- value,
198
- key.sample_rate,
199
- key.tags,
200
- )
201
- when GAUGE
202
- @sink << datagram_builder(no_prefix: key.no_prefix).g(
203
- key.name,
204
- value,
205
- CONST_SAMPLE_RATE,
206
- key.tags,
207
- )
208
- else
209
- StatsD.logger.error { "[#{self.class.name}] Unknown aggregation type: #{key.type}" }
288
+ def do_flush(aggregation_state)
289
+ @flush_mutex.synchronize do
290
+ aggregation_state.each do |key, value|
291
+ if key.is_a?(StatsD::Instrument::CompiledMetric::PrecompiledDatagram)
292
+ @sink << key.to_datagram(value)
293
+ next
294
+ end
295
+
296
+ case key.type
297
+ when COUNT
298
+ @sink << datagram_builder(no_prefix: key.no_prefix).c(
299
+ key.name,
300
+ value,
301
+ key.sample_rate,
302
+ key.tags,
303
+ )
304
+ when DISTRIBUTION, MEASURE, HISTOGRAM
305
+ @sink << datagram_builder(no_prefix: key.no_prefix).timing_value_packed(
306
+ key.name,
307
+ key.type.to_s,
308
+ value,
309
+ key.sample_rate,
310
+ key.tags,
311
+ )
312
+ when GAUGE
313
+ @sink << datagram_builder(no_prefix: key.no_prefix).g(
314
+ key.name,
315
+ value,
316
+ CONST_SAMPLE_RATE,
317
+ key.tags,
318
+ )
319
+ else
320
+ StatsD.logger.error { "[#{self.class.name}] Unknown aggregation type: #{key.type}" }
321
+ end
210
322
  end
211
323
  end
212
- @aggregation_state.clear
213
324
  end
214
325
 
215
326
  def tags_sorted(tags)
@@ -255,7 +366,7 @@ module StatsD
255
366
  end
256
367
 
257
368
  def thread_healthcheck
258
- @mutex.synchronize do
369
+ @aggregation_state_mutex.synchronize do
259
370
  unless @flush_thread&.alive?
260
371
  # The main thread is dead, fallback to direct writes
261
372
  return false unless Thread.main.alive?
@@ -220,14 +220,95 @@ module StatsD
220
220
  def increment(name, value = 1, sample_rate: nil, tags: nil, no_prefix: false)
221
221
  sample_rate ||= @default_sample_rate
222
222
 
223
+ return StatsD::Instrument::VOID if sample_rate && !sample?(sample_rate)
224
+
225
+ if @enable_aggregation
226
+ @aggregator.increment(name, value, tags: tags, no_prefix: no_prefix, sample_rate: sample_rate)
227
+ else
228
+ emit(datagram_builder(no_prefix: no_prefix).c(name, value, sample_rate, tags))
229
+ end
230
+
231
+ StatsD::Instrument::VOID
232
+ end
233
+
234
+ # Emits a precompiled metric for high-performance use cases.
235
+ # This method is used by {StatsD::Instrument::CompiledMetric::Counter} to emit metrics
236
+ # with minimal allocations.
237
+ #
238
+ # @param precompiled_datagram [StatsD::Instrument::CompiledMetric::PrecompiledDatagram]
239
+ # The precompiled metric datagram
240
+ # @param value [Numeric] The metric value
241
+ # @return [void]
242
+ # @api private
243
+ def emit_precompiled_increment_metric(precompiled_datagram, value)
244
+ sample_rate = precompiled_datagram.sample_rate
245
+
223
246
  if @enable_aggregation
224
- @aggregator.increment(name, value, tags: tags, no_prefix: no_prefix)
247
+ # Sampling decision is done at the definition of a compiled metric, see StatsD::Instrument::CompiledMetric#define
248
+ if sample_rate.nil? || sample?(sample_rate)
249
+ @aggregator.aggregate_precompiled_increment_metric(precompiled_datagram, value)
250
+ end
225
251
  return StatsD::Instrument::VOID
226
252
  end
227
253
 
228
254
  if sample_rate.nil? || sample?(sample_rate)
229
- emit(datagram_builder(no_prefix: no_prefix).c(name, value, sample_rate, tags))
255
+ emit(precompiled_datagram.to_datagram(value))
230
256
  end
257
+
258
+ StatsD::Instrument::VOID
259
+ end
260
+
261
+ # Emits a precompiled metric for high-performance use cases.
262
+ # This method is used by {StatsD::Instrument::CompiledMetric::Distribution} to emit metrics
263
+ # with minimal allocations.
264
+ #
265
+ # @param precompiled_datagram [StatsD::Instrument::CompiledMetric::PrecompiledDatagram]
266
+ # The precompiled metric datagram
267
+ # @param value [Numeric] The metric value
268
+ # @return [void]
269
+ # @api private
270
+ def emit_precompiled_distribution_metric(precompiled_datagram, value)
271
+ sample_rate = precompiled_datagram.sample_rate
272
+
273
+ if @enable_aggregation
274
+ # Sampling decision is done at the definition of a compiled metric, see StatsD::Instrument::CompiledMetric.define
275
+ if sample_rate.nil? || sample?(sample_rate)
276
+ @aggregator.aggregate_precompiled_timing_metric(precompiled_datagram, value)
277
+ end
278
+ return StatsD::Instrument::VOID
279
+ end
280
+
281
+ if sample_rate.nil? || sample?(sample_rate)
282
+ emit(precompiled_datagram.to_datagram(value))
283
+ end
284
+
285
+ StatsD::Instrument::VOID
286
+ end
287
+
288
+ # Emits a precompiled metric for high-performance use cases.
289
+ # This method is used by {StatsD::Instrument::CompiledMetric::Gauge} to emit metrics
290
+ # with minimal allocations.
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_precompiled_gauge_metric(precompiled_datagram, value)
298
+ sample_rate = precompiled_datagram.sample_rate
299
+
300
+ if @enable_aggregation
301
+ # Sampling decision is done at the definition of a compiled metric, see StatsD::Instrument::CompiledMetric#define
302
+ if sample_rate.nil? || sample?(sample_rate)
303
+ @aggregator.aggregate_precompiled_gauge_metric(precompiled_datagram, value)
304
+ end
305
+ return StatsD::Instrument::VOID
306
+ end
307
+
308
+ if sample_rate.nil? || sample?(sample_rate)
309
+ emit(precompiled_datagram.to_datagram(value))
310
+ end
311
+
231
312
  StatsD::Instrument::VOID
232
313
  end
233
314
 
@@ -257,7 +338,7 @@ module StatsD
257
338
  end
258
339
 
259
340
  if @enable_aggregation
260
- @aggregator.aggregate_timing(name, value, tags: tags, no_prefix: no_prefix, type: :ms)
341
+ @aggregator.aggregate_timing(name, value, tags: tags, no_prefix: no_prefix, type: :ms, sample_rate: sample_rate)
261
342
  return StatsD::Instrument::VOID
262
343
  end
263
344
  emit(datagram_builder(no_prefix: no_prefix).ms(name, value, sample_rate, tags))
@@ -367,7 +448,7 @@ module StatsD
367
448
  end
368
449
 
369
450
  if @enable_aggregation
370
- @aggregator.aggregate_timing(name, value, tags: tags, no_prefix: no_prefix, type: :h)
451
+ @aggregator.aggregate_timing(name, value, tags: tags, no_prefix: no_prefix, type: :h, sample_rate: sample_rate)
371
452
  return StatsD::Instrument::VOID
372
453
  end
373
454