Repository: prometheus/prometheus (language: go)

Issue:
Native Histograms: not all examplars are ingested
### What did you do?

Exposed exemplars alongside classic buckets, but also exposed a native histogram.

### What did you expect to see?

The native histogram should be ingested together with all the exemplars assigned to the classic buckets.

### What did you see instead? Under which circumstances?

Only the first exemplar is ingested.

### Notes

See https://cloud-native.slack.com/archives/C02KR205UMU/p1688414381799849 for the findings.

It's unclear if that happened with `scrape_classic_histograms` set or not, but that has to be checked in any case.


Discussion / hints:
The protobuf parser seems to do the right thing. Multiple exemplars per histogram is covered by tests. So we have to assume that the exemplars are thrown away later in the process. Investigating…

Fix patch:
diff --git a/model/textparse/interface.go b/model/textparse/interface.go
index efa581410fa..38903afc969 100644
--- a/model/textparse/interface.go
+++ b/model/textparse/interface.go
@@ -59,7 +59,9 @@ type Parser interface {
 	Metric(l *labels.Labels) string
 
 	// Exemplar writes the exemplar of the current sample into the passed
-	// exemplar. It returns if an exemplar exists or not.
+	// exemplar. It can be called repeatedly to retrieve multiple exemplars
+	// for the same sample. It returns false once all exemplars are
+	// retrieved (including the case where no exemplars exist at all).
 	Exemplar(l *exemplar.Exemplar) bool
 
 	// Next advances the parser to the next sample. It returns false if no
diff --git a/model/textparse/openmetricsparse.go b/model/textparse/openmetricsparse.go
index c17d40020a6..e0833636f1d 100644
--- a/model/textparse/openmetricsparse.go
+++ b/model/textparse/openmetricsparse.go
@@ -174,8 +174,10 @@ func (p *OpenMetricsParser) Metric(l *labels.Labels) string {
 	return s
 }
 
-// Exemplar writes the exemplar of the current sample into the passed
-// exemplar. It returns the whether an exemplar exists.
+// Exemplar writes the exemplar of the current sample into the passed exemplar.
+// It returns whether an exemplar exists. As OpenMetrics only ever has one
+// exemplar per sample, every call after the first (for the same sample) will
+// always return false.
 func (p *OpenMetricsParser) Exemplar(e *exemplar.Exemplar) bool {
 	if len(p.exemplar) == 0 {
 		return false
@@ -204,6 +206,8 @@ func (p *OpenMetricsParser) Exemplar(e *exemplar.Exemplar) bool {
 	p.builder.Sort()
 	e.Labels = p.builder.Labels()
 
+	// Wipe exemplar so that future calls return false.
+	p.exemplar = p.exemplar[:0]
 	return true
 }
 
diff --git a/scrape/scrape.go b/scrape/scrape.go
index ec65c5ad9b9..df729b44896 100644
--- a/scrape/scrape.go
+++ b/scrape/scrape.go
@@ -1685,7 +1685,7 @@ loop:
 		// number of samples remaining after relabeling.
 		added++
 
-		if hasExemplar := p.Exemplar(&e); hasExemplar {
+		for hasExemplar := p.Exemplar(&e); hasExemplar; hasExemplar = p.Exemplar(&e) {
 			if !e.HasTs {
 				e.Ts = t
 			}
diff --git a/tsdb/exemplar.go b/tsdb/exemplar.go
index bf401da3ce9..d4c0505cc1b 100644
--- a/tsdb/exemplar.go
+++ b/tsdb/exemplar.go
@@ -365,8 +365,8 @@ func (ce *CircularExemplarStorage) AddExemplar(l labels.Labels, e exemplar.Exemp
 	if prev := ce.exemplars[ce.nextIndex]; prev == nil {
 		ce.exemplars[ce.nextIndex] = &circularBufferEntry{}
 	} else {
-		// There exists exemplar already on this ce.nextIndex entry, drop it, to make place
-		// for others.
+		// There exists an exemplar already on this ce.nextIndex entry,
+		// drop it, to make place for others.
 		var buf [1024]byte
 		prevLabels := prev.ref.seriesLabels.Bytes(buf[:])
 		if prev.next == noExemplar {


Test patch:
diff --git a/scrape/helpers_test.go b/scrape/helpers_test.go
index 4862886e5e4..c580a50510e 100644
--- a/scrape/helpers_test.go
+++ b/scrape/helpers_test.go
@@ -53,10 +53,10 @@ func (a nopAppender) UpdateMetadata(storage.SeriesRef, labels.Labels, metadata.M
 func (a nopAppender) Commit() error   { return nil }
 func (a nopAppender) Rollback() error { return nil }
 
-type sample struct {
+type floatSample struct {
 	metric labels.Labels
 	t      int64
-	v      float64
+	f      float64
 }
 
 type histogramSample struct {
@@ -69,23 +69,23 @@ type histogramSample struct {
 // It can be used as its zero value or be backed by another appender it writes samples through.
 type collectResultAppender struct {
 	next                 storage.Appender
-	result               []sample
-	pendingResult        []sample
-	rolledbackResult     []sample
-	pendingExemplars     []exemplar.Exemplar
-	resultExemplars      []exemplar.Exemplar
+	resultFloats         []floatSample
+	pendingFloats        []floatSample
+	rolledbackFloats     []floatSample
 	resultHistograms     []histogramSample
 	pendingHistograms    []histogramSample
 	rolledbackHistograms []histogramSample
-	pendingMetadata      []metadata.Metadata
+	resultExemplars      []exemplar.Exemplar
+	pendingExemplars     []exemplar.Exemplar
 	resultMetadata       []metadata.Metadata
+	pendingMetadata      []metadata.Metadata
 }
 
 func (a *collectResultAppender) Append(ref storage.SeriesRef, lset labels.Labels, t int64, v float64) (storage.SeriesRef, error) {
-	a.pendingResult = append(a.pendingResult, sample{
+	a.pendingFloats = append(a.pendingFloats, floatSample{
 		metric: lset,
 		t:      t,
-		v:      v,
+		f:      v,
 	})
 
 	if ref == 0 {
@@ -133,11 +133,11 @@ func (a *collectResultAppender) UpdateMetadata(ref storage.SeriesRef, l labels.L
 }
 
 func (a *collectResultAppender) Commit() error {
-	a.result = append(a.result, a.pendingResult...)
+	a.resultFloats = append(a.resultFloats, a.pendingFloats...)
 	a.resultExemplars = append(a.resultExemplars, a.pendingExemplars...)
 	a.resultHistograms = append(a.resultHistograms, a.pendingHistograms...)
 	a.resultMetadata = append(a.resultMetadata, a.pendingMetadata...)
-	a.pendingResult = nil
+	a.pendingFloats = nil
 	a.pendingExemplars = nil
 	a.pendingHistograms = nil
 	a.pendingMetadata = nil
@@ -148,9 +148,9 @@ func (a *collectResultAppender) Commit() error {
 }
 
 func (a *collectResultAppender) Rollback() error {
-	a.rolledbackResult = a.pendingResult
+	a.rolledbackFloats = a.pendingFloats
 	a.rolledbackHistograms = a.pendingHistograms
-	a.pendingResult = nil
+	a.pendingFloats = nil
 	a.pendingHistograms = nil
 	if a.next == nil {
 		return nil
@@ -160,14 +160,14 @@ func (a *collectResultAppender) Rollback() error {
 
 func (a *collectResultAppender) String() string {
 	var sb strings.Builder
-	for _, s := range a.result {
-		sb.WriteString(fmt.Sprintf("committed: %s %f %d\n", s.metric, s.v, s.t))
+	for _, s := range a.resultFloats {
+		sb.WriteString(fmt.Sprintf("committed: %s %f %d\n", s.metric, s.f, s.t))
 	}
-	for _, s := range a.pendingResult {
-		sb.WriteString(fmt.Sprintf("pending: %s %f %d\n", s.metric, s.v, s.t))
+	for _, s := range a.pendingFloats {
+		sb.WriteString(fmt.Sprintf("pending: %s %f %d\n", s.metric, s.f, s.t))
 	}
-	for _, s := range a.rolledbackResult {
-		sb.WriteString(fmt.Sprintf("rolledback: %s %f %d\n", s.metric, s.v, s.t))
+	for _, s := range a.rolledbackFloats {
+		sb.WriteString(fmt.Sprintf("rolledback: %s %f %d\n", s.metric, s.f, s.t))
 	}
 	return sb.String()
 }
diff --git a/scrape/scrape_test.go b/scrape/scrape_test.go
index 6c45c26b429..3f119b94dd0 100644
--- a/scrape/scrape_test.go
+++ b/scrape/scrape_test.go
@@ -17,6 +17,7 @@ import (
 	"bytes"
 	"compress/gzip"
 	"context"
+	"encoding/binary"
 	"fmt"
 	"io"
 	"math"
@@ -29,6 +30,7 @@ import (
 	"time"
 
 	"github.com/go-kit/log"
+	"github.com/gogo/protobuf/proto"
 	"github.com/pkg/errors"
 	"github.com/prometheus/client_golang/prometheus"
 	dto "github.com/prometheus/client_model/go"
@@ -39,6 +41,7 @@ import (
 	"github.com/prometheus/prometheus/config"
 	"github.com/prometheus/prometheus/discovery/targetgroup"
 	"github.com/prometheus/prometheus/model/exemplar"
+	"github.com/prometheus/prometheus/model/histogram"
 	"github.com/prometheus/prometheus/model/labels"
 	"github.com/prometheus/prometheus/model/relabel"
 	"github.com/prometheus/prometheus/model/textparse"
@@ -737,12 +740,12 @@ func TestScrapeLoopStop(t *testing.T) {
 
 	// We expected 1 actual sample for each scrape plus 5 for report samples.
 	// At least 2 scrapes were made, plus the final stale markers.
-	if len(appender.result) < 6*3 || len(appender.result)%6 != 0 {
-		t.Fatalf("Expected at least 3 scrapes with 6 samples each, got %d samples", len(appender.result))
+	if len(appender.resultFloats) < 6*3 || len(appender.resultFloats)%6 != 0 {
+		t.Fatalf("Expected at least 3 scrapes with 6 samples each, got %d samples", len(appender.resultFloats))
 	}
 	// All samples in a scrape must have the same timestamp.
 	var ts int64
-	for i, s := range appender.result {
+	for i, s := range appender.resultFloats {
 		switch {
 		case i%6 == 0:
 			ts = s.t
@@ -751,9 +754,9 @@ func TestScrapeLoopStop(t *testing.T) {
 		}
 	}
 	// All samples from the last scrape must be stale markers.
-	for _, s := range appender.result[len(appender.result)-5:] {
-		if !value.IsStaleNaN(s.v) {
-			t.Fatalf("Appended last sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(s.v))
+	for _, s := range appender.resultFloats[len(appender.resultFloats)-5:] {
+		if !value.IsStaleNaN(s.f) {
+			t.Fatalf("Appended last sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(s.f))
 		}
 	}
 }
@@ -1189,10 +1192,10 @@ func TestScrapeLoopRunCreatesStaleMarkersOnFailedScrape(t *testing.T) {
 
 	// 1 successfully scraped sample, 1 stale marker after first fail, 5 report samples for
 	// each scrape successful or not.
-	require.Equal(t, 27, len(appender.result), "Appended samples not as expected:\n%s", appender)
-	require.Equal(t, 42.0, appender.result[0].v, "Appended first sample not as expected")
-	require.True(t, value.IsStaleNaN(appender.result[6].v),
-		"Appended second sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(appender.result[6].v))
+	require.Equal(t, 27, len(appender.resultFloats), "Appended samples not as expected:\n%s", appender)
+	require.Equal(t, 42.0, appender.resultFloats[0].f, "Appended first sample not as expected")
+	require.True(t, value.IsStaleNaN(appender.resultFloats[6].f),
+		"Appended second sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(appender.resultFloats[6].f))
 }
 
 func TestScrapeLoopRunCreatesStaleMarkersOnParseFailure(t *testing.T) {
@@ -1254,10 +1257,10 @@ func TestScrapeLoopRunCreatesStaleMarkersOnParseFailure(t *testing.T) {
 
 	// 1 successfully scraped sample, 1 stale marker after first fail, 5 report samples for
 	// each scrape successful or not.
-	require.Equal(t, 17, len(appender.result), "Appended samples not as expected:\n%s", appender)
-	require.Equal(t, 42.0, appender.result[0].v, "Appended first sample not as expected")
-	require.True(t, value.IsStaleNaN(appender.result[6].v),
-		"Appended second sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(appender.result[6].v))
+	require.Equal(t, 17, len(appender.resultFloats), "Appended samples not as expected:\n%s", appender)
+	require.Equal(t, 42.0, appender.resultFloats[0].f, "Appended first sample not as expected")
+	require.True(t, value.IsStaleNaN(appender.resultFloats[6].f),
+		"Appended second sample not as expected. Wanted: stale NaN Got: %x", math.Float64bits(appender.resultFloats[6].f))
 }
 
 func TestScrapeLoopCache(t *testing.T) {
@@ -1339,7 +1342,7 @@ func TestScrapeLoopCache(t *testing.T) {
 
 	// 1 successfully scraped sample, 1 stale marker after first fail, 5 report samples for
 	// each scrape successful or not.
-	require.Equal(t, 26, len(appender.result), "Appended samples not as expected:\n%s", appender)
+	require.Equal(t, 26, len(appender.resultFloats), "Appended samples not as expected:\n%s", appender)
 }
 
 func TestScrapeLoopCacheMemoryExhaustionProtection(t *testing.T) {
@@ -1498,11 +1501,11 @@ func TestScrapeLoopAppend(t *testing.T) {
 		require.NoError(t, err)
 		require.NoError(t, slApp.Commit())
 
-		expected := []sample{
+		expected := []floatSample{
 			{
 				metric: test.expLset,
 				t:      timestamp.FromTime(now),
-				v:      test.expValue,
+				f:      test.expValue,
 			},
 		}
 
@@ -1510,11 +1513,11 @@ func TestScrapeLoopAppend(t *testing.T) {
 		// DeepEqual will report NaNs as being different,
 		// so replace it with the expected one.
 		if test.expValue == float64(value.NormalNaN) {
-			app.result[0].v = expected[0].v
+			app.resultFloats[0].f = expected[0].f
 		}
 
 		t.Logf("Test:%s", test.title)
-		require.Equal(t, expected, app.result)
+		require.Equal(t, expected, app.resultFloats)
 	}
 }
 
@@ -1584,13 +1587,13 @@ func TestScrapeLoopAppendForConflictingPrefixedLabels(t *testing.T) {
 
 			require.NoError(t, slApp.Commit())
 
-			require.Equal(t, []sample{
+			require.Equal(t, []floatSample{
 				{
 					metric: labels.FromStrings(tc.expected...),
 					t:      timestamp.FromTime(time.Date(2000, 1, 1, 1, 0, 0, 0, time.UTC)),
-					v:      0,
+					f:      0,
 				},
-			}, app.result)
+			}, app.resultFloats)
 		})
 	}
 }
@@ -1638,15 +1641,15 @@ func TestScrapeLoopAppendCacheEntryButErrNotFound(t *testing.T) {
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
-	expected := []sample{
+	expected := []floatSample{
 		{
 			metric: lset,
 			t:      timestamp.FromTime(now),
-			v:      expValue,
+			f:      expValue,
 		},
 	}
 
-	require.Equal(t, expected, app.result)
+	require.Equal(t, expected, app.resultFloats)
 }
 
 func TestScrapeLoopAppendSampleLimit(t *testing.T) {
@@ -1706,14 +1709,14 @@ func TestScrapeLoopAppendSampleLimit(t *testing.T) {
 	require.Equal(t, 1.0, change, "Unexpected change of sample limit metric: %f", change)
 
 	// And verify that we got the samples that fit under the limit.
-	want := []sample{
+	want := []floatSample{
 		{
 			metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
 			t:      timestamp.FromTime(now),
-			v:      1,
+			f:      1,
 		},
 	}
-	require.Equal(t, want, resApp.rolledbackResult, "Appended samples not as expected:\n%s", appender)
+	require.Equal(t, want, resApp.rolledbackFloats, "Appended samples not as expected:\n%s", appender)
 
 	now = time.Now()
 	slApp = sl.appender(context.Background())
@@ -1866,19 +1869,19 @@ func TestScrapeLoop_ChangingMetricString(t *testing.T) {
 	require.NoError(t, slApp.Commit())
 
 	// DeepEqual will report NaNs as being different, so replace with a different value.
-	want := []sample{
+	want := []floatSample{
 		{
 			metric: labels.FromStrings("__name__", "metric_a", "a", "1", "b", "1"),
 			t:      timestamp.FromTime(now),
-			v:      1,
+			f:      1,
 		},
 		{
 			metric: labels.FromStrings("__name__", "metric_a", "a", "1", "b", "1"),
 			t:      timestamp.FromTime(now.Add(time.Minute)),
-			v:      2,
+			f:      2,
 		},
 	}
-	require.Equal(t, want, capp.result, "Appended samples not as expected:\n%s", appender)
+	require.Equal(t, want, capp.resultFloats, "Appended samples not as expected:\n%s", appender)
 }
 
 func TestScrapeLoopAppendStaleness(t *testing.T) {
@@ -1914,24 +1917,24 @@ func TestScrapeLoopAppendStaleness(t *testing.T) {
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
-	ingestedNaN := math.Float64bits(app.result[1].v)
+	ingestedNaN := math.Float64bits(app.resultFloats[1].f)
 	require.Equal(t, value.StaleNaN, ingestedNaN, "Appended stale sample wasn't as expected")
 
 	// DeepEqual will report NaNs as being different, so replace with a different value.
-	app.result[1].v = 42
-	want := []sample{
+	app.resultFloats[1].f = 42
+	want := []floatSample{
 		{
 			metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
 			t:      timestamp.FromTime(now),
-			v:      1,
+			f:      1,
 		},
 		{
 			metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
 			t:      timestamp.FromTime(now.Add(time.Second)),
-			v:      42,
+			f:      42,
 		},
 	}
-	require.Equal(t, want, app.result, "Appended samples not as expected:\n%s", appender)
+	require.Equal(t, want, app.resultFloats, "Appended samples not as expected:\n%s", appender)
 }
 
 func TestScrapeLoopAppendNoStalenessIfTimestamp(t *testing.T) {
@@ -1966,40 +1969,44 @@ func TestScrapeLoopAppendNoStalenessIfTimestamp(t *testing.T) {
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
-	want := []sample{
+	want := []floatSample{
 		{
 			metric: labels.FromStrings(model.MetricNameLabel, "metric_a"),
 			t:      1000,
-			v:      1,
+			f:      1,
 		},
 	}
-	require.Equal(t, want, app.result, "Appended samples not as expected:\n%s", appender)
+	require.Equal(t, want, app.resultFloats, "Appended samples not as expected:\n%s", appender)
 }
 
 func TestScrapeLoopAppendExemplar(t *testing.T) {
 	tests := []struct {
 		title           string
 		scrapeText      string
+		contentType     string
 		discoveryLabels []string
-		samples         []sample
+		floats          []floatSample
+		histograms      []histogramSample
 		exemplars       []exemplar.Exemplar
 	}{
 		{
 			title:           "Metric without exemplars",
 			scrapeText:      "metric_total{n=\"1\"} 0\n# EOF",
+			contentType:     "application/openmetrics-text",
 			discoveryLabels: []string{"n", "2"},
-			samples: []sample{{
+			floats: []floatSample{{
 				metric: labels.FromStrings("__name__", "metric_total", "exported_n", "1", "n", "2"),
-				v:      0,
+				f:      0,
 			}},
 		},
 		{
 			title:           "Metric with exemplars",
 			scrapeText:      "metric_total{n=\"1\"} 0 # {a=\"abc\"} 1.0\n# EOF",
+			contentType:     "application/openmetrics-text",
 			discoveryLabels: []string{"n", "2"},
-			samples: []sample{{
+			floats: []floatSample{{
 				metric: labels.FromStrings("__name__", "metric_total", "exported_n", "1", "n", "2"),
-				v:      0,
+				f:      0,
 			}},
 			exemplars: []exemplar.Exemplar{
 				{Labels: labels.FromStrings("a", "abc"), Value: 1},
@@ -2008,10 +2015,11 @@ func TestScrapeLoopAppendExemplar(t *testing.T) {
 		{
 			title:           "Metric with exemplars and TS",
 			scrapeText:      "metric_total{n=\"1\"} 0 # {a=\"abc\"} 1.0 10000\n# EOF",
+			contentType:     "application/openmetrics-text",
 			discoveryLabels: []string{"n", "2"},
-			samples: []sample{{
+			floats: []floatSample{{
 				metric: labels.FromStrings("__name__", "metric_total", "exported_n", "1", "n", "2"),
-				v:      0,
+				f:      0,
 			}},
 			exemplars: []exemplar.Exemplar{
 				{Labels: labels.FromStrings("a", "abc"), Value: 1, Ts: 10000000, HasTs: true},
@@ -2022,18 +2030,117 @@ func TestScrapeLoopAppendExemplar(t *testing.T) {
 			scrapeText: `metric_total{n="1"} 1 # {t="1"} 1.0 10000
 metric_total{n="2"} 2 # {t="2"} 2.0 20000
 # EOF`,
-			samples: []sample{{
+			contentType: "application/openmetrics-text",
+			floats: []floatSample{{
 				metric: labels.FromStrings("__name__", "metric_total", "n", "1"),
-				v:      1,
+				f:      1,
 			}, {
 				metric: labels.FromStrings("__name__", "metric_total", "n", "2"),
-				v:      2,
+				f:      2,
 			}},
 			exemplars: []exemplar.Exemplar{
 				{Labels: labels.FromStrings("t", "1"), Value: 1, Ts: 10000000, HasTs: true},
 				{Labels: labels.FromStrings("t", "2"), Value: 2, Ts: 20000000, HasTs: true},
 			},
 		},
+		{
+			title: "Native histogram with two exemplars",
+			scrapeText: `name: "test_histogram"
+help: "Test histogram with many buckets removed to keep it manageable in size."
+type: HISTOGRAM
+metric: <
+  histogram: <
+    sample_count: 175
+    sample_sum: 0.0008280461746287094
+    bucket: <
+      cumulative_count: 2
+      upper_bound: -0.0004899999999999998
+    >
+    bucket: <
+      cumulative_count: 4
+      upper_bound: -0.0003899999999999998
+      exemplar: <
+        label: <
+          name: "dummyID"
+          value: "59727"
+        >
+        value: -0.00039
+        timestamp: <
+          seconds: 1625851155
+          nanos: 146848499
+        >
+      >
+    >
+    bucket: <
+      cumulative_count: 16
+      upper_bound: -0.0002899999999999998
+      exemplar: <
+        label: <
+          name: "dummyID"
+          value: "5617"
+        >
+        value: -0.00029
+      >
+    >
+    schema: 3
+    zero_threshold: 2.938735877055719e-39
+    zero_count: 2
+    negative_span: <
+      offset: -162
+      length: 1
+    >
+    negative_span: <
+      offset: 23
+      length: 4
+    >
+    negative_delta: 1
+    negative_delta: 3
+    negative_delta: -2
+    negative_delta: -1
+    negative_delta: 1
+    positive_span: <
+      offset: -161
+      length: 1
+    >
+    positive_span: <
+      offset: 8
+      length: 3
+    >
+    positive_delta: 1
+    positive_delta: 2
+    positive_delta: -1
+    positive_delta: -1
+  >
+  timestamp_ms: 1234568
+>
+
+`,
+			contentType: "application/vnd.google.protobuf",
+			histograms: []histogramSample{{
+				t: 1234568,
+				h: &histogram.Histogram{
+					Count:         175,
+					ZeroCount:     2,
+					Sum:           0.0008280461746287094,
+					ZeroThreshold: 2.938735877055719e-39,
+					Schema:        3,
+					PositiveSpans: []histogram.Span{
+						{Offset: -161, Length: 1},
+						{Offset: 8, Length: 3},
+					},
+					NegativeSpans: []histogram.Span{
+						{Offset: -162, Length: 1},
+						{Offset: 23, Length: 4},
+					},
+					PositiveBuckets: []int64{1, 2, -1, -1},
+					NegativeBuckets: []int64{1, 3, -2, -1, 1},
+				},
+			}},
+			exemplars: []exemplar.Exemplar{
+				{Labels: labels.FromStrings("dummyID", "59727"), Value: -0.00039, Ts: 1625851155146, HasTs: true},
+				{Labels: labels.FromStrings("dummyID", "5617"), Value: -0.00029, Ts: 1234568, HasTs: false},
+			},
+		},
 	}
 
 	for _, test := range tests {
@@ -2069,8 +2176,8 @@ metric_total{n="2"} 2 # {t="2"} 2.0 20000
 
 			now := time.Now()
 
-			for i := range test.samples {
-				test.samples[i].t = timestamp.FromTime(now)
+			for i := range test.floats {
+				test.floats[i].t = timestamp.FromTime(now)
 			}
 
 			// We need to set the timestamp for expected exemplars that does not have a timestamp.
@@ -2080,10 +2187,29 @@ metric_total{n="2"} 2 # {t="2"} 2.0 20000
 				}
 			}
 
-			_, _, _, err := sl.append(app, []byte(test.scrapeText), "application/openmetrics-text", now)
+			buf := &bytes.Buffer{}
+			if test.contentType == "application/vnd.google.protobuf" {
+				// In case of protobuf, we have to create the binary representation.
+				pb := &dto.MetricFamily{}
+				// From text to proto message.
+				require.NoError(t, proto.UnmarshalText(test.scrapeText, pb))
+				// From proto message to binary protobuf.
+				protoBuf, err := proto.Marshal(pb)
+				require.NoError(t, err)
+
+				// Write first length, then binary protobuf.
+				varintBuf := binary.AppendUvarint(nil, uint64(len(protoBuf)))
+				buf.Write(varintBuf)
+				buf.Write(protoBuf)
+			} else {
+				buf.WriteString(test.scrapeText)
+			}
+
+			_, _, _, err := sl.append(app, buf.Bytes(), test.contentType, now)
 			require.NoError(t, err)
 			require.NoError(t, app.Commit())
-			require.Equal(t, test.samples, app.result)
+			require.Equal(t, test.floats, app.resultFloats)
+			require.Equal(t, test.histograms, app.resultHistograms)
 			require.Equal(t, test.exemplars, app.resultExemplars)
 		})
 	}
@@ -2093,12 +2219,12 @@ func TestScrapeLoopAppendExemplarSeries(t *testing.T) {
 	scrapeText := []string{`metric_total{n="1"} 1 # {t="1"} 1.0 10000
 # EOF`, `metric_total{n="1"} 2 # {t="2"} 2.0 20000
 # EOF`}
-	samples := []sample{{
+	samples := []floatSample{{
 		metric: labels.FromStrings("__name__", "metric_total", "n", "1"),
-		v:      1,
+		f:      1,
 	}, {
 		metric: labels.FromStrings("__name__", "metric_total", "n", "1"),
-		v:      2,
+		f:      2,
 	}}
 	exemplars := []exemplar.Exemplar{
 		{Labels: labels.FromStrings("t", "1"), Value: 1, Ts: 10000000, HasTs: true},
@@ -2154,7 +2280,7 @@ func TestScrapeLoopAppendExemplarSeries(t *testing.T) {
 		require.NoError(t, app.Commit())
 	}
 
-	require.Equal(t, samples, app.result)
+	require.Equal(t, samples, app.resultFloats)
 	require.Equal(t, exemplars, app.resultExemplars)
 }
 
@@ -2192,7 +2318,7 @@ func TestScrapeLoopRunReportsTargetDownOnScrapeError(t *testing.T) {
 	}
 
 	sl.run(nil)
-	require.Equal(t, 0.0, appender.result[0].v, "bad 'up' value")
+	require.Equal(t, 0.0, appender.resultFloats[0].f, "bad 'up' value")
 }
 
 func TestScrapeLoopRunReportsTargetDownOnInvalidUTF8(t *testing.T) {
@@ -2230,7 +2356,7 @@ func TestScrapeLoopRunReportsTargetDownOnInvalidUTF8(t *testing.T) {
 	}
 
 	sl.run(nil)
-	require.Equal(t, 0.0, appender.result[0].v, "bad 'up' value")
+	require.Equal(t, 0.0, appender.resultFloats[0].f, "bad 'up' value")
 }
 
 type errorAppender struct {
@@ -2279,14 +2405,14 @@ func TestScrapeLoopAppendGracefullyIfAmendOrOutOfOrderOrOutOfBounds(t *testing.T
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
-	want := []sample{
+	want := []floatSample{
 		{
 			metric: labels.FromStrings(model.MetricNameLabel, "normal"),
 			t:      timestamp.FromTime(now),
-			v:      1,
+			f:      1,
 		},
 	}
-	require.Equal(t, want, app.result, "Appended samples not as expected:\n%s", appender)
+	require.Equal(t, want, app.resultFloats, "Appended samples not as expected:\n%s", appender)
 	require.Equal(t, 4, total)
 	require.Equal(t, 4, added)
 	require.Equal(t, 1, seriesAdded)
@@ -2598,14 +2724,14 @@ func TestScrapeLoop_RespectTimestamps(t *testing.T) {
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
-	want := []sample{
+	want := []floatSample{
 		{
 			metric: labels.FromStrings("__name__", "metric_a", "a", "1", "b", "1"),
 			t:      0,
-			v:      1,
+			f:      1,
 		},
 	}
-	require.Equal(t, want, capp.result, "Appended samples not as expected:\n%s", appender)
+	require.Equal(t, want, capp.resultFloats, "Appended samples not as expected:\n%s", appender)
 }
 
 func TestScrapeLoop_DiscardTimestamps(t *testing.T) {
@@ -2640,14 +2766,14 @@ func TestScrapeLoop_DiscardTimestamps(t *testing.T) {
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
-	want := []sample{
+	want := []floatSample{
 		{
 			metric: labels.FromStrings("__name__", "metric_a", "a", "1", "b", "1"),
 			t:      timestamp.FromTime(now),
-			v:      1,
+			f:      1,
 		},
 	}
-	require.Equal(t, want, capp.result, "Appended samples not as expected:\n%s", appender)
+	require.Equal(t, want, capp.resultFloats, "Appended samples not as expected:\n%s", appender)
 }
 
 func TestScrapeLoopDiscardDuplicateLabels(t *testing.T) {