Skip to content

Commit ae78230

Browse files
committed
[prometheusreceiver]: allow mixed classic and native histogram
This is technical dept from open-telemetry#28663, especially in migration scenarios we want to receive both versions of the same histogram, the classic explicit and exponential bucket histogram. The current internal model of the receiver does not allow this, so I modified the model to not only key metrics by metric family name, but also whether it's an exponential histogram. Some callbacks from Promtheus scrape don't tell us what the metric type is up front. For AppendExemplar to work I made transaction stateful to remember if it appended an exponential histogram before. For Append of staleness marker to work, I modified the algorithm to discover exponential histogram from the cached metadata and the name. Signed-off-by: György Krajcsovits <[email protected]>
1 parent 0ec4ee9 commit ae78230

File tree

3 files changed

+115
-34
lines changed

3 files changed

+115
-34
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: bug_fix
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: prometheusreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: When a histogram metric has both classic and native histogram buckets, keep both, instead of throwing away the native histogram buckets.
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [26555]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext: This was a technical dept from the previous implementation in PR 28663.
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]

receiver/prometheusreceiver/internal/transaction.go

Lines changed: 76 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,23 @@ type resourceKey struct {
4141
job string
4242
instance string
4343
}
44+
45+
// The name of the metric family doesn't include magic suffixes (e.g. _bucket),
46+
// so for a classic histgram and a native histogram of the same family, the
47+
// metric family will be the same. To be able to tell them apart, we need to
48+
// store whether the metric is a native histogram or not.
49+
type metricFamilyKey struct {
50+
isExponentialHistogram bool
51+
name string
52+
}
53+
4454
type transaction struct {
4555
isNew bool
4656
trimSuffixes bool
4757
enableNativeHistograms bool
58+
addingNativeHistogram bool // true if the last sample was a native histogram.
4859
ctx context.Context
49-
families map[resourceKey]map[scopeID]map[string]*metricFamily
60+
families map[resourceKey]map[scopeID]map[metricFamilyKey]*metricFamily
5061
mc scrape.MetricMetadataStore
5162
sink consumer.Metrics
5263
externalLabels labels.Labels
@@ -79,7 +90,7 @@ func newTransaction(
7990
) *transaction {
8091
return &transaction{
8192
ctx: ctx,
82-
families: make(map[resourceKey]map[scopeID]map[string]*metricFamily),
93+
families: make(map[resourceKey]map[scopeID]map[metricFamilyKey]*metricFamily),
8394
isNew: true,
8495
trimSuffixes: trimSuffixes,
8596
enableNativeHistograms: enableNativeHistograms,
@@ -97,6 +108,8 @@ func newTransaction(
97108

98109
// Append always returns 0 to disable label caching.
99110
func (t *transaction) Append(_ storage.SeriesRef, ls labels.Labels, atMs int64, val float64) (storage.SeriesRef, error) {
111+
t.addingNativeHistogram = false
112+
100113
select {
101114
case <-t.ctx.Done():
102115
return 0, errTransactionAborted
@@ -157,15 +170,16 @@ func (t *transaction) Append(_ storage.SeriesRef, ls labels.Labels, atMs int64,
157170
return 0, nil
158171
}
159172

160-
curMF, existing := t.getOrCreateMetricFamily(*rKey, getScopeID(ls), metricName)
173+
scope := getScopeID(ls)
161174

162-
if t.enableNativeHistograms && curMF.mtype == pmetric.MetricTypeExponentialHistogram {
163-
// If a histogram has both classic and native version, the native histogram is scraped
164-
// first. Getting a float sample for the same series means that `scrape_classic_histogram`
165-
// is set to true in the scrape config. In this case, we should ignore the native histogram.
166-
curMF.mtype = pmetric.MetricTypeHistogram
175+
if t.enableNativeHistograms && value.IsStaleNaN(val) {
176+
if t.detectAndStoreNativeHistogramStaleness(atMs, rKey, scope, metricName, ls) {
177+
return 0, nil
178+
}
167179
}
168180

181+
curMF, existing := t.getOrCreateMetricFamily(*rKey, scope, metricName)
182+
169183
seriesRef := t.getSeriesRef(ls, curMF.mtype)
170184
err = curMF.addSeries(seriesRef, metricName, ls, atMs, val)
171185
if err != nil {
@@ -190,26 +204,64 @@ func (t *transaction) Append(_ storage.SeriesRef, ls labels.Labels, atMs int64,
190204
return 0, nil // never return errors, as that fails the whole scrape
191205
}
192206

207+
// detectAndStoreNativeHistogramStaleness returns true if it detects
208+
// and stores a native histogram staleness marker.
209+
func (t *transaction) detectAndStoreNativeHistogramStaleness(atMs int64, key *resourceKey, scope scopeID, metricName string, ls labels.Labels) bool {
210+
// Detect the special case of stale native histogram series.
211+
// Currently Prometheus does not store the histogram type in
212+
// its staleness tracker.
213+
md, ok := t.mc.GetMetadata(metricName)
214+
if !ok {
215+
// Native histograms always have metadata.
216+
return false
217+
}
218+
if md.Type != model.MetricTypeHistogram {
219+
// Not a histogram.
220+
return false
221+
}
222+
if md.Metric != metricName {
223+
// Not a native histogram because it has magic suffixes (e.g. _bucket).
224+
return false
225+
}
226+
// Store the staleness marker as a native histogram.
227+
t.addingNativeHistogram = true
228+
229+
curMF, _ := t.getOrCreateMetricFamily(*key, scope, metricName)
230+
seriesRef := t.getSeriesRef(ls, curMF.mtype)
231+
232+
_ = curMF.addExponentialHistogramSeries(seriesRef, metricName, ls, atMs, &histogram.Histogram{Sum: math.Float64frombits(value.StaleNaN)}, nil)
233+
// ignore errors here, this is best effort.
234+
235+
return true
236+
}
237+
193238
// getOrCreateMetricFamily returns the metric family for the given metric name and scope,
194239
// and true if an existing family was found.
195240
func (t *transaction) getOrCreateMetricFamily(key resourceKey, scope scopeID, mn string) (*metricFamily, bool) {
196241
if _, ok := t.families[key]; !ok {
197-
t.families[key] = make(map[scopeID]map[string]*metricFamily)
242+
t.families[key] = make(map[scopeID]map[metricFamilyKey]*metricFamily)
198243
}
199244
if _, ok := t.families[key][scope]; !ok {
200-
t.families[key][scope] = make(map[string]*metricFamily)
245+
t.families[key][scope] = make(map[metricFamilyKey]*metricFamily)
201246
}
202247

203-
curMf, ok := t.families[key][scope][mn]
248+
mfKey := metricFamilyKey{isExponentialHistogram: t.addingNativeHistogram, name: mn}
249+
250+
curMf, ok := t.families[key][scope][mfKey]
251+
204252
if !ok {
205253
fn := mn
206254
if _, ok := t.mc.GetMetadata(mn); !ok {
207255
fn = normalizeMetricName(mn)
208256
}
209-
mf, ok := t.families[key][scope][fn]
257+
fnKey := metricFamilyKey{isExponentialHistogram: mfKey.isExponentialHistogram, name: fn}
258+
mf, ok := t.families[key][scope][fnKey]
210259
if !ok || !mf.includesMetric(mn) {
211260
curMf = newMetricFamily(mn, t.mc, t.logger)
212-
t.families[key][scope][curMf.name] = curMf
261+
if curMf.mtype == pmetric.MetricTypeHistogram && mfKey.isExponentialHistogram {
262+
curMf.mtype = pmetric.MetricTypeExponentialHistogram
263+
}
264+
t.families[key][scope][metricFamilyKey{isExponentialHistogram: mfKey.isExponentialHistogram, name: curMf.name}] = curMf
213265
return curMf, false
214266
}
215267
curMf = mf
@@ -257,6 +309,8 @@ func (t *transaction) AppendHistogram(_ storage.SeriesRef, ls labels.Labels, atM
257309
default:
258310
}
259311

312+
t.addingNativeHistogram = true
313+
260314
if t.externalLabels.Len() != 0 {
261315
b := labels.NewBuilder(ls)
262316
t.externalLabels.Range(func(l labels.Label) {
@@ -286,13 +340,7 @@ func (t *transaction) AppendHistogram(_ storage.SeriesRef, ls labels.Labels, atM
286340
// The `up`, `target_info`, `otel_scope_info` metrics should never generate native histograms,
287341
// thus we don't check for them here as opposed to the Append function.
288342

289-
curMF, existing := t.getOrCreateMetricFamily(*rKey, getScopeID(ls), metricName)
290-
if !existing {
291-
curMF.mtype = pmetric.MetricTypeExponentialHistogram
292-
} else if curMF.mtype != pmetric.MetricTypeExponentialHistogram {
293-
// Already scraped as classic histogram.
294-
return 0, nil
295-
}
343+
curMF, _ := t.getOrCreateMetricFamily(*rKey, getScopeID(ls), metricName)
296344

297345
if h != nil && h.CounterResetHint == histogram.GaugeType || fh != nil && fh.CounterResetHint == histogram.GaugeType {
298346
t.logger.Warn("dropping unsupported gauge histogram datapoint", zap.String("metric_name", metricName), zap.Any("labels", ls))
@@ -307,14 +355,16 @@ func (t *transaction) AppendHistogram(_ storage.SeriesRef, ls labels.Labels, atM
307355
}
308356

309357
func (t *transaction) AppendCTZeroSample(_ storage.SeriesRef, ls labels.Labels, atMs, ctMs int64) (storage.SeriesRef, error) {
310-
return t.setCreationTimestamp(ls, atMs, ctMs, false)
358+
t.addingNativeHistogram = false
359+
return t.setCreationTimestamp(ls, atMs, ctMs)
311360
}
312361

313362
func (t *transaction) AppendHistogramCTZeroSample(_ storage.SeriesRef, ls labels.Labels, atMs, ctMs int64, _ *histogram.Histogram, _ *histogram.FloatHistogram) (storage.SeriesRef, error) {
314-
return t.setCreationTimestamp(ls, atMs, ctMs, true)
363+
t.addingNativeHistogram = true
364+
return t.setCreationTimestamp(ls, atMs, ctMs)
315365
}
316366

317-
func (t *transaction) setCreationTimestamp(ls labels.Labels, atMs, ctMs int64, histogram bool) (storage.SeriesRef, error) {
367+
func (t *transaction) setCreationTimestamp(ls labels.Labels, atMs, ctMs int64) (storage.SeriesRef, error) {
318368
select {
319369
case <-t.ctx.Done():
320370
return 0, errTransactionAborted
@@ -347,16 +397,7 @@ func (t *transaction) setCreationTimestamp(ls labels.Labels, atMs, ctMs int64, h
347397
return 0, errMetricNameNotFound
348398
}
349399

350-
curMF, existing := t.getOrCreateMetricFamily(*rKey, getScopeID(ls), metricName)
351-
352-
if histogram {
353-
if !existing {
354-
curMF.mtype = pmetric.MetricTypeExponentialHistogram
355-
} else if curMF.mtype != pmetric.MetricTypeExponentialHistogram {
356-
// Already scraped as classic histogram.
357-
return 0, nil
358-
}
359-
}
400+
curMF, _ := t.getOrCreateMetricFamily(*rKey, getScopeID(ls), metricName)
360401

361402
seriesRef := t.getSeriesRef(ls, curMF.mtype)
362403
curMF.addCreationTimestamp(seriesRef, ls, atMs, ctMs)
@@ -543,6 +584,7 @@ func (t *transaction) UpdateMetadata(_ storage.SeriesRef, _ labels.Labels, _ met
543584
}
544585

545586
func (t *transaction) AddTargetInfo(key resourceKey, ls labels.Labels) {
587+
t.addingNativeHistogram = false
546588
if resource, ok := t.nodeResources[key]; ok {
547589
attrs := resource.Attributes()
548590
ls.Range(func(lbl labels.Label) {
@@ -555,6 +597,7 @@ func (t *transaction) AddTargetInfo(key resourceKey, ls labels.Labels) {
555597
}
556598

557599
func (t *transaction) addScopeInfo(key resourceKey, ls labels.Labels) {
600+
t.addingNativeHistogram = false
558601
attrs := pcommon.NewMap()
559602
scope := scopeID{}
560603
ls.Range(func(lbl labels.Label) {

receiver/prometheusreceiver/metrics_receiver_protobuf_test.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ func TestNativeVsClassicHistogramScrapeViaProtobuf(t *testing.T) {
352352
}},
353353
nil,
354354
},
355-
{ // Only scrape classic buckets from mixed histograms.
355+
{ // Scrape both classic and native buckets from mixed histograms.
356356
"test_mixed_histogram",
357357
pmetric.MetricTypeHistogram,
358358
"",
@@ -363,6 +363,17 @@ func TestNativeVsClassicHistogramScrapeViaProtobuf(t *testing.T) {
363363
}},
364364
nil,
365365
},
366+
{ // Scrape both classic and native buckets from mixed histograms.
367+
"test_mixed_histogram",
368+
pmetric.MetricTypeExponentialHistogram,
369+
"",
370+
[]dataPointExpectation{{
371+
exponentialHistogramComparator: []exponentialHistogramComparator{
372+
compareExponentialHistogram(3, 1213, 456, 2, -1, []uint64{1, 0, 2}, -3, []uint64{1, 0, 1}),
373+
},
374+
}},
375+
nil,
376+
},
366377
{ // Scrape native only histograms as is.
367378
"test_native_histogram",
368379
pmetric.MetricTypeExponentialHistogram,

0 commit comments

Comments
 (0)