netflix-spectator-rb 0.2.2 → 0.3.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: 9493e7ec87d15eaf9caf74b14e45634bf078a4f2110d72e98a4aa4d1ad9f46ae
4
- data.tar.gz: d9a7ad1d567b120a63af1064c582d65bb63fc943812939a049e5b8a8ca6cc9f3
3
+ metadata.gz: bc12cd87bc8fba5cafbbbbd582915daacd75ea11dab2eb66c60efd85e677147f
4
+ data.tar.gz: 3a1bc600d9fcf8dcc65caf1763dce1962c8600ac52f48a87236c1f1f0f7bf670
5
5
  SHA512:
6
- metadata.gz: 6ec61149fb8f779920610a0398eacf939d05035ef617d6726a972ea55bf82ab606b9c4a83f9b8ce046e81dcd2ebb38cfafdc71781b84f592549c5f4d120e8ab3
7
- data.tar.gz: 542f9777517b6ae703a8e91501bfe223c9185b4297555af00adc36a9b90f2b0c8334206439862317f3e85ef868c695e32679cebd0f55f1a181f8f1346d179725
6
+ metadata.gz: 126236080e674a60992713da731c370465637fc11024140efafcfec318166de7c304b7ba0078193aadf89555db3d1feaaee3f8e710d0f40329db069cb9ffee17
7
+ data.tar.gz: 73edbe71d33fa1adbf5dd417f097ccc13b3bec46eed6c98dc001e17b8b8255b12c543281f2cc7cecfc8873f0292aade1e34a1b471008e049e37666914e57ac98
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spectator/atomic_number'
4
- require 'spectator/measure'
3
+ require_relative 'atomic_number'
4
+ require_relative 'measure'
5
5
 
6
6
  module Spectator
7
7
  # A counter is used to measure the rate at which an event is occurring
@@ -20,7 +20,12 @@ module Spectator
20
20
 
21
21
  # Get the current count as a list of Measure and reset the count to 0
22
22
  def measure
23
- [Measure.new(@id.with_stat('count'), @count.get_and_set(0))]
23
+ cnt = @count.get_and_set(0)
24
+ if cnt.positive?
25
+ [Measure.new(@id.with_default_stat('count'), cnt)]
26
+ else
27
+ []
28
+ end
24
29
  end
25
30
 
26
31
  # Read the current count. Calls to measure will reset it
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spectator/atomic_number'
4
- require 'spectator/measure'
3
+ require_relative 'atomic_number'
4
+ require_relative 'measure'
5
5
 
6
6
  module Spectator
7
7
  # A meter with a single value that can only be sampled at a point in time.
@@ -25,7 +25,8 @@ module Spectator
25
25
 
26
26
  # Get the current value, and reset it
27
27
  def measure
28
- [Measure.new(@id.with_stat('gauge'), @value.get_and_set(Float::NAN))]
28
+ [Measure.new(@id.with_default_stat('gauge'),
29
+ @value.get_and_set(Float::NAN))]
29
30
  end
30
31
 
31
32
  # A string representation of this gauge, useful for debugging purposes
@@ -0,0 +1,551 @@
1
+ module Spectator
2
+ # Module for percentile approximations
3
+ module Histogram
4
+ require 'spectator/meter_id'
5
+
6
+ # Internal helper class used by PercentileTimer and
7
+ # PercentileDistributionSummary to help with generating and using
8
+ # buckets for percentile approximations
9
+
10
+ # rubocop: disable Metrics/ClassLength
11
+ class PercentileBuckets
12
+ # rubocop:enable Metrics/ClassLength
13
+ MAX_VALUE = 9_223_372_036_854_775_807
14
+
15
+ @bucket_values = [
16
+ 1,
17
+ 2,
18
+ 3,
19
+ 4,
20
+ 5,
21
+ 6,
22
+ 7,
23
+ 8,
24
+ 9,
25
+ 10,
26
+ 11,
27
+ 12,
28
+ 13,
29
+ 14,
30
+ 16,
31
+ 21,
32
+ 26,
33
+ 31,
34
+ 36,
35
+ 41,
36
+ 46,
37
+ 51,
38
+ 56,
39
+ 64,
40
+ 85,
41
+ 106,
42
+ 127,
43
+ 148,
44
+ 169,
45
+ 190,
46
+ 211,
47
+ 232,
48
+ 256,
49
+ 341,
50
+ 426,
51
+ 511,
52
+ 596,
53
+ 681,
54
+ 766,
55
+ 851,
56
+ 936,
57
+ 1024,
58
+ 1365,
59
+ 1706,
60
+ 2047,
61
+ 2388,
62
+ 2729,
63
+ 3070,
64
+ 3411,
65
+ 3752,
66
+ 4096,
67
+ 5461,
68
+ 6826,
69
+ 8191,
70
+ 9556,
71
+ 10_921,
72
+ 12_286,
73
+ 13_651,
74
+ 15_016,
75
+ 16_384,
76
+ 21_845,
77
+ 27_306,
78
+ 32_767,
79
+ 38_228,
80
+ 43_689,
81
+ 49_150,
82
+ 54_611,
83
+ 60_072,
84
+ 65_536,
85
+ 87_381,
86
+ 109_226,
87
+ 131_071,
88
+ 152_916,
89
+ 174_761,
90
+ 196_606,
91
+ 218_451,
92
+ 240_296,
93
+ 262_144,
94
+ 349_525,
95
+ 436_906,
96
+ 524_287,
97
+ 611_668,
98
+ 699_049,
99
+ 786_430,
100
+ 873_811,
101
+ 961_192,
102
+ 1_048_576,
103
+ 1_398_101,
104
+ 1_747_626,
105
+ 2_097_151,
106
+ 2_446_676,
107
+ 2_796_201,
108
+ 3_145_726,
109
+ 3_495_251,
110
+ 3_844_776,
111
+ 4_194_304,
112
+ 5_592_405,
113
+ 6_990_506,
114
+ 8_388_607,
115
+ 9_786_708,
116
+ 11_184_809,
117
+ 12_582_910,
118
+ 13_981_011,
119
+ 15_379_112,
120
+ 16_777_216,
121
+ 22_369_621,
122
+ 27_962_026,
123
+ 33_554_431,
124
+ 39_146_836,
125
+ 44_739_241,
126
+ 50_331_646,
127
+ 55_924_051,
128
+ 61_516_456,
129
+ 67_108_864,
130
+ 89_478_485,
131
+ 111_848_106,
132
+ 134_217_727,
133
+ 156_587_348,
134
+ 178_956_969,
135
+ 201_326_590,
136
+ 223_696_211,
137
+ 246_065_832,
138
+ 268_435_456,
139
+ 357_913_941,
140
+ 447_392_426,
141
+ 536_870_911,
142
+ 626_349_396,
143
+ 715_827_881,
144
+ 805_306_366,
145
+ 894_784_851,
146
+ 984_263_336,
147
+ 1_073_741_824,
148
+ 1_431_655_765,
149
+ 1_789_569_706,
150
+ 2_147_483_647,
151
+ 2_505_397_588,
152
+ 2_863_311_529,
153
+ 3_221_225_470,
154
+ 3_579_139_411,
155
+ 3_937_053_352,
156
+ 4_294_967_296,
157
+ 5_726_623_061,
158
+ 7_158_278_826,
159
+ 8_589_934_591,
160
+ 10_021_590_356,
161
+ 11_453_246_121,
162
+ 12_884_901_886,
163
+ 14_316_557_651,
164
+ 15_748_213_416,
165
+ 17_179_869_184,
166
+ 22_906_492_245,
167
+ 28_633_115_306,
168
+ 34_359_738_367,
169
+ 40_086_361_428,
170
+ 45_812_984_489,
171
+ 51_539_607_550,
172
+ 57_266_230_611,
173
+ 62_992_853_672,
174
+ 68_719_476_736,
175
+ 91_625_968_981,
176
+ 114_532_461_226,
177
+ 137_438_953_471,
178
+ 160_345_445_716,
179
+ 183_251_937_961,
180
+ 206_158_430_206,
181
+ 229_064_922_451,
182
+ 251_971_414_696,
183
+ 274_877_906_944,
184
+ 366_503_875_925,
185
+ 458_129_844_906,
186
+ 549_755_813_887,
187
+ 641_381_782_868,
188
+ 733_007_751_849,
189
+ 824_633_720_830,
190
+ 916_259_689_811,
191
+ 1_007_885_658_792,
192
+ 1_099_511_627_776,
193
+ 1_466_015_503_701,
194
+ 1_832_519_379_626,
195
+ 2_199_023_255_551,
196
+ 2_565_527_131_476,
197
+ 2_932_031_007_401,
198
+ 3_298_534_883_326,
199
+ 3_665_038_759_251,
200
+ 4_031_542_635_176,
201
+ 4_398_046_511_104,
202
+ 5_864_062_014_805,
203
+ 7_330_077_518_506,
204
+ 8_796_093_022_207,
205
+ 10_262_108_525_908,
206
+ 11_728_124_029_609,
207
+ 13_194_139_533_310,
208
+ 14_660_155_037_011,
209
+ 16_126_170_540_712,
210
+ 17_592_186_044_416,
211
+ 23_456_248_059_221,
212
+ 29_320_310_074_026,
213
+ 35_184_372_088_831,
214
+ 41_048_434_103_636,
215
+ 46_912_496_118_441,
216
+ 52_776_558_133_246,
217
+ 58_640_620_148_051,
218
+ 64_504_682_162_856,
219
+ 70_368_744_177_664,
220
+ 93_824_992_236_885,
221
+ 117_281_240_296_106,
222
+ 140_737_488_355_327,
223
+ 164_193_736_414_548,
224
+ 187_649_984_473_769,
225
+ 211_106_232_532_990,
226
+ 234_562_480_592_211,
227
+ 258_018_728_651_432,
228
+ 281_474_976_710_656,
229
+ 375_299_968_947_541,
230
+ 469_124_961_184_426,
231
+ 562_949_953_421_311,
232
+ 656_774_945_658_196,
233
+ 750_599_937_895_081,
234
+ 844_424_930_131_966,
235
+ 938_249_922_368_851,
236
+ 1_032_074_914_605_736,
237
+ 1_125_899_906_842_624,
238
+ 1_501_199_875_790_165,
239
+ 1_876_499_844_737_706,
240
+ 2_251_799_813_685_247,
241
+ 2_627_099_782_632_788,
242
+ 3_002_399_751_580_329,
243
+ 3_377_699_720_527_870,
244
+ 3_752_999_689_475_411,
245
+ 4_128_299_658_422_952,
246
+ 4_503_599_627_370_496,
247
+ 6_004_799_503_160_661,
248
+ 7_505_999_378_950_826,
249
+ 9_007_199_254_740_991,
250
+ 10_508_399_130_531_156,
251
+ 12_009_599_006_321_321,
252
+ 13_510_798_882_111_486,
253
+ 15_011_998_757_901_651,
254
+ 16_513_198_633_691_816,
255
+ 18_014_398_509_481_984,
256
+ 24_019_198_012_642_645,
257
+ 30_023_997_515_803_306,
258
+ 36_028_797_018_963_967,
259
+ 42_033_596_522_124_628,
260
+ 48_038_396_025_285_289,
261
+ 54_043_195_528_445_950,
262
+ 60_047_995_031_606_611,
263
+ 66_052_794_534_767_272,
264
+ 72_057_594_037_927_936,
265
+ 96_076_792_050_570_581,
266
+ 120_095_990_063_213_226,
267
+ 144_115_188_075_855_871,
268
+ 168_134_386_088_498_516,
269
+ 192_153_584_101_141_161,
270
+ 216_172_782_113_783_806,
271
+ 240_191_980_126_426_451,
272
+ 264_211_178_139_069_096,
273
+ 288_230_376_151_711_744,
274
+ 384_307_168_202_282_325,
275
+ 480_383_960_252_852_906,
276
+ 576_460_752_303_423_487,
277
+ 672_537_544_353_994_068,
278
+ 768_614_336_404_564_649,
279
+ 864_691_128_455_135_230,
280
+ 960_767_920_505_705_811,
281
+ 1_056_844_712_556_276_392,
282
+ 1_152_921_504_606_846_976,
283
+ 1_537_228_672_809_129_301,
284
+ 1_921_535_841_011_411_626,
285
+ 2_305_843_009_213_693_951,
286
+ 2_690_150_177_415_976_276,
287
+ 3_074_457_345_618_258_601,
288
+ 3_458_764_513_820_540_926,
289
+ 3_843_071_682_022_823_251,
290
+ 4_227_378_850_225_105_576,
291
+ MAX_VALUE
292
+ ]
293
+
294
+ @power_of_4_index = [
295
+ 0,
296
+ 3,
297
+ 14,
298
+ 23,
299
+ 32,
300
+ 41,
301
+ 50,
302
+ 59,
303
+ 68,
304
+ 77,
305
+ 86,
306
+ 95,
307
+ 104,
308
+ 113,
309
+ 122,
310
+ 131,
311
+ 140,
312
+ 149,
313
+ 158,
314
+ 167,
315
+ 176,
316
+ 185,
317
+ 194,
318
+ 203,
319
+ 212,
320
+ 221,
321
+ 230,
322
+ 239,
323
+ 248,
324
+ 257,
325
+ 266,
326
+ 275
327
+ ]
328
+
329
+ def self.num_leading_zeros(value)
330
+ leading = 64
331
+ while value.positive?
332
+ value >>= 1
333
+ leading -= 1
334
+ end
335
+ leading
336
+ end
337
+
338
+ # rubocop: disable Metrics/MethodLength
339
+ def self.index_of(value)
340
+ if value <= 0
341
+ 0
342
+ elsif value <= 4
343
+ value
344
+ else
345
+ lz = num_leading_zeros(value)
346
+ shift = 64 - lz - 1
347
+ prev_pwr2 = (value >> shift) << shift
348
+ prev_pwr4 = prev_pwr2
349
+ if shift.odd?
350
+ shift -= 1
351
+ prev_pwr4 >>= 1
352
+ end
353
+ base = prev_pwr4
354
+ delta = base / 3
355
+ offset = (value - base) / delta
356
+ pos = offset + @power_of_4_index[shift / 2]
357
+ if pos >= length - 1
358
+ length - 1
359
+ else
360
+ pos + 1
361
+ end
362
+ end
363
+ end
364
+ # rubocop: enable Metrics/MethodLength
365
+
366
+ def self.length
367
+ @bucket_values.length
368
+ end
369
+
370
+ def self.get(index)
371
+ @bucket_values[index]
372
+ end
373
+
374
+ def self.bucket(value)
375
+ @bucket_values[index_of(value)]
376
+ end
377
+
378
+ def self.percentiles(counts, pcts, results)
379
+ check_perc_args(counts, pcts, results)
380
+ total = counts.inject(0, :+)
381
+
382
+ pct_idx = 0
383
+ prev = 0
384
+ prev_p = 0
385
+ prev_b = 0
386
+ (0..length).each do |i|
387
+ nxt = prev + counts[i]
388
+ next_p = 100.0 * nxt / total
389
+ next_b = @bucket_values[i]
390
+
391
+ while pct_idx < pcts.length && next_p >= pcts[pct_idx]
392
+ f = (pcts[pct_idx] - prev_p) / (next_p - prev_p)
393
+ results[pct_idx] = f * (next_b - prev_b) + prev_b
394
+ pct_idx += 1
395
+ end
396
+
397
+ break if pct_idx >= pcts.length
398
+
399
+ prev = nxt
400
+ prev_p = next_p
401
+ prev_b = next_b
402
+ end
403
+ end
404
+
405
+ def self.percentile(counts, perc)
406
+ pcts = [perc]
407
+ results = [0.0]
408
+ percentiles(counts, pcts, results)
409
+ results[0]
410
+ end
411
+
412
+ def self.counters(registry, id, prefix)
413
+ (0...length).map do |i|
414
+ tags = { statistic: 'percentile',
415
+ percentile: prefix + format('%04X', i) }
416
+ counter_id = id.with_tags(tags)
417
+ registry.counter_with_id(counter_id)
418
+ end
419
+ end
420
+
421
+ def self.check_perc_args(counts, pcts, results)
422
+ if counts.length != length
423
+ raise ArgumentError(
424
+ 'counts is not the same size as the buckets array'
425
+ )
426
+ end
427
+
428
+ raise ArgumentError('pcts cannot be empty') if pcts.empty?
429
+
430
+ raise ArgumentError('pcts is not the same size as the results array') if
431
+ pcts.length != results.length
432
+ end
433
+ end
434
+
435
+ # Timer that buckets the counts to allow for estimating percentiles. This
436
+ # timer type will track the data distribution for the timer by maintaining
437
+ # a set of counters. The distribution can then be used on the server side
438
+ # to estimate percentiles while still allowing for arbitrary slicing and
439
+ # dicing based on dimensions.
440
+ #
441
+ # <b>Percentile timers are expensive compared to basic timers from the
442
+ # registry.</b> In particular they have a higher storage cost, worst case
443
+ # ~300x, to maintain the data distribution. Be diligent about any additional
444
+ # dimensions added to percentile timers and ensure they have a small bounded
445
+ # cardinality. In addition it is highly recommended to set a range (using
446
+ # the min and max parameters in the constructor which expect times in
447
+ # seconds) to greatly restrict the worst case overhead.
448
+ #
449
+ class PercentileTimer
450
+ def initialize(registry, name, tags = nil, min = 10e-3, max = 60)
451
+ @registry = registry
452
+ @id = Spectator::MeterId.new(name, tags)
453
+ @min = min * 1e9
454
+ @max = max * 1e9
455
+ @timer = registry.timer_with_id(@id)
456
+ @counters = PercentileBuckets.counters(registry, @id, 'T')
457
+ end
458
+
459
+ def record(nanos)
460
+ @timer.record(nanos)
461
+ restricted = restrict(nanos)
462
+ idx = PercentileBuckets.index_of(restricted)
463
+ @counters[idx].increment
464
+ end
465
+
466
+ def time
467
+ start = @registry.clock.monotonic_time
468
+ yield
469
+ elapsed = @registry.clock.monotonic_time - start
470
+ record(elapsed)
471
+ end
472
+
473
+ # return the given percentile in seconds
474
+ def percentile(perc)
475
+ counts = @counters.map(&:count)
476
+ v = PercentileBuckets.percentile(counts, perc)
477
+ v / 1e9
478
+ end
479
+
480
+ def total_time
481
+ @timer.total_time
482
+ end
483
+
484
+ def count
485
+ @timer.count
486
+ end
487
+
488
+ private
489
+
490
+ def restrict(nanos)
491
+ nanos = @max if nanos > @max
492
+ nanos = @min if nanos < @min
493
+ nanos.floor
494
+ end
495
+ end
496
+
497
+ # Distribution summary that buckets the counts to allow for estimating
498
+ # percentiles. This distribution summary type will track the data
499
+ # distribution for the summary by maintaining a set of counters. The
500
+ # distribution can then be used on the server side to estimate percentiles
501
+ # while still allowing for arbitrary slicing and dicing based on dimensions.
502
+ #
503
+ # <b>Percentile distribution summaries are expensive compared to basic
504
+ # distribution summaries from the registry.</b> In particular they have a
505
+ # higher storage cost, worst case ~300x, to maintain the data distribution.
506
+ # Be diligent about any additional dimensions added to percentile
507
+ # distribution summaries and ensure they have a small bounded cardinality.
508
+ # In addition it is highly recommended to set a threshold (using the min and
509
+ # max parameters in the constructor) whenever possible to greatly restrict
510
+ # the worst case overhead.
511
+ class PercentileDistributionSummary
512
+ def initialize(registry, name, tags = nil, min = 0, max = MAX_VALUE)
513
+ @registry = registry
514
+ @id = Spectator::MeterId.new(name, tags)
515
+ @min = min
516
+ @max = max
517
+ @ds = registry.distribution_summary_with_id(@id)
518
+ @counters = PercentileBuckets.counters(registry, @id, 'D')
519
+ end
520
+
521
+ def record(amount)
522
+ @ds.record(amount)
523
+ restricted = restrict(amount)
524
+ idx = PercentileBuckets.index_of(restricted)
525
+ @counters[idx].increment
526
+ end
527
+
528
+ # return the given percentile
529
+ def percentile(perc)
530
+ counts = @counters.map(&:count)
531
+ PercentileBuckets.percentile(counts, perc)
532
+ end
533
+
534
+ def total_amount
535
+ @ds.total_amount
536
+ end
537
+
538
+ def count
539
+ @ds.count
540
+ end
541
+
542
+ private
543
+
544
+ def restrict(nanos)
545
+ nanos = @max if nanos > @max
546
+ nanos = @min if nanos < @min
547
+ nanos.floor
548
+ end
549
+ end
550
+ end
551
+ end
@@ -2,6 +2,7 @@ module Spectator
2
2
  # Identifier for a meter or Measure
3
3
  class MeterId
4
4
  attr_reader :name, :tags
5
+
5
6
  def initialize(name, maybe_tags = nil)
6
7
  tags = maybe_tags.nil? ? {} : maybe_tags
7
8
  @name = name.to_sym
@@ -18,11 +19,31 @@ module Spectator
18
19
  MeterId.new(@name, new_tags)
19
20
  end
20
21
 
22
+ # Create a new MeterId adding the given tags
23
+ def with_tags(additional_tags)
24
+ new_tags = @tags.dup
25
+ additional_tags.each do |k, v|
26
+ new_tags[k] = v
27
+ end
28
+ MeterId.new(@name, new_tags)
29
+ end
30
+
21
31
  # Create a new MeterId with key=statistic and the given value
22
32
  def with_stat(stat_value)
23
33
  with_tag(:statistic, stat_value)
24
34
  end
25
35
 
36
+ # Get a MeterId with a statistic tag. If the current MeterId
37
+ # already includes statistic then just return it, otherwise create
38
+ # a new one
39
+ def with_default_stat(stat_value)
40
+ if tags.key?(:statistic)
41
+ self
42
+ else
43
+ with_tag(:statistic, stat_value)
44
+ end
45
+ end
46
+
26
47
  # lazyily compute a key to be used in hashes for efficiency
27
48
  def key
28
49
  @key ||= begin
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spectator/clock'
4
- require 'spectator/counter'
5
- require 'spectator/distribution_summary'
6
- require 'spectator/gauge'
7
- require 'spectator/http'
8
- require 'spectator/meter_id'
9
- require 'spectator/timer'
3
+ require_relative 'clock'
4
+ require_relative 'counter'
5
+ require_relative 'distribution_summary'
6
+ require_relative 'gauge'
7
+ require_relative 'http'
8
+ require_relative 'meter_id'
9
+ require_relative 'timer'
10
10
 
11
11
  module Spectator
12
12
  # Registry to manage a set of meters
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spectator/atomic_number'
4
- require 'spectator/clock'
3
+ require_relative 'atomic_number'
4
+ require_relative 'clock'
5
5
 
6
6
  module Spectator
7
7
  # The class Timer is intended to track a large number of
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spectator
4
- VERSION = '0.2.2'
4
+ VERSION = '0.3.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: netflix-spectator-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Muino
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-07-22 00:00:00.000000000 Z
11
+ date: 2019-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -91,6 +91,7 @@ files:
91
91
  - lib/spectator/counter.rb
92
92
  - lib/spectator/distribution_summary.rb
93
93
  - lib/spectator/gauge.rb
94
+ - lib/spectator/histogram/percentiles.rb
94
95
  - lib/spectator/http.rb
95
96
  - lib/spectator/measure.rb
96
97
  - lib/spectator/meter_id.rb