Skip to content

Commit 3babbc2

Browse files
authored
Add native histogram reporting to the reporter interfaces (#278)
Tally's timers and histograms are pre-bucketed, so percentiles cannot be merged across sources. Native histograms derive their buckets from the data and merge cleanly, but every value-carrying method in tally today is a fixed-width number; a native histogram travels as a marshalled blob. Nothing existing can carry one, so the reporter interfaces have to break. This adds: StatsReporter.ReportNativeHistogram(name, tags, payload, samples) CachedStatsReporter.AllocateNativeHistogram(name, tags, maxBuckets) CachedNativeHistogram.ReportNativeHistogram(payload, samples) There is deliberately no per-bucket equivalent of CachedHistogramBucket. A native histogram rescales its buckets as it observes values, so there is no stable set of bounds to pre-allocate handles against; the whole distribution is reported as one payload. Every in-repo implementer is updated so the tree compiles. Most of them are no-ops, because their wire formats cannot carry an opaque payload: statsd carries scalar values only. prometheus has native histograms of its own, but tally hands the reporter an already-serialized payload in whatever encoding the application chose, and a blob cannot become a prometheus.Collector. m3 speaks thrift over UDP with no payload field. This one is not a plain no-op: it counts drops into tally.internal.num-native-histograms-dropped, reported only when non-zero so that fleets which never use native histograms do not gain a permanently-zero series. This commit lands before the NativeHistogram type it carries, so that the type and the Scope surface can each be reviewed against an interface that already exists.
1 parent f165c68 commit 3babbc2

12 files changed

Lines changed: 308 additions & 20 deletions

File tree

example/main.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ func (r *printStatsReporter) ReportHistogramDurationSamples(
6969
name, bucketLowerBound, bucketUpperBound, samples)
7070
}
7171

72+
func (r *printStatsReporter) ReportNativeHistogram(
73+
name string,
74+
_ map[string]string,
75+
payload []byte,
76+
samples uint64,
77+
) {
78+
fmt.Printf("native histogram %s payload %d bytes samples %d\n",
79+
name, len(payload), samples)
80+
}
81+
7282
func (r *printStatsReporter) Capabilities() tally.Capabilities {
7383
return r
7484
}

m3/reporter.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ type reporter struct {
134134
numWriteErrors atomic.Int64
135135
numWriteErrorsCounter tally.CachedCount
136136
numTagCacheCounter tally.CachedCount
137+
138+
numNativeHistogramsDropped atomic.Int64
139+
numNativeHistogramsDroppedCounter tally.CachedCount
137140
}
138141

139142
// Options is a set of options for the M3 reporter.
@@ -301,6 +304,9 @@ func NewReporter(opts Options) (Reporter, error) {
301304
r.numMetricsCounter = r.AllocateCounter("tally.internal.num-metrics", internalTags)
302305
r.numWriteErrorsCounter = r.AllocateCounter("tally.internal.num-write-errors", internalTags)
303306
r.numTagCacheCounter = r.AllocateCounter("tally.internal.num-tag-cache", internalTags)
307+
r.numNativeHistogramsDroppedCounter = r.AllocateCounter(
308+
"tally.internal.num-native-histograms-dropped", internalTags,
309+
)
304310
r.wg.Add(1)
305311
go func() {
306312
defer r.wg.Done()
@@ -441,6 +447,20 @@ func (r *reporter) AllocateHistogram(
441447
}
442448
}
443449

450+
// AllocateNativeHistogram implements tally.CachedStatsReporter.
451+
//
452+
// The M3 thrift wire format carries only count, gauge and timer values, so
453+
// there is no field a serialized native histogram payload can travel in.
454+
// Rather than silently discard them, the returned handle counts every dropped
455+
// report into tally.internal.num-native-histograms-dropped.
456+
func (r *reporter) AllocateNativeHistogram(
457+
name string,
458+
tags map[string]string,
459+
maxBuckets int,
460+
) tally.CachedNativeHistogram {
461+
return cachedNativeHistogram{reporter: r}
462+
}
463+
444464
func (r *reporter) valueBucketString(v float64) string {
445465
if v == math.MaxFloat64 {
446466
return "infinity"
@@ -699,6 +719,13 @@ func (r *reporter) reportInternalMetrics() {
699719
r.numMetricsCounter.ReportCount(metrics)
700720
r.numWriteErrorsCounter.ReportCount(writeErrors)
701721
r.numTagCacheCounter.ReportCount(int64(r.tagCache.Len()))
722+
723+
// Reported only when non-zero. Emitting it unconditionally would add a
724+
// permanently-zero series to every m3 reporter in a fleet, since almost
725+
// no caller uses native histograms with this reporter.
726+
if dropped := r.numNativeHistogramsDropped.Swap(0); dropped > 0 {
727+
r.numNativeHistogramsDroppedCounter.ReportCount(dropped)
728+
}
702729
}
703730

704731
func (r *reporter) timeLoop() {
@@ -735,6 +762,16 @@ func (c cachedMetric) ReportTimer(interval time.Duration) {
735762
c.reporter.reportCopyMetric(c.metric, c.size, "", "")
736763
}
737764

765+
// cachedNativeHistogram discards native histogram payloads, which the M3
766+
// thrift wire format cannot represent, and counts the drops.
767+
type cachedNativeHistogram struct {
768+
reporter *reporter
769+
}
770+
771+
func (c cachedNativeHistogram) ReportNativeHistogram(payload []byte, samples uint64) {
772+
c.reporter.numNativeHistogramsDropped.Inc()
773+
}
774+
738775
type noopMetric struct{}
739776

740777
func (c noopMetric) ReportCount(value int64) {}

multi/reporter.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,17 @@ func (r *multi) ReportHistogramDurationSamples(
103103
}
104104
}
105105

106+
func (r *multi) ReportNativeHistogram(
107+
name string,
108+
tags map[string]string,
109+
payload []byte,
110+
samples uint64,
111+
) {
112+
for _, r := range r.reporters {
113+
r.ReportNativeHistogram(name, tags, payload, samples)
114+
}
115+
}
116+
106117
func (r *multi) Capabilities() tally.Capabilities {
107118
return r.multiBaseReporters.Capabilities()
108119
}
@@ -175,6 +186,18 @@ func (r *multiCached) AllocateHistogram(
175186
return multiMetric{histograms: metrics}
176187
}
177188

189+
func (r *multiCached) AllocateNativeHistogram(
190+
name string,
191+
tags map[string]string,
192+
maxBuckets int,
193+
) tally.CachedNativeHistogram {
194+
metrics := make([]tally.CachedNativeHistogram, 0, len(r.reporters))
195+
for _, r := range r.reporters {
196+
metrics = append(metrics, r.AllocateNativeHistogram(name, tags, maxBuckets))
197+
}
198+
return multiMetric{nativeHistograms: metrics}
199+
}
200+
178201
func (r *multiCached) Capabilities() tally.Capabilities {
179202
return r.multiBaseReporters.Capabilities()
180203
}
@@ -184,10 +207,11 @@ func (r *multiCached) Flush() {
184207
}
185208

186209
type multiMetric struct {
187-
counters []tally.CachedCount
188-
gauges []tally.CachedGauge
189-
timers []tally.CachedTimer
190-
histograms []tally.CachedHistogram
210+
counters []tally.CachedCount
211+
gauges []tally.CachedGauge
212+
timers []tally.CachedTimer
213+
histograms []tally.CachedHistogram
214+
nativeHistograms []tally.CachedNativeHistogram
191215
}
192216

193217
func (m multiMetric) ReportCount(value int64) {
@@ -230,6 +254,12 @@ func (m multiMetric) DurationBucket(
230254
return multiHistogramBucket{multi}
231255
}
232256

257+
func (m multiMetric) ReportNativeHistogram(payload []byte, samples uint64) {
258+
for _, m := range m.nativeHistograms {
259+
m.ReportNativeHistogram(payload, samples)
260+
}
261+
}
262+
233263
type multiHistogramBucket struct {
234264
multi []tally.CachedHistogramBucket
235265
}

multi/reporter_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func TestMultiReporter(t *testing.T) {
4444
{"foo": "qux"},
4545
{"foo": "bzz"},
4646
{"foo": "buz"},
47+
{"foo": "nhg"},
4748
}
4849

4950
valueBuckets := tally.MustMakeLinearValueBuckets(0, 2, 5)
@@ -57,6 +58,7 @@ func TestMultiReporter(t *testing.T) {
5758
2.0, 4.0, 3)
5859
r.ReportHistogramDurationSamples("buz", tags[4], durationBuckets,
5960
2*time.Second, 4*time.Second, 3)
61+
r.ReportNativeHistogram("nhg", tags[5], []byte("payload"), 7)
6062
for _, r := range all {
6163
require.Equal(t, 2, len(r.counts))
6264

@@ -87,6 +89,12 @@ func TestMultiReporter(t *testing.T) {
8789
assert.Equal(t, 2*time.Second, r.histogramDurationSamples[0].bucketLowerBound)
8890
assert.Equal(t, 4*time.Second, r.histogramDurationSamples[0].bucketUpperBound)
8991
assert.Equal(t, int64(3), r.histogramDurationSamples[0].samples)
92+
93+
require.Equal(t, 1, len(r.nativeHistograms))
94+
assert.Equal(t, "nhg", r.nativeHistograms[0].name)
95+
assert.Equal(t, tags[5], r.nativeHistograms[0].tags)
96+
assert.Equal(t, []byte("payload"), r.nativeHistograms[0].payload)
97+
assert.Equal(t, uint64(7), r.nativeHistograms[0].samples)
9098
}
9199

92100
assert.NotNil(t, r.Capabilities())
@@ -112,6 +120,7 @@ func TestMultiCachedReporter(t *testing.T) {
112120
{"foo": "qux"},
113121
{"foo": "bzz"},
114122
{"foo": "buz"},
123+
{"foo": "nhg"},
115124
}
116125

117126
valueBuckets := tally.MustMakeLinearValueBuckets(0, 2, 5)
@@ -133,6 +142,9 @@ func TestMultiCachedReporter(t *testing.T) {
133142
dhist := r.AllocateHistogram("buz", tags[4], durationBuckets)
134143
dhist.DurationBucket(2*time.Second, 4*time.Second).ReportSamples(3)
135144

145+
nhist := r.AllocateNativeHistogram("nhg", tags[5], 160)
146+
nhist.ReportNativeHistogram([]byte("payload"), 7)
147+
136148
for _, r := range all {
137149
require.Equal(t, 2, len(r.counts))
138150

@@ -163,6 +175,12 @@ func TestMultiCachedReporter(t *testing.T) {
163175
assert.Equal(t, 2*time.Second, r.histogramDurationSamples[0].bucketLowerBound)
164176
assert.Equal(t, 4*time.Second, r.histogramDurationSamples[0].bucketUpperBound)
165177
assert.Equal(t, int64(3), r.histogramDurationSamples[0].samples)
178+
179+
require.Equal(t, 1, len(r.nativeHistograms))
180+
assert.Equal(t, "nhg", r.nativeHistograms[0].name)
181+
assert.Equal(t, tags[5], r.nativeHistograms[0].tags)
182+
assert.Equal(t, []byte("payload"), r.nativeHistograms[0].payload)
183+
assert.Equal(t, uint64(7), r.nativeHistograms[0].samples)
166184
}
167185

168186
assert.NotNil(t, r.Capabilities())
@@ -179,6 +197,7 @@ type capturingStatsReporter struct {
179197
timers []capturedTimer
180198
histogramValueSamples []capturedHistogramValueSamples
181199
histogramDurationSamples []capturedHistogramDurationSamples
200+
nativeHistograms []capturedNativeHistogram
182201
capabilities int
183202
flush int
184203
}
@@ -217,6 +236,13 @@ type capturedHistogramDurationSamples struct {
217236
samples int64
218237
}
219238

239+
type capturedNativeHistogram struct {
240+
name string
241+
tags map[string]string
242+
payload []byte
243+
samples uint64
244+
}
245+
220246
func newCapturingStatsReporter() *capturingStatsReporter {
221247
return &capturingStatsReporter{}
222248
}
@@ -275,6 +301,16 @@ func (r *capturingStatsReporter) ReportHistogramDurationSamples(
275301
r.histogramDurationSamples = append(r.histogramDurationSamples, elem)
276302
}
277303

304+
func (r *capturingStatsReporter) ReportNativeHistogram(
305+
name string,
306+
tags map[string]string,
307+
payload []byte,
308+
samples uint64,
309+
) {
310+
elem := capturedNativeHistogram{name, tags, payload, samples}
311+
r.nativeHistograms = append(r.nativeHistograms, elem)
312+
}
313+
278314
func (r *capturingStatsReporter) AllocateCounter(
279315
name string,
280316
tags map[string]string,
@@ -323,6 +359,17 @@ func (r *capturingStatsReporter) AllocateHistogram(
323359
}
324360
}
325361

362+
func (r *capturingStatsReporter) AllocateNativeHistogram(
363+
name string,
364+
tags map[string]string,
365+
maxBuckets int,
366+
) tally.CachedNativeHistogram {
367+
return cachedNativeHistogram{fn: func(payload []byte, samples uint64) {
368+
elem := capturedNativeHistogram{name, tags, payload, samples}
369+
r.nativeHistograms = append(r.nativeHistograms, elem)
370+
}}
371+
}
372+
326373
func (r *capturingStatsReporter) Capabilities() tally.Capabilities {
327374
r.capabilities++
328375
return r
@@ -364,6 +411,14 @@ func (c cachedTimer) ReportTimer(value time.Duration) {
364411
c.fn(value)
365412
}
366413

414+
type cachedNativeHistogram struct {
415+
fn func(payload []byte, samples uint64)
416+
}
417+
418+
func (h cachedNativeHistogram) ReportNativeHistogram(payload []byte, samples uint64) {
419+
h.fn(payload, samples)
420+
}
421+
367422
type cachedHistogram struct {
368423
valueFn func(bucketLowerBound, bucketUpperBound float64, samples int64)
369424
durationFn func(bucketLowerBound, bucketUpperBound time.Duration, samples int64)

prometheus/reporter.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,9 @@ func (m noopMetric) ReportCount(value int64) {}
216216
func (m noopMetric) ReportGauge(value float64) {}
217217
func (m noopMetric) ReportTimer(interval time.Duration) {}
218218
func (m noopMetric) ReportSamples(value int64) {}
219+
220+
func (m noopMetric) ReportNativeHistogram(payload []byte, samples uint64) {}
221+
219222
func (m noopMetric) ValueBucket(lower, upper float64) tally.CachedHistogramBucket {
220223
return m
221224
}
@@ -571,6 +574,21 @@ func (r *reporter) AllocateHistogram(
571574
return &cachedMetric{histogram: histogramVec.With(tags)}
572575
}
573576

577+
// AllocateNativeHistogram implements tally.CachedStatsReporter.
578+
//
579+
// Prometheus has native histograms of its own, but tally hands this method an
580+
// already-serialized payload in whatever encoding the application chose via
581+
// ScopeOptions.NativeHistogramFactory. An opaque blob cannot be turned into a
582+
// prometheus.Collector, so there is nothing to register and the metric is
583+
// dropped.
584+
func (r *reporter) AllocateNativeHistogram(
585+
name string,
586+
tags map[string]string,
587+
maxBuckets int,
588+
) tally.CachedNativeHistogram {
589+
return noopMetric{}
590+
}
591+
574592
func (r *reporter) Capabilities() tally.Capabilities {
575593
return r
576594
}

reporter.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ type StatsReporter interface {
7575
bucketUpperBound time.Duration,
7676
samples int64,
7777
)
78+
79+
// ReportNativeHistogram reports a serialized native histogram covering
80+
// the samples observed since the last report. The payload encoding is
81+
// determined by the NativeHistogramData the scope was configured with.
82+
ReportNativeHistogram(
83+
name string,
84+
tags map[string]string,
85+
payload []byte,
86+
samples uint64,
87+
)
7888
}
7989

8090
// CachedStatsReporter is a backend for Scopes that pre allocates all
@@ -107,6 +117,14 @@ type CachedStatsReporter interface {
107117
tags map[string]string,
108118
buckets Buckets,
109119
) CachedHistogram
120+
121+
// AllocateNativeHistogram pre allocates a native histogram data structure
122+
// with name, tags and a bucket budget.
123+
AllocateNativeHistogram(
124+
name string,
125+
tags map[string]string,
126+
maxBuckets int,
127+
) CachedNativeHistogram
110128
}
111129

112130
// CachedCount interface for reporting an individual counter
@@ -138,3 +156,13 @@ type CachedHistogram interface {
138156
type CachedHistogramBucket interface {
139157
ReportSamples(value int64)
140158
}
159+
160+
// CachedNativeHistogram interface for reporting an individual native histogram.
161+
//
162+
// There is no per-bucket equivalent of CachedHistogramBucket: a native
163+
// histogram rescales its buckets as it observes values, so there is no stable
164+
// set of bounds to pre-allocate handles against. The whole distribution is
165+
// reported as one payload instead.
166+
type CachedNativeHistogram interface {
167+
ReportNativeHistogram(payload []byte, samples uint64)
168+
}

scope_benchmark_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,8 @@ func (s noopStat) DurationBucket(bucketLowerBound, bucketUpperBound time.Duratio
290290
}
291291
func (s noopStat) ReportSamples(value int64) {}
292292

293+
func (s noopStat) ReportNativeHistogram(payload []byte, samples uint64) {}
294+
293295
type noopCachedReporter struct{}
294296

295297
func (n noopCachedReporter) Capabilities() Capabilities {
@@ -311,6 +313,9 @@ func (n noopCachedReporter) ReportHistogramValueSamples(name string, tags map[st
311313
func (n noopCachedReporter) ReportHistogramDurationSamples(name string, tags map[string]string, buckets Buckets, bucketLowerBound time.Duration, bucketUpperBound time.Duration, samples int64) {
312314
}
313315

316+
func (n noopCachedReporter) ReportNativeHistogram(name string, tags map[string]string, payload []byte, samples uint64) {
317+
}
318+
314319
func (n noopCachedReporter) AllocateCounter(name string, tags map[string]string) CachedCount {
315320
return noopStat{}
316321
}
@@ -325,3 +330,7 @@ func (n noopCachedReporter) AllocateTimer(name string, tags map[string]string) C
325330
func (n noopCachedReporter) AllocateHistogram(name string, tags map[string]string, buckets Buckets) CachedHistogram {
326331
return noopStat{}
327332
}
333+
334+
func (n noopCachedReporter) AllocateNativeHistogram(name string, tags map[string]string, maxBuckets int) CachedNativeHistogram {
335+
return noopStat{}
336+
}

0 commit comments

Comments
 (0)