From fd6bdf52307752fb0c70e4c258f66c67e953b091 Mon Sep 17 00:00:00 2001 From: Charles Korn Date: Fri, 28 Jun 2024 11:19:27 +1000 Subject: [PATCH 01/29] Fix issue where summation of +/- infinity returns NaN instead of infinity Signed-off-by: Charles Korn --- promql/functions.go | 8 +- promql/functions_internal_test.go | 81 +++++++++++++++++++ promql/promqltest/testdata/aggregators.test | 29 +++++++ .../testdata/native_histograms.test | 4 +- 4 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 promql/functions_internal_test.go diff --git a/promql/functions.go b/promql/functions.go index 9b3be22874..dec3abc253 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -950,10 +950,14 @@ func funcTimestamp(vals []parser.Value, args parser.Expressions, enh *EvalNodeHe func kahanSumInc(inc, sum, c float64) (newSum, newC float64) { t := sum + inc + switch { + case math.IsInf(t, 0): + c = 0 + // Using Neumaier improvement, swap if next term larger than sum. - if math.Abs(sum) >= math.Abs(inc) { + case math.Abs(sum) >= math.Abs(inc): c += (sum - t) + inc - } else { + default: c += (inc - t) + sum } return t, c diff --git a/promql/functions_internal_test.go b/promql/functions_internal_test.go new file mode 100644 index 0000000000..85c6cd7973 --- /dev/null +++ b/promql/functions_internal_test.go @@ -0,0 +1,81 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package promql + +import ( + "fmt" + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestKahanSumInc(t *testing.T) { + testCases := map[string]struct { + first float64 + second float64 + expected float64 + }{ + "+Inf + anything = +Inf": { + first: math.Inf(1), + second: 2.0, + expected: math.Inf(1), + }, + "-Inf + anything = -Inf": { + first: math.Inf(-1), + second: 2.0, + expected: math.Inf(-1), + }, + "+Inf + -Inf = NaN": { + first: math.Inf(1), + second: math.Inf(-1), + expected: math.NaN(), + }, + "NaN + anything = NaN": { + first: math.NaN(), + second: 2, + expected: math.NaN(), + }, + "NaN + Inf = NaN": { + first: math.NaN(), + second: math.Inf(1), + expected: math.NaN(), + }, + "NaN + -Inf = NaN": { + first: math.NaN(), + second: math.Inf(-1), + expected: math.NaN(), + }, + } + + runTest := func(t *testing.T, a, b, expected float64) { + t.Run(fmt.Sprintf("%v + %v = %v", a, b, expected), func(t *testing.T) { + sum, c := kahanSumInc(b, a, 0) + result := sum + c + + if math.IsNaN(expected) { + require.Truef(t, math.IsNaN(result), "expected result to be NaN, but got %v (from %v + %v)", result, sum, c) + } else { + require.Equalf(t, expected, result, "expected result to be %v, but got %v (from %v + %v)", expected, result, sum, c) + } + }) + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + runTest(t, testCase.first, testCase.second, testCase.expected) + runTest(t, testCase.second, testCase.first, testCase.expected) + }) + } +} diff --git a/promql/promqltest/testdata/aggregators.test b/promql/promqltest/testdata/aggregators.test index be689c65f6..2a0ce01ee9 100644 --- a/promql/promqltest/testdata/aggregators.test +++ b/promql/promqltest/testdata/aggregators.test @@ -511,10 +511,39 @@ load 10s data{test="ten",point="b"} 8 data{test="ten",point="c"} 1e+100 data{test="ten",point="d"} -1e100 + data{test="pos_inf",group="1",point="a"} Inf + data{test="pos_inf",group="1",point="b"} 2 + data{test="pos_inf",group="2",point="a"} 2 + data{test="pos_inf",group="2",point="b"} Inf + data{test="neg_inf",group="1",point="a"} -Inf + data{test="neg_inf",group="1",point="b"} 2 + data{test="neg_inf",group="2",point="a"} 2 + data{test="neg_inf",group="2",point="b"} -Inf + data{test="inf_inf",point="a"} Inf + data{test="inf_inf",point="b"} -Inf + data{test="nan",group="1",point="a"} NaN + data{test="nan",group="1",point="b"} 2 + data{test="nan",group="2",point="a"} 2 + data{test="nan",group="2",point="b"} NaN eval instant at 1m sum(data{test="ten"}) {} 10 +eval instant at 1m sum by (group) (data{test="pos_inf"}) + {group="1"} Inf + {group="2"} Inf + +eval instant at 1m sum by (group) (data{test="neg_inf"}) + {group="1"} -Inf + {group="2"} -Inf + +eval instant at 1m sum(data{test="inf_inf"}) + {} NaN + +eval instant at 1m sum by (group) (data{test="nan"}) + {group="1"} NaN + {group="2"} NaN + clear # Test that aggregations are deterministic. diff --git a/promql/promqltest/testdata/native_histograms.test b/promql/promqltest/testdata/native_histograms.test index f79517023c..01c2a21574 100644 --- a/promql/promqltest/testdata/native_histograms.test +++ b/promql/promqltest/testdata/native_histograms.test @@ -355,10 +355,10 @@ load 10m histogram_stddev_stdvar_7 {{schema:3 count:7 sum:Inf z_bucket:1 buckets:[0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 ] n_buckets:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 ]}}x1 eval instant at 10m histogram_stddev(histogram_stddev_stdvar_7) - {} NaN + {} Inf eval instant at 10m histogram_stdvar(histogram_stddev_stdvar_7) - {} NaN + {} Inf # Apply quantile function to histogram with all positive buckets with zero bucket. load 10m From ec31acaf0241bb061ae2621a0b8ed4131bf1146f Mon Sep 17 00:00:00 2001 From: Marco Pracucci Date: Mon, 1 Jul 2024 10:12:50 +0200 Subject: [PATCH 02/29] FastRegexMatcher: small optimization for the literal prefix case Signed-off-by: Marco Pracucci --- model/labels/regexp.go | 48 +++++++++++++++++++++++++++---------- model/labels/regexp_test.go | 48 +++++++++++++++++++++---------------- 2 files changed, 63 insertions(+), 33 deletions(-) diff --git a/model/labels/regexp.go b/model/labels/regexp.go index 1e9db882bf..767bd6942f 100644 --- a/model/labels/regexp.go +++ b/model/labels/regexp.go @@ -549,11 +549,7 @@ func stringMatcherFromRegexpInternal(re *syntax.Regexp) StringMatcher { // Right matcher with 1 fixed set match. case left == nil && len(matches) == 1: - return &literalPrefixStringMatcher{ - prefix: matches[0], - prefixCaseSensitive: matchesCaseSensitive, - right: right, - } + return newLiteralPrefixStringMatcher(matches[0], matchesCaseSensitive, right) // Left matcher with 1 fixed set match. case right == nil && len(matches) == 1: @@ -631,21 +627,47 @@ func (m *containsStringMatcher) Matches(s string) bool { return false } -// literalPrefixStringMatcher matches a string with the given literal prefix and right side matcher. -type literalPrefixStringMatcher struct { - prefix string - prefixCaseSensitive bool +func newLiteralPrefixStringMatcher(prefix string, prefixCaseSensitive bool, right StringMatcher) StringMatcher { + if prefixCaseSensitive { + return &literalPrefixSensitiveStringMatcher{ + prefix: prefix, + right: right, + } + } + + return &literalPrefixInsensitiveStringMatcher{ + prefix: prefix, + right: right, + } +} + +// literalPrefixSensitiveStringMatcher matches a string with the given literal case-sensitive prefix and right side matcher. +type literalPrefixSensitiveStringMatcher struct { + prefix string // The matcher that must match the right side. Can be nil. right StringMatcher } -func (m *literalPrefixStringMatcher) Matches(s string) bool { - // Ensure the prefix matches. - if m.prefixCaseSensitive && !strings.HasPrefix(s, m.prefix) { +func (m *literalPrefixSensitiveStringMatcher) Matches(s string) bool { + if !strings.HasPrefix(s, m.prefix) { return false } - if !m.prefixCaseSensitive && !hasPrefixCaseInsensitive(s, m.prefix) { + + // Ensure the right side matches. + return m.right.Matches(s[len(m.prefix):]) +} + +// literalPrefixInsensitiveStringMatcher matches a string with the given literal case-insensitive prefix and right side matcher. +type literalPrefixInsensitiveStringMatcher struct { + prefix string + + // The matcher that must match the right side. Can be nil. + right StringMatcher +} + +func (m *literalPrefixInsensitiveStringMatcher) Matches(s string) bool { + if !hasPrefixCaseInsensitive(s, m.prefix) { return false } diff --git a/model/labels/regexp_test.go b/model/labels/regexp_test.go index 008eae702c..fa5c96f420 100644 --- a/model/labels/regexp_test.go +++ b/model/labels/regexp_test.go @@ -376,7 +376,7 @@ func TestStringMatcherFromRegexp(t *testing.T) { {"10\\.0\\.(1|2)\\.+", nil}, {"10\\.0\\.(1|2).+", &containsStringMatcher{substrings: []string{"10.0.1", "10.0.2"}, left: nil, right: &anyNonEmptyStringMatcher{matchNL: false}}}, {"^.+foo", &literalSuffixStringMatcher{left: &anyNonEmptyStringMatcher{}, suffix: "foo", suffixCaseSensitive: true}}, - {"foo-.*$", &literalPrefixStringMatcher{prefix: "foo-", prefixCaseSensitive: true, right: anyStringWithoutNewlineMatcher{}}}, + {"foo-.*$", &literalPrefixSensitiveStringMatcher{prefix: "foo-", right: anyStringWithoutNewlineMatcher{}}}, {"(prometheus|api_prom)_api_v1_.+", &containsStringMatcher{substrings: []string{"prometheus_api_v1_", "api_prom_api_v1_"}, left: nil, right: &anyNonEmptyStringMatcher{matchNL: false}}}, {"^((.*)(bar|b|buzz)(.+)|foo)$", orStringMatcher([]StringMatcher{&containsStringMatcher{substrings: []string{"bar", "b", "buzz"}, left: anyStringWithoutNewlineMatcher{}, right: &anyNonEmptyStringMatcher{matchNL: false}}, &equalStringMatcher{s: "foo", caseSensitive: true}})}, {"((fo(bar))|.+foo)", orStringMatcher([]StringMatcher{orStringMatcher([]StringMatcher{&equalStringMatcher{s: "fobar", caseSensitive: true}}), &literalSuffixStringMatcher{suffix: "foo", suffixCaseSensitive: true, left: &anyNonEmptyStringMatcher{matchNL: false}}})}, @@ -391,15 +391,15 @@ func TestStringMatcherFromRegexp(t *testing.T) { {".*foo.*bar.*", nil}, {`\d*`, nil}, {".", nil}, - {"/|/bar.*", &literalPrefixStringMatcher{prefix: "/", prefixCaseSensitive: true, right: orStringMatcher{emptyStringMatcher{}, &literalPrefixStringMatcher{prefix: "bar", prefixCaseSensitive: true, right: anyStringWithoutNewlineMatcher{}}}}}, + {"/|/bar.*", &literalPrefixSensitiveStringMatcher{prefix: "/", right: orStringMatcher{emptyStringMatcher{}, &literalPrefixSensitiveStringMatcher{prefix: "bar", right: anyStringWithoutNewlineMatcher{}}}}}, // This one is not supported because `stringMatcherFromRegexp` is not reentrant for syntax.OpConcat. // It would make the code too complex to handle it. {"(.+)/(foo.*|bar$)", nil}, // Case sensitive alternate with same literal prefix and .* suffix. - {"(xyz-016a-ixb-dp.*|xyz-016a-ixb-op.*)", &literalPrefixStringMatcher{prefix: "xyz-016a-ixb-", prefixCaseSensitive: true, right: orStringMatcher{&literalPrefixStringMatcher{prefix: "dp", prefixCaseSensitive: true, right: anyStringWithoutNewlineMatcher{}}, &literalPrefixStringMatcher{prefix: "op", prefixCaseSensitive: true, right: anyStringWithoutNewlineMatcher{}}}}}, + {"(xyz-016a-ixb-dp.*|xyz-016a-ixb-op.*)", &literalPrefixSensitiveStringMatcher{prefix: "xyz-016a-ixb-", right: orStringMatcher{&literalPrefixSensitiveStringMatcher{prefix: "dp", right: anyStringWithoutNewlineMatcher{}}, &literalPrefixSensitiveStringMatcher{prefix: "op", right: anyStringWithoutNewlineMatcher{}}}}}, // Case insensitive alternate with same literal prefix and .* suffix. - {"(?i:(xyz-016a-ixb-dp.*|xyz-016a-ixb-op.*))", &literalPrefixStringMatcher{prefix: "XYZ-016A-IXB-", prefixCaseSensitive: false, right: orStringMatcher{&literalPrefixStringMatcher{prefix: "DP", prefixCaseSensitive: false, right: anyStringWithoutNewlineMatcher{}}, &literalPrefixStringMatcher{prefix: "OP", prefixCaseSensitive: false, right: anyStringWithoutNewlineMatcher{}}}}}, - {"(?i)(xyz-016a-ixb-dp.*|xyz-016a-ixb-op.*)", &literalPrefixStringMatcher{prefix: "XYZ-016A-IXB-", prefixCaseSensitive: false, right: orStringMatcher{&literalPrefixStringMatcher{prefix: "DP", prefixCaseSensitive: false, right: anyStringWithoutNewlineMatcher{}}, &literalPrefixStringMatcher{prefix: "OP", prefixCaseSensitive: false, right: anyStringWithoutNewlineMatcher{}}}}}, + {"(?i:(xyz-016a-ixb-dp.*|xyz-016a-ixb-op.*))", &literalPrefixInsensitiveStringMatcher{prefix: "XYZ-016A-IXB-", right: orStringMatcher{&literalPrefixInsensitiveStringMatcher{prefix: "DP", right: anyStringWithoutNewlineMatcher{}}, &literalPrefixInsensitiveStringMatcher{prefix: "OP", right: anyStringWithoutNewlineMatcher{}}}}}, + {"(?i)(xyz-016a-ixb-dp.*|xyz-016a-ixb-op.*)", &literalPrefixInsensitiveStringMatcher{prefix: "XYZ-016A-IXB-", right: orStringMatcher{&literalPrefixInsensitiveStringMatcher{prefix: "DP", right: anyStringWithoutNewlineMatcher{}}, &literalPrefixInsensitiveStringMatcher{prefix: "OP", right: anyStringWithoutNewlineMatcher{}}}}}, // Concatenated variable length selectors are not supported. {"foo.*.*", nil}, {"foo.+.+", nil}, @@ -408,9 +408,9 @@ func TestStringMatcherFromRegexp(t *testing.T) { {"aaa.?.?", nil}, {"aaa.?.*", nil}, // Regexps with ".?". - {"ext.?|xfs", orStringMatcher{&literalPrefixStringMatcher{prefix: "ext", prefixCaseSensitive: true, right: &zeroOrOneCharacterStringMatcher{matchNL: false}}, &equalStringMatcher{s: "xfs", caseSensitive: true}}}, - {"(?s)(ext.?|xfs)", orStringMatcher{&literalPrefixStringMatcher{prefix: "ext", prefixCaseSensitive: true, right: &zeroOrOneCharacterStringMatcher{matchNL: true}}, &equalStringMatcher{s: "xfs", caseSensitive: true}}}, - {"foo.?", &literalPrefixStringMatcher{prefix: "foo", prefixCaseSensitive: true, right: &zeroOrOneCharacterStringMatcher{matchNL: false}}}, + {"ext.?|xfs", orStringMatcher{&literalPrefixSensitiveStringMatcher{prefix: "ext", right: &zeroOrOneCharacterStringMatcher{matchNL: false}}, &equalStringMatcher{s: "xfs", caseSensitive: true}}}, + {"(?s)(ext.?|xfs)", orStringMatcher{&literalPrefixSensitiveStringMatcher{prefix: "ext", right: &zeroOrOneCharacterStringMatcher{matchNL: true}}, &equalStringMatcher{s: "xfs", caseSensitive: true}}}, + {"foo.?", &literalPrefixSensitiveStringMatcher{prefix: "foo", right: &zeroOrOneCharacterStringMatcher{matchNL: false}}}, {"f.?o", nil}, } { c := c @@ -480,10 +480,13 @@ func TestStringMatcherFromRegexp_LiteralPrefix(t *testing.T) { re := regexp.MustCompile("^" + c.pattern + "$") - // Pre-condition check: ensure it contains literalPrefixStringMatcher. + // Pre-condition check: ensure it contains literalPrefixSensitiveStringMatcher or literalPrefixInsensitiveStringMatcher. numPrefixMatchers := 0 visitStringMatcher(matcher, func(matcher StringMatcher) { - if _, ok := matcher.(*literalPrefixStringMatcher); ok { + if _, ok := matcher.(*literalPrefixSensitiveStringMatcher); ok { + numPrefixMatchers++ + } + if _, ok := matcher.(*literalPrefixInsensitiveStringMatcher); ok { numPrefixMatchers++ } }) @@ -1074,20 +1077,14 @@ func BenchmarkZeroOrOneCharacterStringMatcher(b *testing.B) { } } -func TestLiteralPrefixStringMatcher(t *testing.T) { - m := &literalPrefixStringMatcher{prefix: "mar", prefixCaseSensitive: true, right: &emptyStringMatcher{}} +func TestLiteralPrefixSensitiveStringMatcher(t *testing.T) { + m := &literalPrefixSensitiveStringMatcher{prefix: "mar", right: &emptyStringMatcher{}} require.True(t, m.Matches("mar")) require.False(t, m.Matches("marco")) require.False(t, m.Matches("ma")) require.False(t, m.Matches("mAr")) - m = &literalPrefixStringMatcher{prefix: "mar", prefixCaseSensitive: false, right: &emptyStringMatcher{}} - require.True(t, m.Matches("mar")) - require.False(t, m.Matches("marco")) - require.False(t, m.Matches("ma")) - require.True(t, m.Matches("mAr")) - - m = &literalPrefixStringMatcher{prefix: "mar", prefixCaseSensitive: true, right: &equalStringMatcher{s: "co", caseSensitive: false}} + m = &literalPrefixSensitiveStringMatcher{prefix: "mar", right: &equalStringMatcher{s: "co", caseSensitive: false}} require.True(t, m.Matches("marco")) require.True(t, m.Matches("marCO")) require.False(t, m.Matches("MARco")) @@ -1095,6 +1092,14 @@ func TestLiteralPrefixStringMatcher(t *testing.T) { require.False(t, m.Matches("marcopracucci")) } +func TestLiteralPrefixInsensitiveStringMatcher(t *testing.T) { + m := &literalPrefixInsensitiveStringMatcher{prefix: "mar", right: &emptyStringMatcher{}} + require.True(t, m.Matches("mar")) + require.False(t, m.Matches("marco")) + require.False(t, m.Matches("ma")) + require.True(t, m.Matches("mAr")) +} + func TestLiteralSuffixStringMatcher(t *testing.T) { m := &literalSuffixStringMatcher{left: &emptyStringMatcher{}, suffix: "co", suffixCaseSensitive: true} require.True(t, m.Matches("co")) @@ -1184,7 +1189,10 @@ func visitStringMatcher(matcher StringMatcher, callback func(matcher StringMatch visitStringMatcher(casted.right, callback) } - case *literalPrefixStringMatcher: + case *literalPrefixSensitiveStringMatcher: + visitStringMatcher(casted.right, callback) + + case *literalPrefixInsensitiveStringMatcher: visitStringMatcher(casted.right, callback) case *literalSuffixStringMatcher: From 24b78bef32e83ba0d882fb3ada3819e83d6b387d Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Mon, 1 Jul 2024 11:02:09 +0200 Subject: [PATCH 03/29] otlp: Clean up exponential histogram code slightly Signed-off-by: Arve Knudsen --- .../prometheusremotewrite/helper.go | 7 ++++--- .../prometheusremotewrite/histograms.go | 17 +++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/storage/remote/otlptranslator/prometheusremotewrite/helper.go b/storage/remote/otlptranslator/prometheusremotewrite/helper.go index 68be82e443..acd400320e 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/helper.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/helper.go @@ -182,12 +182,13 @@ func createAttributes(resource pcommon.Resource, attributes pcommon.Map, externa if i+1 >= len(extras) { break } - _, found := l[extras[i]] + + name := extras[i] + _, found := l[name] if found && logOnOverwrite { - log.Println("label " + extras[i] + " is overwritten. Check if Prometheus reserved labels are used.") + log.Println("label " + name + " is overwritten. Check if Prometheus reserved labels are used.") } // internal labels should be maintained - name := extras[i] if !(len(name) > 4 && name[:2] == "__" && name[len(name)-2:] == "__") { name = prometheustranslator.NormalizeLabel(name) } diff --git a/storage/remote/otlptranslator/prometheusremotewrite/histograms.go b/storage/remote/otlptranslator/prometheusremotewrite/histograms.go index 31d343fe4d..e26ce6a575 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/histograms.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/histograms.go @@ -31,9 +31,15 @@ import ( const defaultZeroThreshold = 1e-128 func (c *PrometheusConverter) addExponentialHistogramDataPoints(dataPoints pmetric.ExponentialHistogramDataPointSlice, - resource pcommon.Resource, settings Settings, baseName string) error { + resource pcommon.Resource, settings Settings, promName string) error { for x := 0; x < dataPoints.Len(); x++ { pt := dataPoints.At(x) + + histogram, err := exponentialToNativeHistogram(pt) + if err != nil { + return err + } + lbls := createAttributes( resource, pt.Attributes(), @@ -41,14 +47,9 @@ func (c *PrometheusConverter) addExponentialHistogramDataPoints(dataPoints pmetr nil, true, model.MetricNameLabel, - baseName, + promName, ) ts, _ := c.getOrCreateTimeSeries(lbls) - - histogram, err := exponentialToNativeHistogram(pt) - if err != nil { - return err - } ts.Histograms = append(ts.Histograms, histogram) exemplars := getPromExemplars[pmetric.ExponentialHistogramDataPoint](pt) @@ -58,7 +59,7 @@ func (c *PrometheusConverter) addExponentialHistogramDataPoints(dataPoints pmetr return nil } -// exponentialToNativeHistogram translates OTel Exponential Histogram data point +// exponentialToNativeHistogram translates OTel Exponential Histogram data point // to Prometheus Native Histogram. func exponentialToNativeHistogram(p pmetric.ExponentialHistogramDataPoint) (prompb.Histogram, error) { scale := p.Scale() From 9595b174e5eebb4e24fc95044abfd9a946fd1ed1 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Mon, 1 Jul 2024 13:35:40 +0200 Subject: [PATCH 04/29] otlp: Document regular and exponential histogram conversions Signed-off-by: Arve Knudsen --- .../remote/otlptranslator/prometheusremotewrite/helper.go | 7 +++++++ .../otlptranslator/prometheusremotewrite/histograms.go | 2 ++ 2 files changed, 9 insertions(+) diff --git a/storage/remote/otlptranslator/prometheusremotewrite/helper.go b/storage/remote/otlptranslator/prometheusremotewrite/helper.go index acd400320e..2571338532 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/helper.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/helper.go @@ -220,6 +220,13 @@ func isValidAggregationTemporality(metric pmetric.Metric) bool { return false } +// addHistogramDataPoints adds OTel histogram data points to the corresponding Prometheus time series +// as classical histogram samples. +// +// Note that we can't convert to native histograms, since these have exponential buckets and don't line up +// with the user defined bucket boundaries of non-exponential OTel histograms. +// However, work is under way to resolve this shortcoming through a feature called native histograms custom buckets: +// https://github.com/prometheus/prometheus/issues/13485. func (c *PrometheusConverter) addHistogramDataPoints(dataPoints pmetric.HistogramDataPointSlice, resource pcommon.Resource, settings Settings, baseName string) { for x := 0; x < dataPoints.Len(); x++ { diff --git a/storage/remote/otlptranslator/prometheusremotewrite/histograms.go b/storage/remote/otlptranslator/prometheusremotewrite/histograms.go index e26ce6a575..21b3f5dd9f 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/histograms.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/histograms.go @@ -30,6 +30,8 @@ import ( const defaultZeroThreshold = 1e-128 +// addExponentialHistogramDataPoints adds OTel exponential histogram data points to the corresponding time series +// as native histogram samples. func (c *PrometheusConverter) addExponentialHistogramDataPoints(dataPoints pmetric.ExponentialHistogramDataPointSlice, resource pcommon.Resource, settings Settings, promName string) error { for x := 0; x < dataPoints.Len(); x++ { From c53fdade42c129c6999bcb5659f28fe04d9c7333 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 23:29:21 +0000 Subject: [PATCH 05/29] build(deps): bump github.com/prometheus/common Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.54.0 to 0.55.0. - [Release notes](https://github.com/prometheus/common/releases) - [Changelog](https://github.com/prometheus/common/blob/main/RELEASE.md) - [Commits](https://github.com/prometheus/common/compare/v0.54.0...v0.55.0) --- updated-dependencies: - dependency-name: github.com/prometheus/common dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- documentation/examples/remote_storage/go.mod | 17 +++++---- documentation/examples/remote_storage/go.sum | 40 ++++++++++---------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/documentation/examples/remote_storage/go.mod b/documentation/examples/remote_storage/go.mod index 5278cae096..d4f19749dc 100644 --- a/documentation/examples/remote_storage/go.mod +++ b/documentation/examples/remote_storage/go.mod @@ -9,7 +9,7 @@ require ( github.com/golang/snappy v0.0.4 github.com/influxdata/influxdb v1.11.5 github.com/prometheus/client_golang v1.19.1 - github.com/prometheus/common v0.54.0 + github.com/prometheus/common v0.55.0 github.com/prometheus/prometheus v0.52.1 github.com/stretchr/testify v1.9.0 ) @@ -41,12 +41,13 @@ require ( github.com/kylelemons/godebug v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opentelemetry.io/collector/featuregate v1.5.0 // indirect go.opentelemetry.io/collector/pdata v1.5.0 // indirect @@ -57,15 +58,15 @@ require ( go.opentelemetry.io/otel/trace v1.25.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/oauth2 v0.19.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.34.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apimachinery v0.29.3 // indirect diff --git a/documentation/examples/remote_storage/go.sum b/documentation/examples/remote_storage/go.sum index 9717fceaed..ec04347111 100644 --- a/documentation/examples/remote_storage/go.sum +++ b/documentation/examples/remote_storage/go.sum @@ -269,16 +269,16 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= -github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/prometheus v0.52.1 h1:BrQ29YG+mzdGh8DgHPirHbeMGNqtL+INe0rqg7ttBJ4= github.com/prometheus/prometheus v0.52.1/go.mod h1:3z74cVsmVH0iXOR5QBjB7Pa6A0KJeEAK5A6UsmAFb1g= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -330,8 +330,8 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -351,12 +351,12 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -380,25 +380,25 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -419,8 +419,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= -google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 726ed124e4468d0274ba89b0934a6cc8c975532d Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Tue, 2 Jul 2024 15:51:05 +0200 Subject: [PATCH 06/29] Replace `ListPostings.Seek`'s binary search call by the generic `slices.BinarySearch` (#14393) Signed-off-by: Oleg Zaytsev --- tsdb/index/postings.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tsdb/index/postings.go b/tsdb/index/postings.go index d9b5b69de0..bfe74c323d 100644 --- a/tsdb/index/postings.go +++ b/tsdb/index/postings.go @@ -755,9 +755,7 @@ func (it *ListPostings) Seek(x storage.SeriesRef) bool { } // Do binary search between current position and end. - i := sort.Search(len(it.list), func(i int) bool { - return it.list[i] >= x - }) + i, _ := slices.BinarySearch(it.list, x) if i < len(it.list) { it.cur = it.list[i] it.list = it.list[i+1:] From 134e8dc7af6d810b8fa1216169fe115061a53860 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Wed, 3 Jul 2024 15:08:07 +0100 Subject: [PATCH 07/29] TSDB: Simplify OOO Select by copying the head chunk (#14396) Instead of carrying around extra fields in `Meta` structs which let us approximate what was in the chunk at the time, take a copy of the chunk. This simplifies lots of code, and lets us correct a couple of tests which were embedding the wrong answer. We can also remove boundedIterator, which was only used to constrain the OOO head chunk. Signed-off-by: Bryan Boreham --- tsdb/chunks/chunks.go | 9 --- tsdb/head_read.go | 142 +++--------------------------------- tsdb/head_read_test.go | 145 ------------------------------------- tsdb/ooo_head_read.go | 52 +++++-------- tsdb/ooo_head_read_test.go | 27 ++----- 5 files changed, 35 insertions(+), 340 deletions(-) diff --git a/tsdb/chunks/chunks.go b/tsdb/chunks/chunks.go index e7df0eeed2..ec0f6d4036 100644 --- a/tsdb/chunks/chunks.go +++ b/tsdb/chunks/chunks.go @@ -133,15 +133,6 @@ type Meta struct { // Time range the data covers. // When MaxTime == math.MaxInt64 the chunk is still open and being appended to. MinTime, MaxTime int64 - - // OOOLastRef, OOOLastMinTime and OOOLastMaxTime are kept as markers for - // overlapping chunks. - // These fields point to the last created out of order Chunk (the head) that existed - // when Series() was called and was overlapping. - // Series() and Chunk() method responses should be consistent for the same - // query even if new data is added in between the calls. - OOOLastRef ChunkRef - OOOLastMinTime, OOOLastMaxTime int64 } // ChunkFromSamples requires all samples to have the same type. diff --git a/tsdb/head_read.go b/tsdb/head_read.go index 689972f1b7..b47e24f9e4 100644 --- a/tsdb/head_read.go +++ b/tsdb/head_read.go @@ -487,55 +487,24 @@ func (s *memSeries) oooMergedChunks(meta chunks.Meta, cdm *chunks.ChunkDiskMappe // We create a temporary slice of chunk metas to hold the information of all // possible chunks that may overlap with the requested chunk. - tmpChks := make([]chunkMetaAndChunkDiskMapperRef, 0, len(s.ooo.oooMmappedChunks)) - - oooHeadRef := chunks.ChunkRef(chunks.NewHeadChunkRef(s.ref, s.oooHeadChunkID(len(s.ooo.oooMmappedChunks)))) - if s.ooo.oooHeadChunk != nil && s.ooo.oooHeadChunk.OverlapsClosedInterval(mint, maxt) { - // We only want to append the head chunk if this chunk existed when - // Series() was called. This brings consistency in case new data - // is added in between Series() and Chunk() calls. - if oooHeadRef == meta.OOOLastRef { - tmpChks = append(tmpChks, chunkMetaAndChunkDiskMapperRef{ - meta: chunks.Meta{ - // Ignoring samples added before and after the last known min and max time for this chunk. - MinTime: meta.OOOLastMinTime, - MaxTime: meta.OOOLastMaxTime, - Ref: oooHeadRef, - }, - }) - } - } + tmpChks := make([]chunkMetaAndChunkDiskMapperRef, 0, len(s.ooo.oooMmappedChunks)+1) for i, c := range s.ooo.oooMmappedChunks { - chunkRef := chunks.ChunkRef(chunks.NewHeadChunkRef(s.ref, s.oooHeadChunkID(i))) - // We can skip chunks that came in later than the last known OOOLastRef. - if chunkRef > meta.OOOLastRef { - break - } - - switch { - case chunkRef == meta.OOOLastRef: - tmpChks = append(tmpChks, chunkMetaAndChunkDiskMapperRef{ - meta: chunks.Meta{ - MinTime: meta.OOOLastMinTime, - MaxTime: meta.OOOLastMaxTime, - Ref: chunkRef, - }, - ref: c.ref, - origMinT: c.minTime, - origMaxT: c.maxTime, - }) - case c.OverlapsClosedInterval(mint, maxt): + if c.OverlapsClosedInterval(mint, maxt) { tmpChks = append(tmpChks, chunkMetaAndChunkDiskMapperRef{ meta: chunks.Meta{ MinTime: c.minTime, MaxTime: c.maxTime, - Ref: chunkRef, + Ref: chunks.ChunkRef(chunks.NewHeadChunkRef(s.ref, s.oooHeadChunkID(i))), }, ref: c.ref, }) } } + // Add in data copied from the head OOO chunk. + if meta.Chunk != nil { + tmpChks = append(tmpChks, chunkMetaAndChunkDiskMapperRef{meta: meta}) + } // Next we want to sort all the collected chunks by min time so we can find // those that overlap and stop when we know the rest don't. @@ -548,22 +517,8 @@ func (s *memSeries) oooMergedChunks(meta chunks.Meta, cdm *chunks.ChunkDiskMappe continue } var iterable chunkenc.Iterable - if c.meta.Ref == oooHeadRef { - var xor *chunkenc.XORChunk - var err error - // If head chunk min and max time match the meta OOO markers - // that means that the chunk has not expanded so we can append - // it as it is. - if s.ooo.oooHeadChunk.minTime == meta.OOOLastMinTime && s.ooo.oooHeadChunk.maxTime == meta.OOOLastMaxTime { - xor, err = s.ooo.oooHeadChunk.chunk.ToXOR() // TODO(jesus.vazquez) (This is an optimization idea that has no priority and might not be that useful) See if we could use a copy of the underlying slice. That would leave the more expensive ToXOR() function only for the usecase where Bytes() is called. - } else { - // We need to remove samples that are outside of the markers - xor, err = s.ooo.oooHeadChunk.chunk.ToXORBetweenTimestamps(meta.OOOLastMinTime, meta.OOOLastMaxTime) - } - if err != nil { - return nil, fmt.Errorf("failed to convert ooo head chunk to xor chunk: %w", err) - } - iterable = xor + if c.meta.Chunk != nil { + iterable = c.meta.Chunk } else { chk, err := cdm.Chunk(c.ref) if err != nil { @@ -573,16 +528,7 @@ func (s *memSeries) oooMergedChunks(meta chunks.Meta, cdm *chunks.ChunkDiskMappe } return nil, err } - if c.meta.Ref == meta.OOOLastRef && - (c.origMinT != meta.OOOLastMinTime || c.origMaxT != meta.OOOLastMaxTime) { - // The head expanded and was memory mapped so now we need to - // wrap the chunk within a chunk that doesnt allows us to iterate - // through samples out of the OOOLastMinT and OOOLastMaxT - // markers. - iterable = boundedIterable{chk, meta.OOOLastMinTime, meta.OOOLastMaxTime} - } else { - iterable = chk - } + iterable = chk } mc.chunkIterables = append(mc.chunkIterables, iterable) if c.meta.MaxTime > absoluteMax { @@ -593,74 +539,6 @@ func (s *memSeries) oooMergedChunks(meta chunks.Meta, cdm *chunks.ChunkDiskMappe return mc, nil } -var _ chunkenc.Iterable = &boundedIterable{} - -// boundedIterable is an implementation of chunkenc.Iterable that uses a -// boundedIterator that only iterates through samples which timestamps are -// >= minT and <= maxT. -type boundedIterable struct { - chunk chunkenc.Chunk - minT int64 - maxT int64 -} - -func (b boundedIterable) Iterator(iterator chunkenc.Iterator) chunkenc.Iterator { - it := b.chunk.Iterator(iterator) - if it == nil { - panic("iterator shouldn't be nil") - } - return boundedIterator{it, b.minT, b.maxT} -} - -var _ chunkenc.Iterator = &boundedIterator{} - -// boundedIterator is an implementation of Iterator that only iterates through -// samples which timestamps are >= minT and <= maxT. -type boundedIterator struct { - chunkenc.Iterator - minT int64 - maxT int64 -} - -// Next the first time its called it will advance as many positions as necessary -// until its able to find a sample within the bounds minT and maxT. -// If there are samples within bounds it will advance one by one amongst them. -// If there are no samples within bounds it will return false. -func (b boundedIterator) Next() chunkenc.ValueType { - for b.Iterator.Next() == chunkenc.ValFloat { - t, _ := b.Iterator.At() - switch { - case t < b.minT: - continue - case t > b.maxT: - return chunkenc.ValNone - default: - return chunkenc.ValFloat - } - } - return chunkenc.ValNone -} - -func (b boundedIterator) Seek(t int64) chunkenc.ValueType { - if t < b.minT { - // We must seek at least up to b.minT if it is asked for something before that. - val := b.Iterator.Seek(b.minT) - if !(val == chunkenc.ValFloat) { - return chunkenc.ValNone - } - t, _ := b.Iterator.At() - if t <= b.maxT { - return chunkenc.ValFloat - } - } - if t > b.maxT { - // We seek anyway so that the subsequent Next() calls will also return false. - b.Iterator.Seek(t) - return chunkenc.ValNone - } - return b.Iterator.Seek(t) -} - // safeHeadChunk makes sure that the chunk can be accessed without a race condition. type safeHeadChunk struct { chunkenc.Chunk diff --git a/tsdb/head_read_test.go b/tsdb/head_read_test.go index 8d835e943a..6dd4c0ff55 100644 --- a/tsdb/head_read_test.go +++ b/tsdb/head_read_test.go @@ -15,7 +15,6 @@ package tsdb import ( "context" - "fmt" "sync" "testing" @@ -26,150 +25,6 @@ import ( "github.com/prometheus/prometheus/tsdb/chunks" ) -func TestBoundedChunk(t *testing.T) { - tests := []struct { - name string - inputChunk chunkenc.Chunk - inputMinT int64 - inputMaxT int64 - initialSeek int64 - seekIsASuccess bool - expSamples []sample - }{ - { - name: "if there are no samples it returns nothing", - inputChunk: newTestChunk(0), - expSamples: nil, - }, - { - name: "bounds represent a single sample", - inputChunk: newTestChunk(10), - expSamples: []sample{ - {0, 0, nil, nil}, - }, - }, - { - name: "if there are bounds set only samples within them are returned", - inputChunk: newTestChunk(10), - inputMinT: 1, - inputMaxT: 8, - expSamples: []sample{ - {1, 1, nil, nil}, - {2, 2, nil, nil}, - {3, 3, nil, nil}, - {4, 4, nil, nil}, - {5, 5, nil, nil}, - {6, 6, nil, nil}, - {7, 7, nil, nil}, - {8, 8, nil, nil}, - }, - }, - { - name: "if bounds set and only maxt is less than actual maxt", - inputChunk: newTestChunk(10), - inputMinT: 0, - inputMaxT: 5, - expSamples: []sample{ - {0, 0, nil, nil}, - {1, 1, nil, nil}, - {2, 2, nil, nil}, - {3, 3, nil, nil}, - {4, 4, nil, nil}, - {5, 5, nil, nil}, - }, - }, - { - name: "if bounds set and only mint is more than actual mint", - inputChunk: newTestChunk(10), - inputMinT: 5, - inputMaxT: 9, - expSamples: []sample{ - {5, 5, nil, nil}, - {6, 6, nil, nil}, - {7, 7, nil, nil}, - {8, 8, nil, nil}, - {9, 9, nil, nil}, - }, - }, - { - name: "if there are bounds set with seek before mint", - inputChunk: newTestChunk(10), - inputMinT: 3, - inputMaxT: 7, - initialSeek: 1, - seekIsASuccess: true, - expSamples: []sample{ - {3, 3, nil, nil}, - {4, 4, nil, nil}, - {5, 5, nil, nil}, - {6, 6, nil, nil}, - {7, 7, nil, nil}, - }, - }, - { - name: "if there are bounds set with seek between mint and maxt", - inputChunk: newTestChunk(10), - inputMinT: 3, - inputMaxT: 7, - initialSeek: 5, - seekIsASuccess: true, - expSamples: []sample{ - {5, 5, nil, nil}, - {6, 6, nil, nil}, - {7, 7, nil, nil}, - }, - }, - { - name: "if there are bounds set with seek after maxt", - inputChunk: newTestChunk(10), - inputMinT: 3, - inputMaxT: 7, - initialSeek: 8, - seekIsASuccess: false, - }, - } - for _, tc := range tests { - t.Run(fmt.Sprintf("name=%s", tc.name), func(t *testing.T) { - iterable := boundedIterable{tc.inputChunk, tc.inputMinT, tc.inputMaxT} - - var samples []sample - it := iterable.Iterator(nil) - - if tc.initialSeek != 0 { - // Testing Seek() - val := it.Seek(tc.initialSeek) - require.Equal(t, tc.seekIsASuccess, val == chunkenc.ValFloat) - if val == chunkenc.ValFloat { - t, v := it.At() - samples = append(samples, sample{t, v, nil, nil}) - } - } - - // Testing Next() - for it.Next() == chunkenc.ValFloat { - t, v := it.At() - samples = append(samples, sample{t, v, nil, nil}) - } - - // it.Next() should keep returning no value. - for i := 0; i < 10; i++ { - require.Equal(t, chunkenc.ValNone, it.Next()) - } - - require.Equal(t, tc.expSamples, samples) - }) - } -} - -func newTestChunk(numSamples int) chunkenc.Chunk { - xor := chunkenc.NewXORChunk() - a, _ := xor.Appender() - for i := 0; i < numSamples; i++ { - a.Append(int64(i), float64(i)) - } - return xor -} - // TestMemSeries_chunk runs a series of tests on memSeries.chunk() calls. // It will simulate various conditions to ensure all code paths in that function are covered. func TestMemSeries_chunk(t *testing.T) { diff --git a/tsdb/ooo_head_read.go b/tsdb/ooo_head_read.go index 3b5adf80c9..47972c3cce 100644 --- a/tsdb/ooo_head_read.go +++ b/tsdb/ooo_head_read.go @@ -94,48 +94,32 @@ func (oh *OOOHeadIndexReader) series(ref storage.SeriesRef, builder *labels.Scra tmpChks := make([]chunks.Meta, 0, len(s.ooo.oooMmappedChunks)) - // We define these markers to track the last chunk reference while we - // fill the chunk meta. - // These markers are useful to give consistent responses to repeated queries - // even if new chunks that might be overlapping or not are added afterwards. - // Also, lastMinT and lastMaxT are initialized to the max int as a sentinel - // value to know they are unset. - var lastChunkRef chunks.ChunkRef - lastMinT, lastMaxT := int64(math.MaxInt64), int64(math.MaxInt64) - - addChunk := func(minT, maxT int64, ref chunks.ChunkRef) { - // the first time we get called is for the last included chunk. - // set the markers accordingly - if lastMinT == int64(math.MaxInt64) { - lastChunkRef = ref - lastMinT = minT - lastMaxT = maxT - } - + addChunk := func(minT, maxT int64, ref chunks.ChunkRef, chunk chunkenc.Chunk) { tmpChks = append(tmpChks, chunks.Meta{ - MinTime: minT, - MaxTime: maxT, - Ref: ref, - OOOLastRef: lastChunkRef, - OOOLastMinTime: lastMinT, - OOOLastMaxTime: lastMaxT, + MinTime: minT, + MaxTime: maxT, + Ref: ref, + Chunk: chunk, }) } - // Collect all chunks that overlap the query range, in order from most recent to most old, - // so we can set the correct markers. + // Collect all chunks that overlap the query range. if s.ooo.oooHeadChunk != nil { c := s.ooo.oooHeadChunk if c.OverlapsClosedInterval(oh.mint, oh.maxt) && maxMmapRef == 0 { ref := chunks.ChunkRef(chunks.NewHeadChunkRef(s.ref, s.oooHeadChunkID(len(s.ooo.oooMmappedChunks)))) - addChunk(c.minTime, c.maxTime, ref) + var xor chunkenc.Chunk + if len(c.chunk.samples) > 0 { // Empty samples happens in tests, at least. + xor, _ = c.chunk.ToXOR() // Ignoring error because it can't fail. + } + addChunk(c.minTime, c.maxTime, ref, xor) } } for i := len(s.ooo.oooMmappedChunks) - 1; i >= 0; i-- { c := s.ooo.oooMmappedChunks[i] if c.OverlapsClosedInterval(oh.mint, oh.maxt) && (maxMmapRef == 0 || maxMmapRef.GreaterThanOrEqualTo(c.ref)) && (lastGarbageCollectedMmapRef == 0 || c.ref.GreaterThan(lastGarbageCollectedMmapRef)) { ref := chunks.ChunkRef(chunks.NewHeadChunkRef(s.ref, s.oooHeadChunkID(i))) - addChunk(c.minTime, c.maxTime, ref) + addChunk(c.minTime, c.maxTime, ref, nil) } } @@ -163,6 +147,12 @@ func (oh *OOOHeadIndexReader) series(ref storage.SeriesRef, builder *labels.Scra case c.MaxTime > maxTime: maxTime = c.MaxTime (*chks)[len(*chks)-1].MaxTime = c.MaxTime + fallthrough + default: + // If the head OOO chunk is part of an output chunk, copy the chunk pointer. + if c.Chunk != nil { + (*chks)[len(*chks)-1].Chunk = c.Chunk + } } } @@ -185,10 +175,8 @@ func (oh *OOOHeadIndexReader) LabelValues(ctx context.Context, name string, matc } type chunkMetaAndChunkDiskMapperRef struct { - meta chunks.Meta - ref chunks.ChunkDiskMapperRef - origMinT int64 - origMaxT int64 + meta chunks.Meta + ref chunks.ChunkDiskMapperRef } func refLessByMinTimeAndMinRef(a, b chunkMetaAndChunkDiskMapperRef) int { diff --git a/tsdb/ooo_head_read_test.go b/tsdb/ooo_head_read_test.go index 1716f29b55..ce1fff100f 100644 --- a/tsdb/ooo_head_read_test.go +++ b/tsdb/ooo_head_read_test.go @@ -304,18 +304,6 @@ func TestOOOHeadIndexReader_Series(t *testing.T) { s1, _, _ := h.getOrCreate(s1ID, s1Lset) s1.ooo = &memSeriesOOOFields{} - var lastChunk chunkInterval - var lastChunkPos int - - // the marker should be set based on whichever is the last chunk/interval that overlaps with the query range - for i, interv := range intervals { - if overlapsClosedInterval(interv.mint, interv.maxt, tc.queryMinT, tc.queryMaxT) { - lastChunk = interv - lastChunkPos = i - } - } - lastChunkRef := chunks.ChunkRef(chunks.NewHeadChunkRef(1, chunks.HeadChunkID(uint64(lastChunkPos)))) - // define our expected chunks, by looking at the expected ChunkIntervals and setting... var expChunks []chunks.Meta for _, e := range tc.expChunks { @@ -323,10 +311,6 @@ func TestOOOHeadIndexReader_Series(t *testing.T) { Chunk: chunkenc.Chunk(nil), MinTime: e.mint, MaxTime: e.maxt, - // markers based on the last chunk we found above - OOOLastMinTime: lastChunk.mint, - OOOLastMaxTime: lastChunk.maxt, - OOOLastRef: lastChunkRef, } // Ref to whatever Ref the chunk has, that we refer to by ID @@ -343,6 +327,7 @@ func TestOOOHeadIndexReader_Series(t *testing.T) { if headChunk && len(intervals) > 0 { // Put the last interval in the head chunk s1.ooo.oooHeadChunk = &oooHeadChunk{ + chunk: NewOOOChunk(), minTime: intervals[len(intervals)-1].mint, maxTime: intervals[len(intervals)-1].maxt, } @@ -842,8 +827,8 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { } require.NoError(t, app.Commit()) - // The Series method is the one that populates the chunk meta OOO - // markers like OOOLastRef. These are then used by the ChunkReader. + // The Series method populates the chunk metas, taking a copy of the + // head OOO chunk if necessary. These are then used by the ChunkReader. ir := NewOOOHeadIndexReader(db.head, tc.queryMinT, tc.queryMaxT, 0) var chks []chunks.Meta var b labels.ScratchBuilder @@ -939,7 +924,6 @@ func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( sample{t: minutes(25), f: float64(1)}, sample{t: minutes(26), f: float64(0)}, sample{t: minutes(30), f: float64(0)}, - sample{t: minutes(32), f: float64(1)}, // This sample was added after Series() but before Chunk() and its in between the lastmint and maxt so it should be kept sample{t: minutes(35), f: float64(1)}, }, }, @@ -985,7 +969,6 @@ func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( sample{t: minutes(25), f: float64(1)}, sample{t: minutes(26), f: float64(0)}, sample{t: minutes(30), f: float64(0)}, - sample{t: minutes(32), f: float64(1)}, // This sample was added after Series() but before Chunk() and its in between the lastmint and maxt so it should be kept sample{t: minutes(35), f: float64(1)}, }, }, @@ -1007,8 +990,8 @@ func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( } require.NoError(t, app.Commit()) - // The Series method is the one that populates the chunk meta OOO - // markers like OOOLastRef. These are then used by the ChunkReader. + // The Series method populates the chunk metas, taking a copy of the + // head OOO chunk if necessary. These are then used by the ChunkReader. ir := NewOOOHeadIndexReader(db.head, tc.queryMinT, tc.queryMaxT, 0) var chks []chunks.Meta var b labels.ScratchBuilder From 60917f628b02a45f5fe38a731cd3aaef39112ae9 Mon Sep 17 00:00:00 2001 From: Carrie Edwards Date: Sun, 3 Mar 2024 11:43:05 -0800 Subject: [PATCH 08/29] Add test utilities for testing different sample types. Co-authored-by: Fiona Liao Signed-off-by: Carrie Edwards --- tsdb/testutil.go | 194 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 tsdb/testutil.go diff --git a/tsdb/testutil.go b/tsdb/testutil.go new file mode 100644 index 0000000000..0a26b39661 --- /dev/null +++ b/tsdb/testutil.go @@ -0,0 +1,194 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tsdb + +import ( + prom_testutil "github.com/prometheus/client_golang/prometheus/testutil" + "testing" + + "github.com/prometheus/prometheus/tsdb/chunkenc" + + "github.com/stretchr/testify/require" + + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/tsdb/chunks" +) + +const ( + float = "float" +) + +type tsValue struct { + Ts int64 + V int64 + CounterResetHeader histogram.CounterResetHint +} + +type sampleTypeScenario struct { + sampleType string + appendFunc func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) + sampleFunc func(ts, value int64) sample +} + +var sampleTypeScenarios = map[string]sampleTypeScenario{ + float: { + sampleType: sampleMetricTypeFloat, + appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) { + s := sample{t: ts, f: float64(value)} + ref, err := appender.Append(0, lbls, ts, s.f) + return ref, s, err + }, + sampleFunc: func(ts, value int64) sample { + return sample{t: ts, f: float64(value)} + }, + }, + //intHistogram: { + // sampleType: sampleMetricTypeHistogram, + // appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) { + // s := sample{t: ts, h: tsdbutil.GenerateTestHistogram(int(value))} + // ref, err := appender.AppendHistogram(0, lbls, ts, s.h, nil) + // return ref, s, err + // }, + // sampleFunc: func(ts, value int64) sample { + // return sample{t: ts, h: tsdbutil.GenerateTestHistogram(int(value))} + // }, + //}, + //floatHistogram: { + // sampleType: sampleMetricTypeHistogram, + // appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) { + // s := sample{t: ts, fh: tsdbutil.GenerateTestFloatHistogram(int(value))} + // ref, err := appender.AppendHistogram(0, lbls, ts, nil, s.fh) + // return ref, s, err + // }, + // sampleFunc: func(ts, value int64) sample { + // return sample{t: ts, fh: tsdbutil.GenerateTestFloatHistogram(int(value))} + // }, + //}, + //gaugeIntHistogram: { + // sampleType: sampleMetricTypeHistogram, + // appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) { + // s := sample{t: ts, h: tsdbutil.GenerateTestGaugeHistogram(int(value))} + // ref, err := appender.AppendHistogram(0, lbls, ts, s.h, nil) + // return ref, s, err + // }, + // sampleFunc: func(ts, value int64) sample { + // return sample{t: ts, h: tsdbutil.GenerateTestGaugeHistogram(int(value))} + // }, + //}, + //gaugeFloatHistogram: { + // sampleType: sampleMetricTypeHistogram, + // appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) { + // s := sample{t: ts, fh: tsdbutil.GenerateTestGaugeFloatHistogram(int(value))} + // ref, err := appender.AppendHistogram(0, lbls, ts, nil, s.fh) + // return ref, s, err + // }, + // sampleFunc: func(ts, value int64) sample { + // return sample{t: ts, fh: tsdbutil.GenerateTestGaugeFloatHistogram(int(value))} + // }, + //}, +} + +// requireEqualSamples checks that the actual series are equal to the expected ones. It ignores the counter reset hints for histograms. +func requireEqualSamples(t *testing.T, expected, actual map[string][]chunks.Sample, ignoreCounterResets bool) { + for name, expectedItem := range expected { + actualItem, ok := actual[name] + require.True(t, ok, "Expected series %s not found", name) + compareSamples(t, name, expectedItem, actualItem, ignoreCounterResets) + } + for name := range actual { + _, ok := expected[name] + require.True(t, ok, "Unexpected series %s", name) + } +} + +func requireEqualOOOSamples(t *testing.T, expectedSamples int, db *DB) { + require.GreaterOrEqual(t, float64(expectedSamples), + prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamplesAppended.WithLabelValues(sampleMetricTypeFloat))+ + prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamplesAppended.WithLabelValues(sampleMetricTypeHistogram)), + "number of ooo appended samples mismatch") +} + +func compareSamples(t *testing.T, name string, expected, actual []chunks.Sample, ignoreCounterResets bool) { + require.Equal(t, len(expected), len(actual), "Length not expected for %s", name) + for i, s := range expected { + expectedSample := s + actualSample := actual[i] + require.Equal(t, expectedSample.T(), expectedSample.T(), "Different timestamps for %s[%d]", name, i) + require.Equal(t, expectedSample.Type().String(), actualSample.Type().String(), "Different types for %s[%d] at ts %d", name, i, expectedSample.T()) + switch { + case s.H() != nil: + { + expectedHist := expectedSample.H() + actualHist := actualSample.H() + if ignoreCounterResets && expectedHist.CounterResetHint != histogram.GaugeType { + expectedHist.CounterResetHint = histogram.UnknownCounterReset + actualHist.CounterResetHint = histogram.UnknownCounterReset + } else { + require.Equal(t, expectedHist.CounterResetHint, actualHist.CounterResetHint, "Sample header doesn't match for %s[%d] at ts %d, expected: %s, actual: %s", name, i, expectedSample.T(), counterResetAsString(expectedHist.CounterResetHint), counterResetAsString(actualHist.CounterResetHint)) + } + require.Equal(t, expectedHist, actualHist, "Sample doesn't match for %s[%d] at ts %d", name, i, expectedSample.T()) + } + case s.FH() != nil: + { + expectedHist := expectedSample.FH() + actualHist := actualSample.FH() + if ignoreCounterResets { + expectedHist.CounterResetHint = histogram.UnknownCounterReset + actualHist.CounterResetHint = histogram.UnknownCounterReset + } else { + require.Equal(t, expectedHist.CounterResetHint, actualHist.CounterResetHint, "Sample header doesn't match for %s[%d] at ts %d, expected: %s, actual: %s", name, i, expectedSample.T(), counterResetAsString(expectedHist.CounterResetHint), counterResetAsString(actualHist.CounterResetHint)) + } + require.Equal(t, expectedHist, actualHist, "Sample doesn't match for %s[%d] at ts %d", name, i, expectedSample.T()) + } + default: + require.Equal(t, expectedSample, actualSample, "Sample doesn't match for %s[%d] at ts %d", name, i, expectedSample.T()) + } + } +} + +func counterResetAsString(h histogram.CounterResetHint) string { + switch h { + case histogram.UnknownCounterReset: + return "UnknownCounterReset" + case histogram.CounterReset: + return "CounterReset" + case histogram.NotCounterReset: + return "NotCounterReset" + case histogram.GaugeType: + return "GaugeType" + } + panic("Unexpected counter reset type") +} + +func samplesFromIterator(t testing.TB, it chunkenc.Iterator) []chunks.Sample { + var samples []chunks.Sample + for typ := it.Next(); typ != chunkenc.ValNone; typ = it.Next() { + switch typ { + case chunkenc.ValFloat: + ts, val := it.At() + samples = append(samples, sample{t: ts, f: val}) + case chunkenc.ValHistogram: + ts, val := it.AtHistogram(nil) + samples = append(samples, sample{t: ts, h: val}) + case chunkenc.ValFloatHistogram: + ts, val := it.AtFloatHistogram(nil) + samples = append(samples, sample{t: ts, fh: val}) + default: + t.Fatalf("unknown sample value type %s", typ) + } + } + return samples +} From 45a32a29ef3b4ad0c7ec87dc948b80320733e19e Mon Sep 17 00:00:00 2001 From: Carrie Edwards Date: Sun, 3 Mar 2024 11:44:12 -0800 Subject: [PATCH 09/29] Update tsdb tests to use test utils. Co-authored-by: Fiona Liao Signed-off-by: Carrie Edwards --- tsdb/db_test.go | 250 ++++++++++++------ tsdb/head_test.go | 88 +++++-- tsdb/ooo_head_read_test.go | 522 ++++++++++++++++++++----------------- 3 files changed, 515 insertions(+), 345 deletions(-) diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 1fb6d30d61..3d6d55c90c 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -4495,6 +4495,14 @@ func TestMetadataAssertInMemoryData(t *testing.T) { // // are not included in this compaction. func TestOOOCompaction(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOOOCompaction(t, scenario) + }) + } +} + +func testOOOCompaction(t *testing.T, scenario sampleTypeScenario) { dir := t.TempDir() ctx := context.Background() @@ -4516,9 +4524,9 @@ func TestOOOCompaction(t *testing.T) { app := db.Appender(context.Background()) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - _, err := app.Append(0, series1, ts, float64(ts)) + _, _, err := scenario.appendFunc(app, series1, ts, ts) require.NoError(t, err) - _, err = app.Append(0, series2, ts, float64(2*ts)) + _, _, err = scenario.appendFunc(app, series2, ts, 2*ts) require.NoError(t, err) } require.NoError(t, app.Commit()) @@ -4551,8 +4559,8 @@ func TestOOOCompaction(t *testing.T) { fromMins, toMins := r[0], r[1] for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - series1Samples = append(series1Samples, sample{ts, float64(ts), nil, nil}) - series2Samples = append(series2Samples, sample{ts, float64(2 * ts), nil, nil}) + series1Samples = append(series1Samples, scenario.sampleFunc(ts, ts)) + series2Samples = append(series2Samples, scenario.sampleFunc(ts, 2*ts)) } } expRes := map[string][]chunks.Sample{ @@ -4564,7 +4572,7 @@ func TestOOOCompaction(t *testing.T) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - require.Equal(t, expRes, actRes) + requireEqualSamples(t, expRes, actRes, true) } verifyDBSamples() // Before any compaction. @@ -4619,8 +4627,8 @@ func TestOOOCompaction(t *testing.T) { series2Samples := make([]chunks.Sample, 0, toMins-fromMins+1) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - series1Samples = append(series1Samples, sample{ts, float64(ts), nil, nil}) - series2Samples = append(series2Samples, sample{ts, float64(2 * ts), nil, nil}) + series1Samples = append(series1Samples, scenario.sampleFunc(ts, ts)) + series2Samples = append(series2Samples, scenario.sampleFunc(ts, 2*ts)) } expRes := map[string][]chunks.Sample{ series1.String(): series1Samples, @@ -4631,7 +4639,7 @@ func TestOOOCompaction(t *testing.T) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - require.Equal(t, expRes, actRes) + requireEqualSamples(t, expRes, actRes, true) } // Checking for expected data in the blocks. @@ -4675,6 +4683,14 @@ func TestOOOCompaction(t *testing.T) { // TestOOOCompactionWithNormalCompaction tests if OOO compaction is performed // when the normal head's compaction is done. func TestOOOCompactionWithNormalCompaction(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOOOCompactionWithNormalCompaction(t, scenario) + }) + } +} + +func testOOOCompactionWithNormalCompaction(t *testing.T, scenario sampleTypeScenario) { dir := t.TempDir() ctx := context.Background() @@ -4696,9 +4712,9 @@ func TestOOOCompactionWithNormalCompaction(t *testing.T) { app := db.Appender(context.Background()) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - _, err := app.Append(0, series1, ts, float64(ts)) + _, _, err := scenario.appendFunc(app, series1, ts, ts) require.NoError(t, err) - _, err = app.Append(0, series2, ts, float64(2*ts)) + _, _, err = scenario.appendFunc(app, series2, ts, 2*ts) require.NoError(t, err) } require.NoError(t, app.Commit()) @@ -4751,8 +4767,8 @@ func TestOOOCompactionWithNormalCompaction(t *testing.T) { series2Samples := make([]chunks.Sample, 0, toMins-fromMins+1) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - series1Samples = append(series1Samples, sample{ts, float64(ts), nil, nil}) - series2Samples = append(series2Samples, sample{ts, float64(2 * ts), nil, nil}) + series1Samples = append(series1Samples, scenario.sampleFunc(ts, ts)) + series2Samples = append(series2Samples, scenario.sampleFunc(ts, 2*ts)) } expRes := map[string][]chunks.Sample{ series1.String(): series1Samples, @@ -4763,7 +4779,7 @@ func TestOOOCompactionWithNormalCompaction(t *testing.T) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - require.Equal(t, expRes, actRes) + requireEqualSamples(t, expRes, actRes, true) } // Checking for expected data in the blocks. @@ -4775,6 +4791,14 @@ func TestOOOCompactionWithNormalCompaction(t *testing.T) { // configured to not have wal and wbl but its able to compact both the in-order // and out-of-order head. func TestOOOCompactionWithDisabledWriteLog(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOOOCompactionWithDisabledWriteLog(t, scenario) + }) + } +} + +func testOOOCompactionWithDisabledWriteLog(t *testing.T, scenario sampleTypeScenario) { dir := t.TempDir() ctx := context.Background() @@ -4797,9 +4821,9 @@ func TestOOOCompactionWithDisabledWriteLog(t *testing.T) { app := db.Appender(context.Background()) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - _, err := app.Append(0, series1, ts, float64(ts)) + _, _, err := scenario.appendFunc(app, series1, ts, ts) require.NoError(t, err) - _, err = app.Append(0, series2, ts, float64(2*ts)) + _, _, err = scenario.appendFunc(app, series2, ts, 2*ts) require.NoError(t, err) } require.NoError(t, app.Commit()) @@ -4852,8 +4876,8 @@ func TestOOOCompactionWithDisabledWriteLog(t *testing.T) { series2Samples := make([]chunks.Sample, 0, toMins-fromMins+1) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - series1Samples = append(series1Samples, sample{ts, float64(ts), nil, nil}) - series2Samples = append(series2Samples, sample{ts, float64(2 * ts), nil, nil}) + series1Samples = append(series1Samples, scenario.sampleFunc(ts, ts)) + series2Samples = append(series2Samples, scenario.sampleFunc(ts, 2*ts)) } expRes := map[string][]chunks.Sample{ series1.String(): series1Samples, @@ -4864,7 +4888,7 @@ func TestOOOCompactionWithDisabledWriteLog(t *testing.T) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - require.Equal(t, expRes, actRes) + requireEqualSamples(t, expRes, actRes, true) } // Checking for expected data in the blocks. @@ -4876,6 +4900,14 @@ func TestOOOCompactionWithDisabledWriteLog(t *testing.T) { // missing after a restart while snapshot was enabled, but the query still returns the right // data from the mmap chunks. func TestOOOQueryAfterRestartWithSnapshotAndRemovedWBL(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOOOQueryAfterRestartWithSnapshotAndRemovedWBL(t, scenario) + }) + } +} + +func testOOOQueryAfterRestartWithSnapshotAndRemovedWBL(t *testing.T, scenario sampleTypeScenario) { dir := t.TempDir() ctx := context.Background() @@ -4898,9 +4930,9 @@ func TestOOOQueryAfterRestartWithSnapshotAndRemovedWBL(t *testing.T) { app := db.Appender(context.Background()) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - _, err := app.Append(0, series1, ts, float64(ts)) + _, _, err := scenario.appendFunc(app, series1, ts, ts) require.NoError(t, err) - _, err = app.Append(0, series2, ts, float64(2*ts)) + _, _, err = scenario.appendFunc(app, series2, ts, 2*ts) require.NoError(t, err) } require.NoError(t, app.Commit()) @@ -4946,8 +4978,8 @@ func TestOOOQueryAfterRestartWithSnapshotAndRemovedWBL(t *testing.T) { series2Samples := make([]chunks.Sample, 0, toMins-fromMins+1) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - series1Samples = append(series1Samples, sample{ts, float64(ts), nil, nil}) - series2Samples = append(series2Samples, sample{ts, float64(2 * ts), nil, nil}) + series1Samples = append(series1Samples, scenario.sampleFunc(ts, ts)) + series2Samples = append(series2Samples, scenario.sampleFunc(ts, ts*2)) } expRes := map[string][]chunks.Sample{ series1.String(): series1Samples, @@ -4958,7 +4990,7 @@ func TestOOOQueryAfterRestartWithSnapshotAndRemovedWBL(t *testing.T) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - require.Equal(t, expRes, actRes) + requireEqualSamples(t, expRes, actRes, true) } // Checking for expected ooo data from mmap chunks. @@ -5159,6 +5191,14 @@ func Test_ChunkQuerier_OOOQuery(t *testing.T) { } func TestOOOAppendAndQuery(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOOOAppendAndQuery(t, scenario) + }) + } +} + +func testOOOAppendAndQuery(t *testing.T, scenario sampleTypeScenario) { opts := DefaultOptions() opts.OutOfOrderCapMax = 30 opts.OutOfOrderTimeWindow = 4 * time.Hour.Milliseconds() @@ -5180,13 +5220,13 @@ func TestOOOAppendAndQuery(t *testing.T) { key := lbls.String() from, to := minutes(fromMins), minutes(toMins) for min := from; min <= to; min += time.Minute.Milliseconds() { - val := rand.Float64() - _, err := app.Append(0, lbls, min, val) + val := rand.Intn(1000) + _, s, err := scenario.appendFunc(app, lbls, min, int64(val)) if faceError { require.Error(t, err) } else { require.NoError(t, err) - appendedSamples[key] = append(appendedSamples[key], sample{t: min, f: val}) + appendedSamples[key] = append(appendedSamples[key], s) totalSamples++ } } @@ -5222,7 +5262,7 @@ func TestOOOAppendAndQuery(t *testing.T) { expSamples[k] = append(expSamples[k], s) } } - require.Equal(t, expSamples, seriesSet) + requireEqualSamples(t, expSamples, seriesSet, true) requireEqualOOOSamples(t, totalSamples-2, db) } @@ -5284,6 +5324,14 @@ func TestOOOAppendAndQuery(t *testing.T) { } func TestOOODisabled(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOOODisabled(t, scenario) + }) + } +} + +func testOOODisabled(t *testing.T, scenario sampleTypeScenario) { opts := DefaultOptions() opts.OutOfOrderTimeWindow = 0 db := openTestDB(t, opts, nil) @@ -5297,19 +5345,19 @@ func TestOOODisabled(t *testing.T) { expSamples := make(map[string][]chunks.Sample) totalSamples := 0 failedSamples := 0 - addSample := func(lbls labels.Labels, fromMins, toMins int64, faceError bool) { + + addSample := func(db *DB, lbls labels.Labels, fromMins, toMins int64, faceError bool) { app := db.Appender(context.Background()) key := lbls.String() from, to := minutes(fromMins), minutes(toMins) for min := from; min <= to; min += time.Minute.Milliseconds() { - val := rand.Float64() - _, err := app.Append(0, lbls, min, val) + _, _, err := scenario.appendFunc(app, lbls, min, min) if faceError { require.Error(t, err) failedSamples++ } else { require.NoError(t, err) - expSamples[key] = append(expSamples[key], sample{t: min, f: val}) + expSamples[key] = append(expSamples[key], scenario.sampleFunc(min, min)) totalSamples++ } } @@ -5320,21 +5368,21 @@ func TestOOODisabled(t *testing.T) { } } - addSample(s1, 300, 300, false) // In-order samples. - addSample(s1, 250, 260, true) // Some ooo samples. - addSample(s1, 59, 59, true) // Out of time window. - addSample(s1, 60, 65, true) // At the edge of time window, also it would be "out of bound" without the ooo support. - addSample(s1, 59, 59, true) // Out of time window again. - addSample(s1, 301, 310, false) // More in-order samples. + addSample(db, s1, 300, 300, false) // In-order samples. + addSample(db, s1, 250, 260, true) // Some ooo samples. + addSample(db, s1, 59, 59, true) // Out of time window. + addSample(db, s1, 60, 65, true) // At the edge of time window, also it would be "out of bound" without the ooo support. + addSample(db, s1, 59, 59, true) // Out of time window again. + addSample(db, s1, 301, 310, false) // More in-order samples. querier, err := db.Querier(math.MinInt64, math.MaxInt64) require.NoError(t, err) seriesSet := query(t, querier, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.")) - require.Equal(t, expSamples, seriesSet) + requireEqualSamples(t, expSamples, seriesSet, true) requireEqualOOOSamples(t, 0, db) require.Equal(t, float64(failedSamples), - prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(sampleMetricTypeFloat))+prom_testutil.ToFloat64(db.head.metrics.outOfBoundSamples.WithLabelValues(sampleMetricTypeFloat)), + prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType))+prom_testutil.ToFloat64(db.head.metrics.outOfBoundSamples.WithLabelValues(scenario.sampleType)), "number of ooo/oob samples mismatch") // Verifying that no OOO artifacts were generated. @@ -5349,6 +5397,14 @@ func TestOOODisabled(t *testing.T) { } func TestWBLAndMmapReplay(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testWBLAndMmapReplay(t, scenario) + }) + } +} + +func testWBLAndMmapReplay(t *testing.T, scenario sampleTypeScenario) { opts := DefaultOptions() opts.OutOfOrderCapMax = 30 opts.OutOfOrderTimeWindow = 4 * time.Hour.Milliseconds() @@ -5369,10 +5425,10 @@ func TestWBLAndMmapReplay(t *testing.T) { key := lbls.String() from, to := minutes(fromMins), minutes(toMins) for min := from; min <= to; min += time.Minute.Milliseconds() { - val := rand.Float64() - _, err := app.Append(0, lbls, min, val) + val := rand.Intn(1000) + _, s, err := scenario.appendFunc(app, lbls, min, int64(val)) require.NoError(t, err) - expSamples[key] = append(expSamples[key], sample{t: min, f: val}) + expSamples[key] = append(expSamples[key], s) totalSamples++ } require.NoError(t, app.Commit()) @@ -5390,7 +5446,7 @@ func TestWBLAndMmapReplay(t *testing.T) { }) exp[k] = v } - require.Equal(t, exp, seriesSet) + requireEqualSamples(t, exp, seriesSet, true) } // In-order samples. @@ -5413,10 +5469,7 @@ func TestWBLAndMmapReplay(t *testing.T) { chk, err := db.head.chunkDiskMapper.Chunk(mc.ref) require.NoError(t, err) it := chk.Iterator(nil) - for it.Next() == chunkenc.ValFloat { - ts, val := it.At() - s1MmapSamples = append(s1MmapSamples, sample{t: ts, f: val}) - } + s1MmapSamples = append(s1MmapSamples, samplesFromIterator(t, it)...) } require.NotEmpty(t, s1MmapSamples) @@ -5534,6 +5587,14 @@ func TestWBLAndMmapReplay(t *testing.T) { } func TestOOOCompactionFailure(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOOOCompactionFailure(t, scenario) + }) + } +} + +func testOOOCompactionFailure(t *testing.T, scenario sampleTypeScenario) { dir := t.TempDir() ctx := context.Background() @@ -5554,7 +5615,7 @@ func TestOOOCompactionFailure(t *testing.T) { app := db.Appender(context.Background()) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - _, err := app.Append(0, series1, ts, float64(ts)) + _, _, err := scenario.appendFunc(app, series1, ts, ts) require.NoError(t, err) } require.NoError(t, app.Commit()) @@ -5642,7 +5703,7 @@ func TestOOOCompactionFailure(t *testing.T) { series1Samples := make([]chunks.Sample, 0, toMins-fromMins+1) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - series1Samples = append(series1Samples, sample{ts, float64(ts), nil, nil}) + series1Samples = append(series1Samples, scenario.sampleFunc(ts, ts)) } expRes := map[string][]chunks.Sample{ series1.String(): series1Samples, @@ -5650,9 +5711,8 @@ func TestOOOCompactionFailure(t *testing.T) { q, err := NewBlockQuerier(block, math.MinInt64, math.MaxInt64) require.NoError(t, err) - actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - require.Equal(t, expRes, actRes) + requireEqualSamples(t, expRes, actRes, true) } // Checking for expected data in the blocks. @@ -5819,6 +5879,14 @@ func TestWBLCorruption(t *testing.T) { } func TestOOOMmapCorruption(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOOOMmapCorruption(t, scenario) + }) + } +} + +func testOOOMmapCorruption(t *testing.T, scenario sampleTypeScenario) { dir := t.TempDir() opts := DefaultOptions() @@ -5838,11 +5906,11 @@ func TestOOOMmapCorruption(t *testing.T) { app := db.Appender(context.Background()) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - _, err := app.Append(0, series1, ts, float64(ts)) + _, s, err := scenario.appendFunc(app, series1, ts, ts) require.NoError(t, err) - allSamples = append(allSamples, sample{t: ts, f: float64(ts)}) + allSamples = append(allSamples, s) if inMmapAfterCorruption { - expInMmapChunks = append(expInMmapChunks, sample{t: ts, f: float64(ts)}) + expInMmapChunks = append(expInMmapChunks, s) } } require.NoError(t, app.Commit()) @@ -5880,7 +5948,7 @@ func TestOOOMmapCorruption(t *testing.T) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - require.Equal(t, expRes, actRes) + requireEqualSamples(t, expRes, actRes, true) } verifySamples(allSamples) @@ -5942,6 +6010,14 @@ func TestOOOMmapCorruption(t *testing.T) { } func TestOutOfOrderRuntimeConfig(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOutOfOrderRuntimeConfig(t, scenario) + }) + } +} + +func testOutOfOrderRuntimeConfig(t *testing.T, scenario sampleTypeScenario) { ctx := context.Background() getDB := func(oooTimeWindow int64) *DB { @@ -5975,10 +6051,10 @@ func TestOutOfOrderRuntimeConfig(t *testing.T) { app := db.Appender(context.Background()) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - _, err := app.Append(0, series1, ts, float64(ts)) + _, s, err := scenario.appendFunc(app, series1, ts, ts) if success { require.NoError(t, err) - allSamples = append(allSamples, sample{t: ts, f: float64(ts)}) + allSamples = append(allSamples, s) } else { require.Error(t, err) } @@ -6000,7 +6076,7 @@ func TestOutOfOrderRuntimeConfig(t *testing.T) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - require.Equal(t, expRes, actRes) + requireEqualSamples(t, expRes, actRes, true) } doOOOCompaction := func(t *testing.T, db *DB) { @@ -6173,12 +6249,20 @@ func TestOutOfOrderRuntimeConfig(t *testing.T) { } func TestNoGapAfterRestartWithOOO(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testNoGapAfterRestartWithOOO(t, scenario) + }) + } +} + +func testNoGapAfterRestartWithOOO(t *testing.T, scenario sampleTypeScenario) { series1 := labels.FromStrings("foo", "bar1") addSamples := func(t *testing.T, db *DB, fromMins, toMins int64, success bool) { app := db.Appender(context.Background()) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - _, err := app.Append(0, series1, ts, float64(ts)) + _, _, err := scenario.appendFunc(app, series1, ts, ts) if success { require.NoError(t, err) } else { @@ -6192,7 +6276,7 @@ func TestNoGapAfterRestartWithOOO(t *testing.T) { var expSamples []chunks.Sample for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - expSamples = append(expSamples, sample{t: ts, f: float64(ts)}) + expSamples = append(expSamples, scenario.sampleFunc(ts, ts)) } expRes := map[string][]chunks.Sample{ @@ -6203,7 +6287,7 @@ func TestNoGapAfterRestartWithOOO(t *testing.T) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - require.Equal(t, expRes, actRes) + requireEqualSamples(t, expRes, actRes, true) } cases := []struct { @@ -6280,6 +6364,14 @@ func TestNoGapAfterRestartWithOOO(t *testing.T) { } func TestWblReplayAfterOOODisableAndRestart(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testWblReplayAfterOOODisableAndRestart(t, scenario) + }) + } +} + +func testWblReplayAfterOOODisableAndRestart(t *testing.T, scenario sampleTypeScenario) { dir := t.TempDir() opts := DefaultOptions() @@ -6298,9 +6390,9 @@ func TestWblReplayAfterOOODisableAndRestart(t *testing.T) { app := db.Appender(context.Background()) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - _, err := app.Append(0, series1, ts, float64(ts)) + _, s, err := scenario.appendFunc(app, series1, ts, ts) require.NoError(t, err) - allSamples = append(allSamples, sample{t: ts, f: float64(ts)}) + allSamples = append(allSamples, s) } require.NoError(t, app.Commit()) } @@ -6323,7 +6415,7 @@ func TestWblReplayAfterOOODisableAndRestart(t *testing.T) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - require.Equal(t, expRes, actRes) + requireEqualSamples(t, expRes, actRes, true) } verifySamples(allSamples) @@ -6339,6 +6431,14 @@ func TestWblReplayAfterOOODisableAndRestart(t *testing.T) { } func TestPanicOnApplyConfig(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testPanicOnApplyConfig(t, scenario) + }) + } +} + +func testPanicOnApplyConfig(t *testing.T, scenario sampleTypeScenario) { dir := t.TempDir() opts := DefaultOptions() @@ -6357,9 +6457,9 @@ func TestPanicOnApplyConfig(t *testing.T) { app := db.Appender(context.Background()) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - _, err := app.Append(0, series1, ts, float64(ts)) + _, s, err := scenario.appendFunc(app, series1, ts, ts) require.NoError(t, err) - allSamples = append(allSamples, sample{t: ts, f: float64(ts)}) + allSamples = append(allSamples, s) } require.NoError(t, app.Commit()) } @@ -6387,6 +6487,14 @@ func TestPanicOnApplyConfig(t *testing.T) { } func TestDiskFillingUpAfterDisablingOOO(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testDiskFillingUpAfterDisablingOOO(t, scenario) + }) + } +} + +func testDiskFillingUpAfterDisablingOOO(t *testing.T, scenario sampleTypeScenario) { dir := t.TempDir() ctx := context.Background() @@ -6406,9 +6514,9 @@ func TestDiskFillingUpAfterDisablingOOO(t *testing.T) { app := db.Appender(context.Background()) for min := fromMins; min <= toMins; min++ { ts := min * time.Minute.Milliseconds() - _, err := app.Append(0, series1, ts, float64(ts)) + _, s, err := scenario.appendFunc(app, series1, ts, ts) require.NoError(t, err) - allSamples = append(allSamples, sample{t: ts, f: float64(ts)}) + allSamples = append(allSamples, s) } require.NoError(t, app.Commit()) } @@ -7060,12 +7168,6 @@ Outer: require.NoError(t, writerErr) } -func requireEqualOOOSamples(t *testing.T, expectedSamples int, db *DB) { - require.Equal(t, float64(expectedSamples), - prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamplesAppended.WithLabelValues(sampleMetricTypeFloat)), - "number of ooo appended samples mismatch") -} - type mockCompactorFn struct { planFn func() ([]string, error) compactFn func() ([]ulid.ULID, error) diff --git a/tsdb/head_test.go b/tsdb/head_test.go index 93f046e5b3..1eb0c0534d 100644 --- a/tsdb/head_test.go +++ b/tsdb/head_test.go @@ -2665,6 +2665,14 @@ func TestIsolationWithoutAdd(t *testing.T) { } func TestOutOfOrderSamplesMetric(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOutOfOrderSamplesMetric(t, scenario) + }) + } +} + +func testOutOfOrderSamplesMetric(t *testing.T, scenario sampleTypeScenario) { dir := t.TempDir() db, err := Open(dir, nil, nil, DefaultOptions(), nil) @@ -2674,33 +2682,38 @@ func TestOutOfOrderSamplesMetric(t *testing.T) { }() db.DisableCompactions() + appendSample := func(appender storage.Appender, ts int64) (storage.SeriesRef, error) { + ref, _, err := scenario.appendFunc(appender, labels.FromStrings("a", "b"), ts, 99) + return ref, err + } + ctx := context.Background() app := db.Appender(ctx) for i := 1; i <= 5; i++ { - _, err = app.Append(0, labels.FromStrings("a", "b"), int64(i), 99) + _, err = appendSample(app, int64(i)) require.NoError(t, err) } require.NoError(t, app.Commit()) // Test out of order metric. - require.Equal(t, 0.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(sampleMetricTypeFloat))) + require.Equal(t, 0.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType))) app = db.Appender(ctx) - _, err = app.Append(0, labels.FromStrings("a", "b"), 2, 99) + _, err = appendSample(app, 2) require.Equal(t, storage.ErrOutOfOrderSample, err) - require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(sampleMetricTypeFloat))) + require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType))) - _, err = app.Append(0, labels.FromStrings("a", "b"), 3, 99) + _, err = appendSample(app, 3) require.Equal(t, storage.ErrOutOfOrderSample, err) - require.Equal(t, 2.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(sampleMetricTypeFloat))) + require.Equal(t, 2.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType))) - _, err = app.Append(0, labels.FromStrings("a", "b"), 4, 99) + _, err = appendSample(app, 4) require.Equal(t, storage.ErrOutOfOrderSample, err) - require.Equal(t, 3.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(sampleMetricTypeFloat))) + require.Equal(t, 3.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType))) require.NoError(t, app.Commit()) // Compact Head to test out of bound metric. app = db.Appender(ctx) - _, err = app.Append(0, labels.FromStrings("a", "b"), DefaultBlockDuration*2, 99) + _, err = appendSample(app, DefaultBlockDuration*2) require.NoError(t, err) require.NoError(t, app.Commit()) @@ -2709,36 +2722,36 @@ func TestOutOfOrderSamplesMetric(t *testing.T) { require.Greater(t, db.head.minValidTime.Load(), int64(0)) app = db.Appender(ctx) - _, err = app.Append(0, labels.FromStrings("a", "b"), db.head.minValidTime.Load()-2, 99) + _, err = appendSample(app, db.head.minValidTime.Load()-2) require.Equal(t, storage.ErrOutOfBounds, err) - require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.outOfBoundSamples.WithLabelValues(sampleMetricTypeFloat))) + require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.outOfBoundSamples.WithLabelValues(scenario.sampleType))) - _, err = app.Append(0, labels.FromStrings("a", "b"), db.head.minValidTime.Load()-1, 99) + _, err = appendSample(app, db.head.minValidTime.Load()-1) require.Equal(t, storage.ErrOutOfBounds, err) - require.Equal(t, 2.0, prom_testutil.ToFloat64(db.head.metrics.outOfBoundSamples.WithLabelValues(sampleMetricTypeFloat))) + require.Equal(t, 2.0, prom_testutil.ToFloat64(db.head.metrics.outOfBoundSamples.WithLabelValues(scenario.sampleType))) require.NoError(t, app.Commit()) // Some more valid samples for out of order. app = db.Appender(ctx) for i := 1; i <= 5; i++ { - _, err = app.Append(0, labels.FromStrings("a", "b"), db.head.minValidTime.Load()+DefaultBlockDuration+int64(i), 99) + _, err = appendSample(app, db.head.minValidTime.Load()+DefaultBlockDuration+int64(i)) require.NoError(t, err) } require.NoError(t, app.Commit()) // Test out of order metric. app = db.Appender(ctx) - _, err = app.Append(0, labels.FromStrings("a", "b"), db.head.minValidTime.Load()+DefaultBlockDuration+2, 99) + _, err = appendSample(app, db.head.minValidTime.Load()+DefaultBlockDuration+2) require.Equal(t, storage.ErrOutOfOrderSample, err) - require.Equal(t, 4.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(sampleMetricTypeFloat))) + require.Equal(t, 4.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType))) - _, err = app.Append(0, labels.FromStrings("a", "b"), db.head.minValidTime.Load()+DefaultBlockDuration+3, 99) + _, err = appendSample(app, db.head.minValidTime.Load()+DefaultBlockDuration+3) require.Equal(t, storage.ErrOutOfOrderSample, err) - require.Equal(t, 5.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(sampleMetricTypeFloat))) + require.Equal(t, 5.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType))) - _, err = app.Append(0, labels.FromStrings("a", "b"), db.head.minValidTime.Load()+DefaultBlockDuration+4, 99) + _, err = appendSample(app, db.head.minValidTime.Load()+DefaultBlockDuration+4) require.Equal(t, storage.ErrOutOfOrderSample, err) - require.Equal(t, 6.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(sampleMetricTypeFloat))) + require.Equal(t, 6.0, prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType))) require.NoError(t, app.Commit()) } @@ -4801,6 +4814,14 @@ func TestWBLReplay(t *testing.T) { // TestOOOMmapReplay checks the replay at a low level. func TestOOOMmapReplay(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOOOMmapReplay(t, scenario) + }) + } +} + +func testOOOMmapReplay(t *testing.T, scenario sampleTypeScenario) { dir := t.TempDir() wal, err := wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, wlog.CompressionSnappy) require.NoError(t, err) @@ -4820,8 +4841,7 @@ func TestOOOMmapReplay(t *testing.T) { l := labels.FromStrings("foo", "bar") appendSample := func(mins int64) { app := h.Appender(context.Background()) - ts, v := mins*time.Minute.Milliseconds(), float64(mins) - _, err := app.Append(0, l, ts, v) + _, _, err := scenario.appendFunc(app, l, mins*time.Minute.Milliseconds(), mins) require.NoError(t, err) require.NoError(t, app.Commit()) } @@ -5096,6 +5116,14 @@ func TestReplayAfterMmapReplayError(t *testing.T) { } func TestOOOAppendWithNoSeries(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOOOAppendWithNoSeries(t, scenario.appendFunc) + }) + } +} + +func testOOOAppendWithNoSeries(t *testing.T, appendFunc func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error)) { dir := t.TempDir() wal, err := wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, wlog.CompressionSnappy) require.NoError(t, err) @@ -5116,7 +5144,7 @@ func TestOOOAppendWithNoSeries(t *testing.T) { appendSample := func(lbls labels.Labels, ts int64) { app := h.Appender(context.Background()) - _, err := app.Append(0, lbls, ts*time.Minute.Milliseconds(), float64(ts)) + _, _, err := appendFunc(app, lbls, ts*time.Minute.Milliseconds(), ts) require.NoError(t, err) require.NoError(t, app.Commit()) } @@ -5164,7 +5192,7 @@ func TestOOOAppendWithNoSeries(t *testing.T) { // Now 179m is too old. s4 := newLabels(4) app := h.Appender(context.Background()) - _, err = app.Append(0, s4, 179*time.Minute.Milliseconds(), float64(179)) + _, _, err = appendFunc(app, s4, 179*time.Minute.Milliseconds(), 179) require.Equal(t, storage.ErrTooOldSample, err) require.NoError(t, app.Rollback()) verifyOOOSamples(s3, 1) @@ -5177,6 +5205,14 @@ func TestOOOAppendWithNoSeries(t *testing.T) { } func TestHeadMinOOOTimeUpdate(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testHeadMinOOOTimeUpdate(t, scenario) + }) + } +} + +func testHeadMinOOOTimeUpdate(t *testing.T, scenario sampleTypeScenario) { dir := t.TempDir() wal, err := wlog.NewSize(nil, nil, filepath.Join(dir, "wal"), 32768, wlog.CompressionSnappy) require.NoError(t, err) @@ -5195,15 +5231,13 @@ func TestHeadMinOOOTimeUpdate(t *testing.T) { require.NoError(t, h.Init(0)) appendSample := func(ts int64) { - lbls := labels.FromStrings("foo", "bar") app := h.Appender(context.Background()) - _, err := app.Append(0, lbls, ts*time.Minute.Milliseconds(), float64(ts)) + _, _, err = scenario.appendFunc(app, labels.FromStrings("a", "b"), ts*time.Minute.Milliseconds(), 99.0) require.NoError(t, err) require.NoError(t, app.Commit()) } appendSample(300) // In-order sample. - require.Equal(t, int64(math.MaxInt64), h.MinOOOTime()) appendSample(295) // OOO sample. diff --git a/tsdb/ooo_head_read_test.go b/tsdb/ooo_head_read_test.go index ce1fff100f..9eaab13dcf 100644 --- a/tsdb/ooo_head_read_test.go +++ b/tsdb/ooo_head_read_test.go @@ -359,6 +359,15 @@ func TestOOOHeadIndexReader_Series(t *testing.T) { } func TestOOOHeadChunkReader_LabelValues(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOOOHeadChunkReader_LabelValues(t, scenario) + }) + } +} + +//nolint:revive // unexported-return. +func testOOOHeadChunkReader_LabelValues(t *testing.T, scenario sampleTypeScenario) { chunkRange := int64(2000) head, _ := newTestHead(t, chunkRange, wlog.CompressionNone, true) t.Cleanup(func() { require.NoError(t, head.Close()) }) @@ -368,15 +377,15 @@ func TestOOOHeadChunkReader_LabelValues(t *testing.T) { app := head.Appender(context.Background()) // Add in-order samples - _, err := app.Append(0, labels.FromStrings("foo", "bar1"), 100, 1) + _, _, err := scenario.appendFunc(app, labels.FromStrings("foo", "bar1"), 100, int64(1)) require.NoError(t, err) - _, err = app.Append(0, labels.FromStrings("foo", "bar2"), 100, 2) + _, _, err = scenario.appendFunc(app, labels.FromStrings("foo", "bar2"), 100, int64(2)) require.NoError(t, err) // Add ooo samples for those series - _, err = app.Append(0, labels.FromStrings("foo", "bar1"), 90, 1) + _, _, err = scenario.appendFunc(app, labels.FromStrings("foo", "bar1"), 90, int64(1)) require.NoError(t, err) - _, err = app.Append(0, labels.FromStrings("foo", "bar2"), 90, 2) + _, _, err = scenario.appendFunc(app, labels.FromStrings("foo", "bar2"), 90, int64(2)) require.NoError(t, err) require.NoError(t, app.Commit()) @@ -453,6 +462,19 @@ func TestOOOHeadChunkReader_LabelValues(t *testing.T) { // It does so by appending out of order samples to the db and then initializing // an OOOHeadChunkReader to read chunks from it. func TestOOOHeadChunkReader_Chunk(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOOOHeadChunkReader_Chunk(t, scenario) + }) + } +} + +// TestOOOHeadChunkReader_Chunk tests that the Chunk method works as expected. +// It does so by appending out of order samples to the db and then initializing +// an OOOHeadChunkReader to read chunks from it. +// +//nolint:revive // unexported-return. +func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { opts := DefaultOptions() opts.OutOfOrderCapMax = 5 opts.OutOfOrderTimeWindow = 120 * time.Minute.Milliseconds() @@ -460,12 +482,6 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { s1 := labels.FromStrings("l", "v1") minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() } - appendSample := func(app storage.Appender, l labels.Labels, timestamp int64, value float64) storage.SeriesRef { - ref, err := app.Append(0, l, timestamp, value) - require.NoError(t, err) - return ref - } - t.Run("Getting a non existing chunk fails with not found error", func(t *testing.T) { db := newTestDBWithOpts(t, opts) @@ -484,7 +500,7 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { queryMinT int64 queryMaxT int64 firstInOrderSampleAt int64 - inputSamples chunks.SampleSlice + inputSamples []tsValue expChunkError bool expChunksSamples []chunks.SampleSlice }{ @@ -493,9 +509,9 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - inputSamples: chunks.SampleSlice{ - sample{t: minutes(30), f: float64(0)}, - sample{t: minutes(40), f: float64(0)}, + inputSamples: []tsValue{ + {Ts: minutes(30), V: 0}, + {Ts: minutes(40), V: 0}, }, expChunkError: false, // ts (in minutes) 0 10 20 30 40 50 60 70 80 90 100 @@ -504,8 +520,8 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { // Output Graphically [--------] (With 2 samples) expChunksSamples: []chunks.SampleSlice{ { - sample{t: minutes(30), f: float64(0)}, - sample{t: minutes(40), f: float64(0)}, + scenario.sampleFunc(minutes(30), 0), + scenario.sampleFunc(minutes(40), 0), }, }, }, @@ -514,19 +530,8 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - inputSamples: chunks.SampleSlice{ - // opts.OOOCapMax is 5 so these will be mmapped to the first mmapped chunk - sample{t: minutes(41), f: float64(0)}, - sample{t: minutes(42), f: float64(0)}, - sample{t: minutes(43), f: float64(0)}, - sample{t: minutes(44), f: float64(0)}, - sample{t: minutes(45), f: float64(0)}, - // The following samples will go to the head chunk, and we want it - // to overlap with the previous chunk - sample{t: minutes(30), f: float64(1)}, - sample{t: minutes(50), f: float64(1)}, - }, - expChunkError: false, + inputSamples: []tsValue{{Ts: minutes(41), V: 0}, {Ts: minutes(42), V: 0}, {Ts: minutes(43), V: 0}, {Ts: minutes(44), V: 0}, {Ts: minutes(45), V: 0}, {Ts: minutes(30), V: 1}, {Ts: minutes(50), V: 1}}, + expChunkError: false, // ts (in minutes) 0 10 20 30 40 50 60 70 80 90 100 // Query Interval [------------------------------------------------------------------------------------------] // Chunk 0 [---] (With 5 samples) @@ -534,13 +539,13 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { // Output Graphically [-----------------] (With 7 samples) expChunksSamples: []chunks.SampleSlice{ { - sample{t: minutes(30), f: float64(1)}, - sample{t: minutes(41), f: float64(0)}, - sample{t: minutes(42), f: float64(0)}, - sample{t: minutes(43), f: float64(0)}, - sample{t: minutes(44), f: float64(0)}, - sample{t: minutes(45), f: float64(0)}, - sample{t: minutes(50), f: float64(1)}, + scenario.sampleFunc(minutes(30), 1), + scenario.sampleFunc(minutes(41), 0), + scenario.sampleFunc(minutes(42), 0), + scenario.sampleFunc(minutes(43), 0), + scenario.sampleFunc(minutes(44), 0), + scenario.sampleFunc(minutes(45), 0), + scenario.sampleFunc(minutes(50), 1), }, }, }, @@ -549,28 +554,28 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - inputSamples: chunks.SampleSlice{ + inputSamples: []tsValue{ // Chunk 0 - sample{t: minutes(10), f: float64(0)}, - sample{t: minutes(12), f: float64(0)}, - sample{t: minutes(14), f: float64(0)}, - sample{t: minutes(16), f: float64(0)}, - sample{t: minutes(20), f: float64(0)}, + {Ts: minutes(10), V: 0}, + {Ts: minutes(12), V: 0}, + {Ts: minutes(14), V: 0}, + {Ts: minutes(16), V: 0}, + {Ts: minutes(20), V: 0}, // Chunk 1 - sample{t: minutes(20), f: float64(1)}, - sample{t: minutes(22), f: float64(1)}, - sample{t: minutes(24), f: float64(1)}, - sample{t: minutes(26), f: float64(1)}, - sample{t: minutes(29), f: float64(1)}, - // Chunk 2 - sample{t: minutes(30), f: float64(2)}, - sample{t: minutes(32), f: float64(2)}, - sample{t: minutes(34), f: float64(2)}, - sample{t: minutes(36), f: float64(2)}, - sample{t: minutes(40), f: float64(2)}, + {Ts: minutes(20), V: 1}, + {Ts: minutes(22), V: 1}, + {Ts: minutes(24), V: 1}, + {Ts: minutes(26), V: 1}, + {Ts: minutes(29), V: 1}, + // Chunk 3 + {Ts: minutes(30), V: 2}, + {Ts: minutes(32), V: 2}, + {Ts: minutes(34), V: 2}, + {Ts: minutes(36), V: 2}, + {Ts: minutes(40), V: 2}, // Head - sample{t: minutes(40), f: float64(3)}, - sample{t: minutes(50), f: float64(3)}, + {Ts: minutes(40), V: 3}, + {Ts: minutes(50), V: 3}, }, expChunkError: false, // ts (in minutes) 0 10 20 30 40 50 60 70 80 90 100 @@ -582,23 +587,23 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { // Output Graphically [----------------][-----------------] expChunksSamples: []chunks.SampleSlice{ { - sample{t: minutes(10), f: float64(0)}, - sample{t: minutes(12), f: float64(0)}, - sample{t: minutes(14), f: float64(0)}, - sample{t: minutes(16), f: float64(0)}, - sample{t: minutes(20), f: float64(1)}, - sample{t: minutes(22), f: float64(1)}, - sample{t: minutes(24), f: float64(1)}, - sample{t: minutes(26), f: float64(1)}, - sample{t: minutes(29), f: float64(1)}, + scenario.sampleFunc(minutes(10), 0), + scenario.sampleFunc(minutes(12), 0), + scenario.sampleFunc(minutes(14), 0), + scenario.sampleFunc(minutes(16), 0), + scenario.sampleFunc(minutes(20), 1), + scenario.sampleFunc(minutes(22), 1), + scenario.sampleFunc(minutes(24), 1), + scenario.sampleFunc(minutes(26), 1), + scenario.sampleFunc(minutes(29), 1), }, { - sample{t: minutes(30), f: float64(2)}, - sample{t: minutes(32), f: float64(2)}, - sample{t: minutes(34), f: float64(2)}, - sample{t: minutes(36), f: float64(2)}, - sample{t: minutes(40), f: float64(3)}, - sample{t: minutes(50), f: float64(3)}, + scenario.sampleFunc(minutes(30), 2), + scenario.sampleFunc(minutes(32), 2), + scenario.sampleFunc(minutes(34), 2), + scenario.sampleFunc(minutes(36), 2), + scenario.sampleFunc(minutes(40), 3), + scenario.sampleFunc(minutes(50), 3), }, }, }, @@ -607,28 +612,28 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - inputSamples: chunks.SampleSlice{ + inputSamples: []tsValue{ // Chunk 0 - sample{t: minutes(40), f: float64(0)}, - sample{t: minutes(42), f: float64(0)}, - sample{t: minutes(44), f: float64(0)}, - sample{t: minutes(46), f: float64(0)}, - sample{t: minutes(50), f: float64(0)}, + {Ts: minutes(40), V: 0}, + {Ts: minutes(42), V: 0}, + {Ts: minutes(44), V: 0}, + {Ts: minutes(46), V: 0}, + {Ts: minutes(50), V: 0}, // Chunk 1 - sample{t: minutes(30), f: float64(1)}, - sample{t: minutes(32), f: float64(1)}, - sample{t: minutes(34), f: float64(1)}, - sample{t: minutes(36), f: float64(1)}, - sample{t: minutes(40), f: float64(1)}, - // Chunk 2 - sample{t: minutes(20), f: float64(2)}, - sample{t: minutes(22), f: float64(2)}, - sample{t: minutes(24), f: float64(2)}, - sample{t: minutes(26), f: float64(2)}, - sample{t: minutes(29), f: float64(2)}, + {Ts: minutes(30), V: 1}, + {Ts: minutes(32), V: 1}, + {Ts: minutes(34), V: 1}, + {Ts: minutes(36), V: 1}, + {Ts: minutes(40), V: 1}, + // Chunk 3 + {Ts: minutes(20), V: 2}, + {Ts: minutes(22), V: 2}, + {Ts: minutes(24), V: 2}, + {Ts: minutes(26), V: 2}, + {Ts: minutes(29), V: 2}, // Head - sample{t: minutes(10), f: float64(3)}, - sample{t: minutes(20), f: float64(3)}, + {Ts: minutes(10), V: 3}, + {Ts: minutes(20), V: 3}, }, expChunkError: false, // ts (in minutes) 0 10 20 30 40 50 60 70 80 90 100 @@ -640,23 +645,23 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { // Output Graphically [----------------][-----------------] expChunksSamples: []chunks.SampleSlice{ { - sample{t: minutes(10), f: float64(3)}, - sample{t: minutes(20), f: float64(2)}, - sample{t: minutes(22), f: float64(2)}, - sample{t: minutes(24), f: float64(2)}, - sample{t: minutes(26), f: float64(2)}, - sample{t: minutes(29), f: float64(2)}, + scenario.sampleFunc(minutes(10), 3), + scenario.sampleFunc(minutes(20), 2), + scenario.sampleFunc(minutes(22), 2), + scenario.sampleFunc(minutes(24), 2), + scenario.sampleFunc(minutes(26), 2), + scenario.sampleFunc(minutes(29), 2), }, { - sample{t: minutes(30), f: float64(1)}, - sample{t: minutes(32), f: float64(1)}, - sample{t: minutes(34), f: float64(1)}, - sample{t: minutes(36), f: float64(1)}, - sample{t: minutes(40), f: float64(0)}, - sample{t: minutes(42), f: float64(0)}, - sample{t: minutes(44), f: float64(0)}, - sample{t: minutes(46), f: float64(0)}, - sample{t: minutes(50), f: float64(0)}, + scenario.sampleFunc(minutes(30), 1), + scenario.sampleFunc(minutes(32), 1), + scenario.sampleFunc(minutes(34), 1), + scenario.sampleFunc(minutes(36), 1), + scenario.sampleFunc(minutes(40), 0), + scenario.sampleFunc(minutes(42), 0), + scenario.sampleFunc(minutes(44), 0), + scenario.sampleFunc(minutes(46), 0), + scenario.sampleFunc(minutes(50), 0), }, }, }, @@ -665,28 +670,28 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - inputSamples: chunks.SampleSlice{ + inputSamples: []tsValue{ // Chunk 0 - sample{t: minutes(10), f: float64(0)}, - sample{t: minutes(12), f: float64(0)}, - sample{t: minutes(14), f: float64(0)}, - sample{t: minutes(16), f: float64(0)}, - sample{t: minutes(18), f: float64(0)}, + {Ts: minutes(10), V: 0}, + {Ts: minutes(12), V: 0}, + {Ts: minutes(14), V: 0}, + {Ts: minutes(16), V: 0}, + {Ts: minutes(18), V: 0}, // Chunk 1 - sample{t: minutes(20), f: float64(1)}, - sample{t: minutes(22), f: float64(1)}, - sample{t: minutes(24), f: float64(1)}, - sample{t: minutes(26), f: float64(1)}, - sample{t: minutes(28), f: float64(1)}, - // Chunk 2 - sample{t: minutes(30), f: float64(2)}, - sample{t: minutes(32), f: float64(2)}, - sample{t: minutes(34), f: float64(2)}, - sample{t: minutes(36), f: float64(2)}, - sample{t: minutes(38), f: float64(2)}, + {Ts: minutes(20), V: 1}, + {Ts: minutes(22), V: 1}, + {Ts: minutes(24), V: 1}, + {Ts: minutes(26), V: 1}, + {Ts: minutes(28), V: 1}, + // Chunk 3 + {Ts: minutes(30), V: 2}, + {Ts: minutes(32), V: 2}, + {Ts: minutes(34), V: 2}, + {Ts: minutes(36), V: 2}, + {Ts: minutes(38), V: 2}, // Head - sample{t: minutes(40), f: float64(3)}, - sample{t: minutes(42), f: float64(3)}, + {Ts: minutes(40), V: 3}, + {Ts: minutes(42), V: 3}, }, expChunkError: false, // ts (in minutes) 0 10 20 30 40 50 60 70 80 90 100 @@ -698,29 +703,29 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { // Output Graphically [-------][-------][-------][--------] expChunksSamples: []chunks.SampleSlice{ { - sample{t: minutes(10), f: float64(0)}, - sample{t: minutes(12), f: float64(0)}, - sample{t: minutes(14), f: float64(0)}, - sample{t: minutes(16), f: float64(0)}, - sample{t: minutes(18), f: float64(0)}, + scenario.sampleFunc(minutes(10), 0), + scenario.sampleFunc(minutes(12), 0), + scenario.sampleFunc(minutes(14), 0), + scenario.sampleFunc(minutes(16), 0), + scenario.sampleFunc(minutes(18), 0), }, { - sample{t: minutes(20), f: float64(1)}, - sample{t: minutes(22), f: float64(1)}, - sample{t: minutes(24), f: float64(1)}, - sample{t: minutes(26), f: float64(1)}, - sample{t: minutes(28), f: float64(1)}, + scenario.sampleFunc(minutes(20), 1), + scenario.sampleFunc(minutes(22), 1), + scenario.sampleFunc(minutes(24), 1), + scenario.sampleFunc(minutes(26), 1), + scenario.sampleFunc(minutes(28), 1), }, { - sample{t: minutes(30), f: float64(2)}, - sample{t: minutes(32), f: float64(2)}, - sample{t: minutes(34), f: float64(2)}, - sample{t: minutes(36), f: float64(2)}, - sample{t: minutes(38), f: float64(2)}, + scenario.sampleFunc(minutes(30), 2), + scenario.sampleFunc(minutes(32), 2), + scenario.sampleFunc(minutes(34), 2), + scenario.sampleFunc(minutes(36), 2), + scenario.sampleFunc(minutes(38), 2), }, { - sample{t: minutes(40), f: float64(3)}, - sample{t: minutes(42), f: float64(3)}, + scenario.sampleFunc(minutes(40), 3), + scenario.sampleFunc(minutes(42), 3), }, }, }, @@ -729,22 +734,22 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - inputSamples: chunks.SampleSlice{ + inputSamples: []tsValue{ // Chunk 0 - sample{t: minutes(10), f: float64(0)}, - sample{t: minutes(15), f: float64(0)}, - sample{t: minutes(20), f: float64(0)}, - sample{t: minutes(25), f: float64(0)}, - sample{t: minutes(30), f: float64(0)}, + {Ts: minutes(10), V: 0}, + {Ts: minutes(15), V: 0}, + {Ts: minutes(20), V: 0}, + {Ts: minutes(25), V: 0}, + {Ts: minutes(30), V: 0}, // Chunk 1 - sample{t: minutes(20), f: float64(1)}, - sample{t: minutes(25), f: float64(1)}, - sample{t: minutes(30), f: float64(1)}, - sample{t: minutes(35), f: float64(1)}, - sample{t: minutes(42), f: float64(1)}, + {Ts: minutes(20), V: 1}, + {Ts: minutes(25), V: 1}, + {Ts: minutes(30), V: 1}, + {Ts: minutes(35), V: 1}, + {Ts: minutes(42), V: 1}, // Chunk 2 Head - sample{t: minutes(32), f: float64(2)}, - sample{t: minutes(50), f: float64(2)}, + {Ts: minutes(32), V: 2}, + {Ts: minutes(50), V: 2}, }, expChunkError: false, // ts (in minutes) 0 10 20 30 40 50 60 70 80 90 100 @@ -755,15 +760,15 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { // Output Graphically [-----------------------------------] expChunksSamples: []chunks.SampleSlice{ { - sample{t: minutes(10), f: float64(0)}, - sample{t: minutes(15), f: float64(0)}, - sample{t: minutes(20), f: float64(1)}, - sample{t: minutes(25), f: float64(1)}, - sample{t: minutes(30), f: float64(1)}, - sample{t: minutes(32), f: float64(2)}, - sample{t: minutes(35), f: float64(1)}, - sample{t: minutes(42), f: float64(1)}, - sample{t: minutes(50), f: float64(2)}, + scenario.sampleFunc(minutes(10), 0), + scenario.sampleFunc(minutes(15), 0), + scenario.sampleFunc(minutes(20), 1), + scenario.sampleFunc(minutes(25), 1), + scenario.sampleFunc(minutes(30), 1), + scenario.sampleFunc(minutes(32), 2), + scenario.sampleFunc(minutes(35), 1), + scenario.sampleFunc(minutes(42), 1), + scenario.sampleFunc(minutes(50), 2), }, }, }, @@ -772,22 +777,22 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { queryMinT: minutes(12), queryMaxT: minutes(33), firstInOrderSampleAt: minutes(120), - inputSamples: chunks.SampleSlice{ + inputSamples: []tsValue{ // Chunk 0 - sample{t: minutes(10), f: float64(0)}, - sample{t: minutes(15), f: float64(0)}, - sample{t: minutes(20), f: float64(0)}, - sample{t: minutes(25), f: float64(0)}, - sample{t: minutes(30), f: float64(0)}, + {Ts: minutes(10), V: 0}, + {Ts: minutes(15), V: 0}, + {Ts: minutes(20), V: 0}, + {Ts: minutes(25), V: 0}, + {Ts: minutes(30), V: 0}, // Chunk 1 - sample{t: minutes(20), f: float64(1)}, - sample{t: minutes(25), f: float64(1)}, - sample{t: minutes(30), f: float64(1)}, - sample{t: minutes(35), f: float64(1)}, - sample{t: minutes(42), f: float64(1)}, + {Ts: minutes(20), V: 1}, + {Ts: minutes(25), V: 1}, + {Ts: minutes(30), V: 1}, + {Ts: minutes(35), V: 1}, + {Ts: minutes(42), V: 1}, // Chunk 2 Head - sample{t: minutes(32), f: float64(2)}, - sample{t: minutes(50), f: float64(2)}, + {Ts: minutes(32), V: 2}, + {Ts: minutes(50), V: 2}, }, expChunkError: false, // ts (in minutes) 0 10 20 30 40 50 60 70 80 90 100 @@ -798,15 +803,15 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { // Output Graphically [-----------------------------------] expChunksSamples: []chunks.SampleSlice{ { - sample{t: minutes(10), f: float64(0)}, - sample{t: minutes(15), f: float64(0)}, - sample{t: minutes(20), f: float64(1)}, - sample{t: minutes(25), f: float64(1)}, - sample{t: minutes(30), f: float64(1)}, - sample{t: minutes(32), f: float64(2)}, - sample{t: minutes(35), f: float64(1)}, - sample{t: minutes(42), f: float64(1)}, - sample{t: minutes(50), f: float64(2)}, + scenario.sampleFunc(minutes(10), 0), + scenario.sampleFunc(minutes(15), 0), + scenario.sampleFunc(minutes(20), 1), + scenario.sampleFunc(minutes(25), 1), + scenario.sampleFunc(minutes(30), 1), + scenario.sampleFunc(minutes(32), 2), + scenario.sampleFunc(minutes(35), 1), + scenario.sampleFunc(minutes(42), 1), + scenario.sampleFunc(minutes(50), 2), }, }, }, @@ -817,13 +822,14 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { db := newTestDBWithOpts(t, opts) app := db.Appender(context.Background()) - s1Ref := appendSample(app, s1, tc.firstInOrderSampleAt, float64(tc.firstInOrderSampleAt/1*time.Minute.Milliseconds())) + s1Ref, _, _ := scenario.appendFunc(app, s1, tc.firstInOrderSampleAt, tc.firstInOrderSampleAt/1*time.Minute.Milliseconds()) require.NoError(t, app.Commit()) // OOO few samples for s1. app = db.Appender(context.Background()) for _, s := range tc.inputSamples { - appendSample(app, s1, s.T(), s.F()) + _, _, err := scenario.appendFunc(app, s1, s.Ts, s.V) + require.NoError(t, err) } require.NoError(t, app.Commit()) @@ -843,13 +849,9 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { require.NoError(t, err) require.Nil(t, c) - var resultSamples chunks.SampleSlice it := iterable.Iterator(nil) - for it.Next() == chunkenc.ValFloat { - t, v := it.At() - resultSamples = append(resultSamples, sample{t: t, f: v}) - } - require.Equal(t, tc.expChunksSamples[i], resultSamples) + resultSamples := samplesFromIterator(t, it) + compareSamples(t, s1.String(), tc.expChunksSamples[i], resultSamples, true) } }) } @@ -864,6 +866,24 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { // - Response B comes from : Series(), in parallel new samples added to the head, then Chunk() // - A == B func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding(t *testing.T) { + for name, scenario := range sampleTypeScenarios { + t.Run(name, func(t *testing.T) { + testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding(t, scenario) + }) + } +} + +// TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding tests +// that if a query comes and performs a Series() call followed by a Chunks() call +// the response is consistent with the data seen by Series() even if the OOO +// head receives more samples before Chunks() is called. +// An example: +// - Response A comes from: Series() then Chunk() +// - Response B comes from : Series(), in parallel new samples added to the head, then Chunk() +// - A == B +// +//nolint:revive // unexported-return. +func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding(t *testing.T, scenario sampleTypeScenario) { opts := DefaultOptions() opts.OutOfOrderCapMax = 5 opts.OutOfOrderTimeWindow = 120 * time.Minute.Milliseconds() @@ -871,19 +891,13 @@ func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( s1 := labels.FromStrings("l", "v1") minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() } - appendSample := func(app storage.Appender, l labels.Labels, timestamp int64, value float64) storage.SeriesRef { - ref, err := app.Append(0, l, timestamp, value) - require.NoError(t, err) - return ref - } - tests := []struct { name string queryMinT int64 queryMaxT int64 firstInOrderSampleAt int64 - initialSamples chunks.SampleSlice - samplesAfterSeriesCall chunks.SampleSlice + initialSamples []tsValue + samplesAfterSeriesCall []tsValue expChunkError bool expChunksSamples []chunks.SampleSlice }{ @@ -892,21 +906,21 @@ func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - initialSamples: chunks.SampleSlice{ + initialSamples: []tsValue{ // Chunk 0 - sample{t: minutes(20), f: float64(0)}, - sample{t: minutes(22), f: float64(0)}, - sample{t: minutes(24), f: float64(0)}, - sample{t: minutes(26), f: float64(0)}, - sample{t: minutes(30), f: float64(0)}, + {Ts: minutes(20), V: 0}, + {Ts: minutes(22), V: 0}, + {Ts: minutes(24), V: 0}, + {Ts: minutes(26), V: 0}, + {Ts: minutes(30), V: 0}, // Chunk 1 Head - sample{t: minutes(25), f: float64(1)}, - sample{t: minutes(35), f: float64(1)}, + {Ts: minutes(25), V: 1}, + {Ts: minutes(35), V: 1}, }, - samplesAfterSeriesCall: chunks.SampleSlice{ - sample{t: minutes(10), f: float64(1)}, - sample{t: minutes(32), f: float64(1)}, - sample{t: minutes(50), f: float64(1)}, + samplesAfterSeriesCall: []tsValue{ + {Ts: minutes(10), V: 1}, + {Ts: minutes(32), V: 1}, + {Ts: minutes(50), V: 1}, }, expChunkError: false, // ts (in minutes) 0 10 20 30 40 50 60 70 80 90 100 @@ -918,39 +932,40 @@ func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( // Output Graphically [------------] (With 8 samples, samples newer than lastmint or older than lastmaxt are omitted but the ones in between are kept) expChunksSamples: []chunks.SampleSlice{ { - sample{t: minutes(20), f: float64(0)}, - sample{t: minutes(22), f: float64(0)}, - sample{t: minutes(24), f: float64(0)}, - sample{t: minutes(25), f: float64(1)}, - sample{t: minutes(26), f: float64(0)}, - sample{t: minutes(30), f: float64(0)}, - sample{t: minutes(35), f: float64(1)}, + scenario.sampleFunc(minutes(20), 0), + scenario.sampleFunc(minutes(22), 0), + scenario.sampleFunc(minutes(24), 0), + scenario.sampleFunc(minutes(25), 1), + scenario.sampleFunc(minutes(26), 0), + scenario.sampleFunc(minutes(30), 0), + scenario.sampleFunc(minutes(32), 1), // This sample was added after Series() but before Chunk() and its in between the lastmint and maxt so it should be kept + scenario.sampleFunc(minutes(35), 1), }, }, }, { - name: "After Series() previous head gets mmapped after getting samples, new head gets new samples also overlapping, none of these should appear in the response.", + name: "After Series() prev head gets mmapped after getting samples, new head gets new samples also overlapping, none of these should appear in response.", queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - initialSamples: chunks.SampleSlice{ + initialSamples: []tsValue{ // Chunk 0 - sample{t: minutes(20), f: float64(0)}, - sample{t: minutes(22), f: float64(0)}, - sample{t: minutes(24), f: float64(0)}, - sample{t: minutes(26), f: float64(0)}, - sample{t: minutes(30), f: float64(0)}, + {Ts: minutes(20), V: 0}, + {Ts: minutes(22), V: 0}, + {Ts: minutes(24), V: 0}, + {Ts: minutes(26), V: 0}, + {Ts: minutes(30), V: 0}, // Chunk 1 Head - sample{t: minutes(25), f: float64(1)}, - sample{t: minutes(35), f: float64(1)}, + {Ts: minutes(25), V: 1}, + {Ts: minutes(35), V: 1}, }, - samplesAfterSeriesCall: chunks.SampleSlice{ - sample{t: minutes(10), f: float64(1)}, - sample{t: minutes(32), f: float64(1)}, - sample{t: minutes(50), f: float64(1)}, + samplesAfterSeriesCall: []tsValue{ + {Ts: minutes(10), V: 1}, + {Ts: minutes(32), V: 1}, + {Ts: minutes(50), V: 1}, // Chunk 1 gets mmapped and Chunk 2, the new head is born - sample{t: minutes(25), f: float64(2)}, - sample{t: minutes(31), f: float64(2)}, + {Ts: minutes(25), V: 2}, + {Ts: minutes(31), V: 2}, }, expChunkError: false, // ts (in minutes) 0 10 20 30 40 50 60 70 80 90 100 @@ -963,6 +978,7 @@ func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( // Output Graphically [------------] (8 samples) It has 5 from Chunk 0 and 3 from Chunk 1 expChunksSamples: []chunks.SampleSlice{ { +<<<<<<< HEAD sample{t: minutes(20), f: float64(0)}, sample{t: minutes(22), f: float64(0)}, sample{t: minutes(24), f: float64(0)}, @@ -970,6 +986,25 @@ func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( sample{t: minutes(26), f: float64(0)}, sample{t: minutes(30), f: float64(0)}, sample{t: minutes(35), f: float64(1)}, +||||||| parent of 2795db1c2 (Update tsdb tests to use test utils.) + sample{t: minutes(20), f: float64(0)}, + sample{t: minutes(22), f: float64(0)}, + sample{t: minutes(24), f: float64(0)}, + sample{t: minutes(25), f: float64(1)}, + sample{t: minutes(26), f: float64(0)}, + sample{t: minutes(30), f: float64(0)}, + sample{t: minutes(32), f: float64(1)}, // This sample was added after Series() but before Chunk() and its in between the lastmint and maxt so it should be kept + sample{t: minutes(35), f: float64(1)}, +======= + scenario.sampleFunc(minutes(20), 0), + scenario.sampleFunc(minutes(22), 0), + scenario.sampleFunc(minutes(24), 0), + scenario.sampleFunc(minutes(25), 1), + scenario.sampleFunc(minutes(26), 0), + scenario.sampleFunc(minutes(30), 0), + scenario.sampleFunc(minutes(32), 1), // This sample was added after Series() but before Chunk() and its in between the lastmint and maxt so it should be kept + scenario.sampleFunc(minutes(35), 1), +>>>>>>> 2795db1c2 (Update tsdb tests to use test utils.) }, }, }, @@ -980,13 +1015,15 @@ func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( db := newTestDBWithOpts(t, opts) app := db.Appender(context.Background()) - s1Ref := appendSample(app, s1, tc.firstInOrderSampleAt, float64(tc.firstInOrderSampleAt/1*time.Minute.Milliseconds())) + s1Ref, _, err := scenario.appendFunc(app, s1, tc.firstInOrderSampleAt, tc.firstInOrderSampleAt/1*time.Minute.Milliseconds()) + require.NoError(t, err) require.NoError(t, app.Commit()) // OOO few samples for s1. app = db.Appender(context.Background()) for _, s := range tc.initialSamples { - appendSample(app, s1, s.T(), s.F()) + _, _, err := scenario.appendFunc(app, s1, s.Ts, s.V) + require.NoError(t, err) } require.NoError(t, app.Commit()) @@ -995,7 +1032,7 @@ func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( ir := NewOOOHeadIndexReader(db.head, tc.queryMinT, tc.queryMaxT, 0) var chks []chunks.Meta var b labels.ScratchBuilder - err := ir.Series(s1Ref, &b, &chks) + err = ir.Series(s1Ref, &b, &chks) require.NoError(t, err) require.Equal(t, len(tc.expChunksSamples), len(chks)) @@ -1003,7 +1040,8 @@ func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( // OOO few samples for s1. app = db.Appender(context.Background()) for _, s := range tc.samplesAfterSeriesCall { - appendSample(app, s1, s.T(), s.F()) + _, _, err = scenario.appendFunc(app, s1, s.Ts, s.V) + require.NoError(t, err) } require.NoError(t, app.Commit()) @@ -1014,13 +1052,9 @@ func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( require.NoError(t, err) require.Nil(t, c) - var resultSamples chunks.SampleSlice it := iterable.Iterator(nil) - for it.Next() == chunkenc.ValFloat { - ts, v := it.At() - resultSamples = append(resultSamples, sample{t: ts, f: v}) - } - require.Equal(t, tc.expChunksSamples[i], resultSamples) + resultSamples := samplesFromIterator(t, it) + compareSamples(t, s1.String(), tc.expChunksSamples[i], resultSamples, true) } }) } From cb76884352304d791e11afd2b0ea6751a21a0300 Mon Sep 17 00:00:00 2001 From: Carrie Edwards Date: Mon, 4 Mar 2024 07:58:29 -0800 Subject: [PATCH 10/29] Fix linting Co-authored by: Fiona Liao : Signed-off-by: Carrie Edwards --- tsdb/testutil.go | 91 ++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/tsdb/testutil.go b/tsdb/testutil.go index 0a26b39661..0412cc2c2a 100644 --- a/tsdb/testutil.go +++ b/tsdb/testutil.go @@ -14,9 +14,10 @@ package tsdb import ( - prom_testutil "github.com/prometheus/client_golang/prometheus/testutil" "testing" + prom_testutil "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/prometheus/prometheus/tsdb/chunkenc" "github.com/stretchr/testify/require" @@ -55,50 +56,50 @@ var sampleTypeScenarios = map[string]sampleTypeScenario{ return sample{t: ts, f: float64(value)} }, }, - //intHistogram: { - // sampleType: sampleMetricTypeHistogram, - // appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) { - // s := sample{t: ts, h: tsdbutil.GenerateTestHistogram(int(value))} - // ref, err := appender.AppendHistogram(0, lbls, ts, s.h, nil) - // return ref, s, err - // }, - // sampleFunc: func(ts, value int64) sample { - // return sample{t: ts, h: tsdbutil.GenerateTestHistogram(int(value))} - // }, - //}, - //floatHistogram: { - // sampleType: sampleMetricTypeHistogram, - // appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) { - // s := sample{t: ts, fh: tsdbutil.GenerateTestFloatHistogram(int(value))} - // ref, err := appender.AppendHistogram(0, lbls, ts, nil, s.fh) - // return ref, s, err - // }, - // sampleFunc: func(ts, value int64) sample { - // return sample{t: ts, fh: tsdbutil.GenerateTestFloatHistogram(int(value))} - // }, - //}, - //gaugeIntHistogram: { - // sampleType: sampleMetricTypeHistogram, - // appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) { - // s := sample{t: ts, h: tsdbutil.GenerateTestGaugeHistogram(int(value))} - // ref, err := appender.AppendHistogram(0, lbls, ts, s.h, nil) - // return ref, s, err - // }, - // sampleFunc: func(ts, value int64) sample { - // return sample{t: ts, h: tsdbutil.GenerateTestGaugeHistogram(int(value))} - // }, - //}, - //gaugeFloatHistogram: { - // sampleType: sampleMetricTypeHistogram, - // appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) { - // s := sample{t: ts, fh: tsdbutil.GenerateTestGaugeFloatHistogram(int(value))} - // ref, err := appender.AppendHistogram(0, lbls, ts, nil, s.fh) - // return ref, s, err - // }, - // sampleFunc: func(ts, value int64) sample { - // return sample{t: ts, fh: tsdbutil.GenerateTestGaugeFloatHistogram(int(value))} - // }, - //}, + // intHistogram: { + // sampleType: sampleMetricTypeHistogram, + // appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) { + // s := sample{t: ts, h: tsdbutil.GenerateTestHistogram(int(value))} + // ref, err := appender.AppendHistogram(0, lbls, ts, s.h, nil) + // return ref, s, err + // }, + // sampleFunc: func(ts, value int64) sample { + // return sample{t: ts, h: tsdbutil.GenerateTestHistogram(int(value))} + // }, + // }, + // floatHistogram: { + // sampleType: sampleMetricTypeHistogram, + // appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) { + // s := sample{t: ts, fh: tsdbutil.GenerateTestFloatHistogram(int(value))} + // ref, err := appender.AppendHistogram(0, lbls, ts, nil, s.fh) + // return ref, s, err + // }, + // sampleFunc: func(ts, value int64) sample { + // return sample{t: ts, fh: tsdbutil.GenerateTestFloatHistogram(int(value))} + // }, + // }, + // gaugeIntHistogram: { + // sampleType: sampleMetricTypeHistogram, + // appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) { + // s := sample{t: ts, h: tsdbutil.GenerateTestGaugeHistogram(int(value))} + // ref, err := appender.AppendHistogram(0, lbls, ts, s.h, nil) + // return ref, s, err + // }, + // sampleFunc: func(ts, value int64) sample { + // return sample{t: ts, h: tsdbutil.GenerateTestGaugeHistogram(int(value))} + // }, + // }, + // gaugeFloatHistogram: { + // sampleType: sampleMetricTypeHistogram, + // appendFunc: func(appender storage.Appender, lbls labels.Labels, ts, value int64) (storage.SeriesRef, sample, error) { + // s := sample{t: ts, fh: tsdbutil.GenerateTestGaugeFloatHistogram(int(value))} + // ref, err := appender.AppendHistogram(0, lbls, ts, nil, s.fh) + // return ref, s, err + // }, + // sampleFunc: func(ts, value int64) sample { + // return sample{t: ts, fh: tsdbutil.GenerateTestGaugeFloatHistogram(int(value))} + // }, + // }, } // requireEqualSamples checks that the actual series are equal to the expected ones. It ignores the counter reset hints for histograms. From 06550883c168bc839cdb13dee0ce2e099d8fd2ae Mon Sep 17 00:00:00 2001 From: Carrie Edwards Date: Thu, 7 Mar 2024 09:41:03 -0800 Subject: [PATCH 11/29] Clean up of tests and test utils Co-authored by: Fiona Liao : Signed-off-by: Carrie Edwards --- tsdb/db_test.go | 26 +++++++++++++------------- tsdb/head_test.go | 2 +- tsdb/ooo_head_read_test.go | 22 +++++----------------- tsdb/testutil.go | 13 +++++++------ 4 files changed, 26 insertions(+), 37 deletions(-) diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 3d6d55c90c..a9c7dee756 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -4572,7 +4572,7 @@ func testOOOCompaction(t *testing.T, scenario sampleTypeScenario) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - requireEqualSamples(t, expRes, actRes, true) + requireEqualSeries(t, expRes, actRes, true) } verifyDBSamples() // Before any compaction. @@ -4639,7 +4639,7 @@ func testOOOCompaction(t *testing.T, scenario sampleTypeScenario) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - requireEqualSamples(t, expRes, actRes, true) + requireEqualSeries(t, expRes, actRes, true) } // Checking for expected data in the blocks. @@ -4779,7 +4779,7 @@ func testOOOCompactionWithNormalCompaction(t *testing.T, scenario sampleTypeScen require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - requireEqualSamples(t, expRes, actRes, true) + requireEqualSeries(t, expRes, actRes, true) } // Checking for expected data in the blocks. @@ -4888,7 +4888,7 @@ func testOOOCompactionWithDisabledWriteLog(t *testing.T, scenario sampleTypeScen require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - requireEqualSamples(t, expRes, actRes, true) + requireEqualSeries(t, expRes, actRes, true) } // Checking for expected data in the blocks. @@ -4990,7 +4990,7 @@ func testOOOQueryAfterRestartWithSnapshotAndRemovedWBL(t *testing.T, scenario sa require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - requireEqualSamples(t, expRes, actRes, true) + requireEqualSeries(t, expRes, actRes, true) } // Checking for expected ooo data from mmap chunks. @@ -5262,7 +5262,7 @@ func testOOOAppendAndQuery(t *testing.T, scenario sampleTypeScenario) { expSamples[k] = append(expSamples[k], s) } } - requireEqualSamples(t, expSamples, seriesSet, true) + requireEqualSeries(t, expSamples, seriesSet, true) requireEqualOOOSamples(t, totalSamples-2, db) } @@ -5379,7 +5379,7 @@ func testOOODisabled(t *testing.T, scenario sampleTypeScenario) { require.NoError(t, err) seriesSet := query(t, querier, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.")) - requireEqualSamples(t, expSamples, seriesSet, true) + requireEqualSeries(t, expSamples, seriesSet, true) requireEqualOOOSamples(t, 0, db) require.Equal(t, float64(failedSamples), prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamples.WithLabelValues(scenario.sampleType))+prom_testutil.ToFloat64(db.head.metrics.outOfBoundSamples.WithLabelValues(scenario.sampleType)), @@ -5446,7 +5446,7 @@ func testWBLAndMmapReplay(t *testing.T, scenario sampleTypeScenario) { }) exp[k] = v } - requireEqualSamples(t, exp, seriesSet, true) + requireEqualSeries(t, exp, seriesSet, true) } // In-order samples. @@ -5712,7 +5712,7 @@ func testOOOCompactionFailure(t *testing.T, scenario sampleTypeScenario) { q, err := NewBlockQuerier(block, math.MinInt64, math.MaxInt64) require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - requireEqualSamples(t, expRes, actRes, true) + requireEqualSeries(t, expRes, actRes, true) } // Checking for expected data in the blocks. @@ -5948,7 +5948,7 @@ func testOOOMmapCorruption(t *testing.T, scenario sampleTypeScenario) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - requireEqualSamples(t, expRes, actRes, true) + requireEqualSeries(t, expRes, actRes, true) } verifySamples(allSamples) @@ -6076,7 +6076,7 @@ func testOutOfOrderRuntimeConfig(t *testing.T, scenario sampleTypeScenario) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - requireEqualSamples(t, expRes, actRes, true) + requireEqualSeries(t, expRes, actRes, true) } doOOOCompaction := func(t *testing.T, db *DB) { @@ -6287,7 +6287,7 @@ func testNoGapAfterRestartWithOOO(t *testing.T, scenario sampleTypeScenario) { require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - requireEqualSamples(t, expRes, actRes, true) + requireEqualSeries(t, expRes, actRes, true) } cases := []struct { @@ -6415,7 +6415,7 @@ func testWblReplayAfterOOODisableAndRestart(t *testing.T, scenario sampleTypeSce require.NoError(t, err) actRes := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) - requireEqualSamples(t, expRes, actRes, true) + requireEqualSeries(t, expRes, actRes, true) } verifySamples(allSamples) diff --git a/tsdb/head_test.go b/tsdb/head_test.go index 1eb0c0534d..fa48345165 100644 --- a/tsdb/head_test.go +++ b/tsdb/head_test.go @@ -5232,7 +5232,7 @@ func testHeadMinOOOTimeUpdate(t *testing.T, scenario sampleTypeScenario) { appendSample := func(ts int64) { app := h.Appender(context.Background()) - _, _, err = scenario.appendFunc(app, labels.FromStrings("a", "b"), ts*time.Minute.Milliseconds(), 99.0) + _, _, err = scenario.appendFunc(app, labels.FromStrings("a", "b"), ts*time.Minute.Milliseconds(), ts) require.NoError(t, err) require.NoError(t, app.Commit()) } diff --git a/tsdb/ooo_head_read_test.go b/tsdb/ooo_head_read_test.go index 9eaab13dcf..ed9a9f88ce 100644 --- a/tsdb/ooo_head_read_test.go +++ b/tsdb/ooo_head_read_test.go @@ -469,10 +469,6 @@ func TestOOOHeadChunkReader_Chunk(t *testing.T) { } } -// TestOOOHeadChunkReader_Chunk tests that the Chunk method works as expected. -// It does so by appending out of order samples to the db and then initializing -// an OOOHeadChunkReader to read chunks from it. -// //nolint:revive // unexported-return. func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { opts := DefaultOptions() @@ -822,7 +818,8 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { db := newTestDBWithOpts(t, opts) app := db.Appender(context.Background()) - s1Ref, _, _ := scenario.appendFunc(app, s1, tc.firstInOrderSampleAt, tc.firstInOrderSampleAt/1*time.Minute.Milliseconds()) + s1Ref, _, err := scenario.appendFunc(app, s1, tc.firstInOrderSampleAt, tc.firstInOrderSampleAt/1*time.Minute.Milliseconds()) + require.NoError(t, err) require.NoError(t, app.Commit()) // OOO few samples for s1. @@ -838,7 +835,7 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { ir := NewOOOHeadIndexReader(db.head, tc.queryMinT, tc.queryMaxT, 0) var chks []chunks.Meta var b labels.ScratchBuilder - err := ir.Series(s1Ref, &b, &chks) + err = ir.Series(s1Ref, &b, &chks) require.NoError(t, err) require.Equal(t, len(tc.expChunksSamples), len(chks)) @@ -851,7 +848,7 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { it := iterable.Iterator(nil) resultSamples := samplesFromIterator(t, it) - compareSamples(t, s1.String(), tc.expChunksSamples[i], resultSamples, true) + requireEqualSamples(t, s1.String(), tc.expChunksSamples[i], resultSamples, true) } }) } @@ -873,15 +870,6 @@ func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( } } -// TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding tests -// that if a query comes and performs a Series() call followed by a Chunks() call -// the response is consistent with the data seen by Series() even if the OOO -// head receives more samples before Chunks() is called. -// An example: -// - Response A comes from: Series() then Chunk() -// - Response B comes from : Series(), in parallel new samples added to the head, then Chunk() -// - A == B -// //nolint:revive // unexported-return. func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding(t *testing.T, scenario sampleTypeScenario) { opts := DefaultOptions() @@ -1054,7 +1042,7 @@ func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( it := iterable.Iterator(nil) resultSamples := samplesFromIterator(t, it) - compareSamples(t, s1.String(), tc.expChunksSamples[i], resultSamples, true) + requireEqualSamples(t, s1.String(), tc.expChunksSamples[i], resultSamples, true) } }) } diff --git a/tsdb/testutil.go b/tsdb/testutil.go index 0412cc2c2a..3f7df2d3a8 100644 --- a/tsdb/testutil.go +++ b/tsdb/testutil.go @@ -44,6 +44,7 @@ type sampleTypeScenario struct { sampleFunc func(ts, value int64) sample } +// TODO: native histogram sample types will be added as part of out-of-order native histogram support; see #11220. var sampleTypeScenarios = map[string]sampleTypeScenario{ float: { sampleType: sampleMetricTypeFloat, @@ -102,12 +103,12 @@ var sampleTypeScenarios = map[string]sampleTypeScenario{ // }, } -// requireEqualSamples checks that the actual series are equal to the expected ones. It ignores the counter reset hints for histograms. -func requireEqualSamples(t *testing.T, expected, actual map[string][]chunks.Sample, ignoreCounterResets bool) { +// requireEqualSeries checks that the actual series are equal to the expected ones. It ignores the counter reset hints for histograms. +func requireEqualSeries(t *testing.T, expected, actual map[string][]chunks.Sample, ignoreCounterResets bool) { for name, expectedItem := range expected { actualItem, ok := actual[name] require.True(t, ok, "Expected series %s not found", name) - compareSamples(t, name, expectedItem, actualItem, ignoreCounterResets) + requireEqualSamples(t, name, expectedItem, actualItem, ignoreCounterResets) } for name := range actual { _, ok := expected[name] @@ -122,12 +123,12 @@ func requireEqualOOOSamples(t *testing.T, expectedSamples int, db *DB) { "number of ooo appended samples mismatch") } -func compareSamples(t *testing.T, name string, expected, actual []chunks.Sample, ignoreCounterResets bool) { - require.Equal(t, len(expected), len(actual), "Length not expected for %s", name) +func requireEqualSamples(t *testing.T, name string, expected, actual []chunks.Sample, ignoreCounterResets bool) { + require.Equal(t, len(expected), len(actual), "Length not equal to expected for %s", name) for i, s := range expected { expectedSample := s actualSample := actual[i] - require.Equal(t, expectedSample.T(), expectedSample.T(), "Different timestamps for %s[%d]", name, i) + require.Equal(t, expectedSample.T(), actualSample.T(), "Different timestamps for %s[%d]", name, i) require.Equal(t, expectedSample.Type().String(), actualSample.Type().String(), "Different types for %s[%d] at ts %d", name, i, expectedSample.T()) switch { case s.H() != nil: From 55f53330b2118f0ba72d28df8b08a4a0c954f87e Mon Sep 17 00:00:00 2001 From: Carrie Edwards Date: Tue, 25 Jun 2024 11:01:50 -0700 Subject: [PATCH 12/29] Use storage.ExpandSamples instead of samplesFromIterator Co-authored by: Fiona Liao : Signed-off-by: Carrie Edwards --- tsdb/db_test.go | 4 +++- tsdb/ooo_head_read_test.go | 6 ++++-- tsdb/testutil.go | 26 +++----------------------- 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/tsdb/db_test.go b/tsdb/db_test.go index a9c7dee756..770b1a1b48 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -5469,7 +5469,9 @@ func testWBLAndMmapReplay(t *testing.T, scenario sampleTypeScenario) { chk, err := db.head.chunkDiskMapper.Chunk(mc.ref) require.NoError(t, err) it := chk.Iterator(nil) - s1MmapSamples = append(s1MmapSamples, samplesFromIterator(t, it)...) + smpls, err := storage.ExpandSamples(it, newSample) + require.NoError(t, err) + s1MmapSamples = append(s1MmapSamples, smpls...) } require.NotEmpty(t, s1MmapSamples) diff --git a/tsdb/ooo_head_read_test.go b/tsdb/ooo_head_read_test.go index ed9a9f88ce..938f9a7f71 100644 --- a/tsdb/ooo_head_read_test.go +++ b/tsdb/ooo_head_read_test.go @@ -847,7 +847,8 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { require.Nil(t, c) it := iterable.Iterator(nil) - resultSamples := samplesFromIterator(t, it) + resultSamples, err := storage.ExpandSamples(it, nil) + require.NoError(t, err) requireEqualSamples(t, s1.String(), tc.expChunksSamples[i], resultSamples, true) } }) @@ -1041,7 +1042,8 @@ func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( require.Nil(t, c) it := iterable.Iterator(nil) - resultSamples := samplesFromIterator(t, it) + resultSamples, err := storage.ExpandSamples(it, nil) + require.NoError(t, err) requireEqualSamples(t, s1.String(), tc.expChunksSamples[i], resultSamples, true) } }) diff --git a/tsdb/testutil.go b/tsdb/testutil.go index 3f7df2d3a8..71c54761e2 100644 --- a/tsdb/testutil.go +++ b/tsdb/testutil.go @@ -18,8 +18,6 @@ import ( prom_testutil "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/prometheus/prometheus/tsdb/chunkenc" - "github.com/stretchr/testify/require" "github.com/prometheus/prometheus/model/histogram" @@ -156,7 +154,9 @@ func requireEqualSamples(t *testing.T, name string, expected, actual []chunks.Sa require.Equal(t, expectedHist, actualHist, "Sample doesn't match for %s[%d] at ts %d", name, i, expectedSample.T()) } default: - require.Equal(t, expectedSample, actualSample, "Sample doesn't match for %s[%d] at ts %d", name, i, expectedSample.T()) + expectedFloat := expectedSample.F() + actualFloat := actualSample.F() + require.Equal(t, expectedFloat, actualFloat, "Sample doesn't match for %s[%d] at ts %d", name, i, expectedSample.T()) } } } @@ -174,23 +174,3 @@ func counterResetAsString(h histogram.CounterResetHint) string { } panic("Unexpected counter reset type") } - -func samplesFromIterator(t testing.TB, it chunkenc.Iterator) []chunks.Sample { - var samples []chunks.Sample - for typ := it.Next(); typ != chunkenc.ValNone; typ = it.Next() { - switch typ { - case chunkenc.ValFloat: - ts, val := it.At() - samples = append(samples, sample{t: ts, f: val}) - case chunkenc.ValHistogram: - ts, val := it.AtHistogram(nil) - samples = append(samples, sample{t: ts, h: val}) - case chunkenc.ValFloatHistogram: - ts, val := it.AtFloatHistogram(nil) - samples = append(samples, sample{t: ts, fh: val}) - default: - t.Fatalf("unknown sample value type %s", typ) - } - } - return samples -} From 00499006c08e75f043cfdeb042add98027092c7d Mon Sep 17 00:00:00 2001 From: Carrie Edwards Date: Wed, 3 Jul 2024 08:28:31 -0700 Subject: [PATCH 13/29] Testutil refactoring Signed-off-by: Carrie Edwards --- tsdb/ooo_head_read_test.go | 47 ++++++++++++-------------------------- tsdb/testutil.go | 4 ++-- 2 files changed, 16 insertions(+), 35 deletions(-) diff --git a/tsdb/ooo_head_read_test.go b/tsdb/ooo_head_read_test.go index 938f9a7f71..48ab0496f2 100644 --- a/tsdb/ooo_head_read_test.go +++ b/tsdb/ooo_head_read_test.go @@ -496,7 +496,7 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { queryMinT int64 queryMaxT int64 firstInOrderSampleAt int64 - inputSamples []tsValue + inputSamples []testValue expChunkError bool expChunksSamples []chunks.SampleSlice }{ @@ -505,7 +505,7 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - inputSamples: []tsValue{ + inputSamples: []testValue{ {Ts: minutes(30), V: 0}, {Ts: minutes(40), V: 0}, }, @@ -526,7 +526,7 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - inputSamples: []tsValue{{Ts: minutes(41), V: 0}, {Ts: minutes(42), V: 0}, {Ts: minutes(43), V: 0}, {Ts: minutes(44), V: 0}, {Ts: minutes(45), V: 0}, {Ts: minutes(30), V: 1}, {Ts: minutes(50), V: 1}}, + inputSamples: []testValue{{Ts: minutes(41), V: 0}, {Ts: minutes(42), V: 0}, {Ts: minutes(43), V: 0}, {Ts: minutes(44), V: 0}, {Ts: minutes(45), V: 0}, {Ts: minutes(30), V: 1}, {Ts: minutes(50), V: 1}}, expChunkError: false, // ts (in minutes) 0 10 20 30 40 50 60 70 80 90 100 // Query Interval [------------------------------------------------------------------------------------------] @@ -550,7 +550,7 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - inputSamples: []tsValue{ + inputSamples: []testValue{ // Chunk 0 {Ts: minutes(10), V: 0}, {Ts: minutes(12), V: 0}, @@ -608,7 +608,7 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - inputSamples: []tsValue{ + inputSamples: []testValue{ // Chunk 0 {Ts: minutes(40), V: 0}, {Ts: minutes(42), V: 0}, @@ -666,7 +666,7 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - inputSamples: []tsValue{ + inputSamples: []testValue{ // Chunk 0 {Ts: minutes(10), V: 0}, {Ts: minutes(12), V: 0}, @@ -730,7 +730,7 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - inputSamples: []tsValue{ + inputSamples: []testValue{ // Chunk 0 {Ts: minutes(10), V: 0}, {Ts: minutes(15), V: 0}, @@ -773,7 +773,7 @@ func testOOOHeadChunkReader_Chunk(t *testing.T, scenario sampleTypeScenario) { queryMinT: minutes(12), queryMaxT: minutes(33), firstInOrderSampleAt: minutes(120), - inputSamples: []tsValue{ + inputSamples: []testValue{ // Chunk 0 {Ts: minutes(10), V: 0}, {Ts: minutes(15), V: 0}, @@ -885,8 +885,8 @@ func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( queryMinT int64 queryMaxT int64 firstInOrderSampleAt int64 - initialSamples []tsValue - samplesAfterSeriesCall []tsValue + initialSamples []testValue + samplesAfterSeriesCall []testValue expChunkError bool expChunksSamples []chunks.SampleSlice }{ @@ -895,7 +895,7 @@ func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - initialSamples: []tsValue{ + initialSamples: []testValue{ // Chunk 0 {Ts: minutes(20), V: 0}, {Ts: minutes(22), V: 0}, @@ -906,7 +906,7 @@ func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( {Ts: minutes(25), V: 1}, {Ts: minutes(35), V: 1}, }, - samplesAfterSeriesCall: []tsValue{ + samplesAfterSeriesCall: []testValue{ {Ts: minutes(10), V: 1}, {Ts: minutes(32), V: 1}, {Ts: minutes(50), V: 1}, @@ -937,7 +937,7 @@ func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( queryMinT: minutes(0), queryMaxT: minutes(100), firstInOrderSampleAt: minutes(120), - initialSamples: []tsValue{ + initialSamples: []testValue{ // Chunk 0 {Ts: minutes(20), V: 0}, {Ts: minutes(22), V: 0}, @@ -948,7 +948,7 @@ func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( {Ts: minutes(25), V: 1}, {Ts: minutes(35), V: 1}, }, - samplesAfterSeriesCall: []tsValue{ + samplesAfterSeriesCall: []testValue{ {Ts: minutes(10), V: 1}, {Ts: minutes(32), V: 1}, {Ts: minutes(50), V: 1}, @@ -967,24 +967,6 @@ func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( // Output Graphically [------------] (8 samples) It has 5 from Chunk 0 and 3 from Chunk 1 expChunksSamples: []chunks.SampleSlice{ { -<<<<<<< HEAD - sample{t: minutes(20), f: float64(0)}, - sample{t: minutes(22), f: float64(0)}, - sample{t: minutes(24), f: float64(0)}, - sample{t: minutes(25), f: float64(1)}, - sample{t: minutes(26), f: float64(0)}, - sample{t: minutes(30), f: float64(0)}, - sample{t: minutes(35), f: float64(1)}, -||||||| parent of 2795db1c2 (Update tsdb tests to use test utils.) - sample{t: minutes(20), f: float64(0)}, - sample{t: minutes(22), f: float64(0)}, - sample{t: minutes(24), f: float64(0)}, - sample{t: minutes(25), f: float64(1)}, - sample{t: minutes(26), f: float64(0)}, - sample{t: minutes(30), f: float64(0)}, - sample{t: minutes(32), f: float64(1)}, // This sample was added after Series() but before Chunk() and its in between the lastmint and maxt so it should be kept - sample{t: minutes(35), f: float64(1)}, -======= scenario.sampleFunc(minutes(20), 0), scenario.sampleFunc(minutes(22), 0), scenario.sampleFunc(minutes(24), 0), @@ -993,7 +975,6 @@ func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( scenario.sampleFunc(minutes(30), 0), scenario.sampleFunc(minutes(32), 1), // This sample was added after Series() but before Chunk() and its in between the lastmint and maxt so it should be kept scenario.sampleFunc(minutes(35), 1), ->>>>>>> 2795db1c2 (Update tsdb tests to use test utils.) }, }, }, diff --git a/tsdb/testutil.go b/tsdb/testutil.go index 71c54761e2..9730e47132 100644 --- a/tsdb/testutil.go +++ b/tsdb/testutil.go @@ -30,7 +30,7 @@ const ( float = "float" ) -type tsValue struct { +type testValue struct { Ts int64 V int64 CounterResetHeader histogram.CounterResetHint @@ -115,7 +115,7 @@ func requireEqualSeries(t *testing.T, expected, actual map[string][]chunks.Sampl } func requireEqualOOOSamples(t *testing.T, expectedSamples int, db *DB) { - require.GreaterOrEqual(t, float64(expectedSamples), + require.Equal(t, float64(expectedSamples), prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamplesAppended.WithLabelValues(sampleMetricTypeFloat))+ prom_testutil.ToFloat64(db.head.metrics.outOfOrderSamplesAppended.WithLabelValues(sampleMetricTypeHistogram)), "number of ooo appended samples mismatch") From ee909b340b490964552ef7ca1540790ea330c44d Mon Sep 17 00:00:00 2001 From: Carrie Edwards Date: Wed, 3 Jul 2024 09:27:43 -0700 Subject: [PATCH 14/29] Fix test Signed-off-by: Carrie Edwards --- tsdb/ooo_head_read_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/tsdb/ooo_head_read_test.go b/tsdb/ooo_head_read_test.go index 48ab0496f2..7ecd355b55 100644 --- a/tsdb/ooo_head_read_test.go +++ b/tsdb/ooo_head_read_test.go @@ -927,7 +927,6 @@ func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( scenario.sampleFunc(minutes(25), 1), scenario.sampleFunc(minutes(26), 0), scenario.sampleFunc(minutes(30), 0), - scenario.sampleFunc(minutes(32), 1), // This sample was added after Series() but before Chunk() and its in between the lastmint and maxt so it should be kept scenario.sampleFunc(minutes(35), 1), }, }, @@ -973,7 +972,6 @@ func testOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding( scenario.sampleFunc(minutes(25), 1), scenario.sampleFunc(minutes(26), 0), scenario.sampleFunc(minutes(30), 0), - scenario.sampleFunc(minutes(32), 1), // This sample was added after Series() but before Chunk() and its in between the lastmint and maxt so it should be kept scenario.sampleFunc(minutes(35), 1), }, }, From 82a8c6abe23a2afa3d7cbf3ccebc35e59d646dbb Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Wed, 3 Jul 2024 18:45:36 +0100 Subject: [PATCH 15/29] [ENHANCEMENT] Optimize regexps with multiple prefixes (#13843) For example `foo.*|bar.*|baz.*`. Instead of checking each one in turn, we build a map of prefixes, then check the smaller set that could match the string supplied. Signed-off-by: Bryan Boreham * Improve testing and readability Address review comments on #13843 Signed-off-by: Marco Pracucci --- model/labels/regexp.go | 129 +++++++++++++---- model/labels/regexp_test.go | 279 ++++++++++++++++++++++++++++-------- 2 files changed, 323 insertions(+), 85 deletions(-) diff --git a/model/labels/regexp.go b/model/labels/regexp.go index 767bd6942f..d2151d83dd 100644 --- a/model/labels/regexp.go +++ b/model/labels/regexp.go @@ -28,7 +28,7 @@ const ( maxSetMatches = 256 // The minimum number of alternate values a regex should have to trigger - // the optimization done by optimizeEqualStringMatchers() and so use a map + // the optimization done by optimizeEqualOrPrefixStringMatchers() and so use a map // to match values instead of iterating over a list. This value has // been computed running BenchmarkOptimizeEqualStringMatchers. minEqualMultiStringMatcherMapThreshold = 16 @@ -337,7 +337,7 @@ func optimizeAlternatingLiterals(s string) (StringMatcher, []string) { return nil, nil } - multiMatcher := newEqualMultiStringMatcher(true, estimatedAlternates) + multiMatcher := newEqualMultiStringMatcher(true, estimatedAlternates, 0, 0) for end := strings.IndexByte(s, '|'); end > -1; end = strings.IndexByte(s, '|') { // Split the string into the next literal and the remainder @@ -412,7 +412,7 @@ func stringMatcherFromRegexp(re *syntax.Regexp) StringMatcher { clearBeginEndText(re) m := stringMatcherFromRegexpInternal(re) - m = optimizeEqualStringMatchers(m, minEqualMultiStringMatcherMapThreshold) + m = optimizeEqualOrPrefixStringMatchers(m, minEqualMultiStringMatcherMapThreshold) return m } @@ -732,17 +732,20 @@ func (m *equalStringMatcher) Matches(s string) bool { type multiStringMatcherBuilder interface { StringMatcher add(s string) + addPrefix(prefix string, prefixCaseSensitive bool, matcher StringMatcher) setMatches() []string } -func newEqualMultiStringMatcher(caseSensitive bool, estimatedSize int) multiStringMatcherBuilder { +func newEqualMultiStringMatcher(caseSensitive bool, estimatedSize, estimatedPrefixes, minPrefixLength int) multiStringMatcherBuilder { // If the estimated size is low enough, it's faster to use a slice instead of a map. - if estimatedSize < minEqualMultiStringMatcherMapThreshold { + if estimatedSize < minEqualMultiStringMatcherMapThreshold && estimatedPrefixes == 0 { return &equalMultiStringSliceMatcher{caseSensitive: caseSensitive, values: make([]string, 0, estimatedSize)} } return &equalMultiStringMapMatcher{ values: make(map[string]struct{}, estimatedSize), + prefixes: make(map[string][]StringMatcher, estimatedPrefixes), + minPrefixLen: minPrefixLength, caseSensitive: caseSensitive, } } @@ -758,6 +761,10 @@ func (m *equalMultiStringSliceMatcher) add(s string) { m.values = append(m.values, s) } +func (m *equalMultiStringSliceMatcher) addPrefix(_ string, _ bool, _ StringMatcher) { + panic("not implemented") +} + func (m *equalMultiStringSliceMatcher) setMatches() []string { return m.values } @@ -779,12 +786,17 @@ func (m *equalMultiStringSliceMatcher) Matches(s string) bool { return false } -// equalMultiStringMapMatcher matches a string exactly against a map of valid values. +// equalMultiStringMapMatcher matches a string exactly against a map of valid values +// or against a set of prefix matchers. type equalMultiStringMapMatcher struct { // values contains values to match a string against. If the matching is case insensitive, // the values here must be lowercase. values map[string]struct{} - + // prefixes maps strings, all of length minPrefixLen, to sets of matchers to check the rest of the string. + // If the matching is case insensitive, prefixes are all lowercase. + prefixes map[string][]StringMatcher + // minPrefixLen can be zero, meaning there are no prefix matchers. + minPrefixLen int caseSensitive bool } @@ -796,8 +808,27 @@ func (m *equalMultiStringMapMatcher) add(s string) { m.values[s] = struct{}{} } +func (m *equalMultiStringMapMatcher) addPrefix(prefix string, prefixCaseSensitive bool, matcher StringMatcher) { + if m.minPrefixLen == 0 { + panic("addPrefix called when no prefix length defined") + } + if len(prefix) < m.minPrefixLen { + panic("addPrefix called with a too short prefix") + } + if m.caseSensitive != prefixCaseSensitive { + panic("addPrefix called with a prefix whose case sensitivity is different than the expected one") + } + + s := prefix[:m.minPrefixLen] + if !m.caseSensitive { + s = strings.ToLower(s) + } + + m.prefixes[s] = append(m.prefixes[s], matcher) +} + func (m *equalMultiStringMapMatcher) setMatches() []string { - if len(m.values) >= maxSetMatches { + if len(m.values) >= maxSetMatches || len(m.prefixes) > 0 { return nil } @@ -813,8 +844,17 @@ func (m *equalMultiStringMapMatcher) Matches(s string) bool { s = toNormalisedLower(s) } - _, ok := m.values[s] - return ok + if _, ok := m.values[s]; ok { + return true + } + if m.minPrefixLen > 0 && len(s) >= m.minPrefixLen { + for _, matcher := range m.prefixes[s[:m.minPrefixLen]] { + if matcher.Matches(s) { + return true + } + } + } + return false } // toNormalisedLower normalise the input string using "Unicode Normalization Form D" and then convert @@ -897,20 +937,24 @@ func (m trueMatcher) Matches(_ string) bool { return true } -// optimizeEqualStringMatchers optimize a specific case where all matchers are made by an -// alternation (orStringMatcher) of strings checked for equality (equalStringMatcher). In -// this specific case, when we have many strings to match against we can use a map instead +// optimizeEqualOrPrefixStringMatchers optimize a specific case where all matchers are made by an +// alternation (orStringMatcher) of strings checked for equality (equalStringMatcher) or +// with a literal prefix (literalPrefixSensitiveStringMatcher or literalPrefixInsensitiveStringMatcher). +// +// In this specific case, when we have many strings to match against we can use a map instead // of iterating over the list of strings. -func optimizeEqualStringMatchers(input StringMatcher, threshold int) StringMatcher { +func optimizeEqualOrPrefixStringMatchers(input StringMatcher, threshold int) StringMatcher { var ( caseSensitive bool caseSensitiveSet bool numValues int + numPrefixes int + minPrefixLength int ) // Analyse the input StringMatcher to count the number of occurrences // and ensure all of them have the same case sensitivity. - analyseCallback := func(matcher *equalStringMatcher) bool { + analyseEqualMatcherCallback := func(matcher *equalStringMatcher) bool { // Ensure we don't have mixed case sensitivity. if caseSensitiveSet && caseSensitive != matcher.caseSensitive { return false @@ -923,34 +967,55 @@ func optimizeEqualStringMatchers(input StringMatcher, threshold int) StringMatch return true } - if !findEqualStringMatchers(input, analyseCallback) { + analysePrefixMatcherCallback := func(prefix string, prefixCaseSensitive bool, matcher StringMatcher) bool { + // Ensure we don't have mixed case sensitivity. + if caseSensitiveSet && caseSensitive != prefixCaseSensitive { + return false + } else if !caseSensitiveSet { + caseSensitive = prefixCaseSensitive + caseSensitiveSet = true + } + if numPrefixes == 0 || len(prefix) < minPrefixLength { + minPrefixLength = len(prefix) + } + + numPrefixes++ + return true + } + + if !findEqualOrPrefixStringMatchers(input, analyseEqualMatcherCallback, analysePrefixMatcherCallback) { return input } - // If the number of values found is less than the threshold, then we should skip the optimization. - if numValues < threshold { + // If the number of values and prefixes found is less than the threshold, then we should skip the optimization. + if (numValues + numPrefixes) < threshold { return input } // Parse again the input StringMatcher to extract all values and storing them. // We can skip the case sensitivity check because we've already checked it and // if the code reach this point then it means all matchers have the same case sensitivity. - multiMatcher := newEqualMultiStringMatcher(caseSensitive, numValues) + multiMatcher := newEqualMultiStringMatcher(caseSensitive, numValues, numPrefixes, minPrefixLength) // Ignore the return value because we already iterated over the input StringMatcher // and it was all good. - findEqualStringMatchers(input, func(matcher *equalStringMatcher) bool { + findEqualOrPrefixStringMatchers(input, func(matcher *equalStringMatcher) bool { multiMatcher.add(matcher.s) return true + }, func(prefix string, prefixCaseSensitive bool, matcher StringMatcher) bool { + multiMatcher.addPrefix(prefix, caseSensitive, matcher) + return true }) return multiMatcher } -// findEqualStringMatchers analyze the input StringMatcher and calls the callback for each -// equalStringMatcher found. Returns true if and only if the input StringMatcher is *only* -// composed by an alternation of equalStringMatcher. -func findEqualStringMatchers(input StringMatcher, callback func(matcher *equalStringMatcher) bool) bool { +// findEqualOrPrefixStringMatchers analyze the input StringMatcher and calls the equalMatcherCallback for each +// equalStringMatcher found, and prefixMatcherCallback for each literalPrefixSensitiveStringMatcher and literalPrefixInsensitiveStringMatcher found. +// +// Returns true if and only if the input StringMatcher is *only* composed by an alternation of equalStringMatcher and/or +// literal prefix matcher. Returns false if prefixMatcherCallback is nil and a literal prefix matcher is encountered. +func findEqualOrPrefixStringMatchers(input StringMatcher, equalMatcherCallback func(matcher *equalStringMatcher) bool, prefixMatcherCallback func(prefix string, prefixCaseSensitive bool, matcher StringMatcher) bool) bool { orInput, ok := input.(orStringMatcher) if !ok { return false @@ -959,17 +1024,27 @@ func findEqualStringMatchers(input StringMatcher, callback func(matcher *equalSt for _, m := range orInput { switch casted := m.(type) { case orStringMatcher: - if !findEqualStringMatchers(m, callback) { + if !findEqualOrPrefixStringMatchers(m, equalMatcherCallback, prefixMatcherCallback) { return false } case *equalStringMatcher: - if !callback(casted) { + if !equalMatcherCallback(casted) { + return false + } + + case *literalPrefixSensitiveStringMatcher: + if prefixMatcherCallback == nil || !prefixMatcherCallback(casted.prefix, true, casted) { + return false + } + + case *literalPrefixInsensitiveStringMatcher: + if prefixMatcherCallback == nil || !prefixMatcherCallback(casted.prefix, false, casted) { return false } default: - // It's not an equal string matcher, so we have to stop searching + // It's not an equal or prefix string matcher, so we have to stop searching // cause this optimization can't be applied. return false } diff --git a/model/labels/regexp_test.go b/model/labels/regexp_test.go index fa5c96f420..24875e64ef 100644 --- a/model/labels/regexp_test.go +++ b/model/labels/regexp_test.go @@ -71,6 +71,8 @@ var ( // A long case insensitive alternation. "(?i:(zQPbMkNO|NNSPdvMi|iWuuSoAl|qbvKMimS|IecrXtPa|seTckYqt|NxnyHkgB|fIDlOgKb|UhlWIygH|OtNoJxHG|cUTkFVIV|mTgFIHjr|jQkoIDtE|PPMKxRXl|AwMfwVkQ|CQyMrTQJ|BzrqxVSi|nTpcWuhF|PertdywG|ZZDgCtXN|WWdDPyyE|uVtNQsKk|BdeCHvPZ|wshRnFlH|aOUIitIp|RxZeCdXT|CFZMslCj|AVBZRDxl|IzIGCnhw|ythYuWiz|oztXVXhl|VbLkwqQx|qvaUgyVC|VawUjPWC|ecloYJuj|boCLTdSU|uPrKeAZx|hrMWLWBq|JOnUNHRM|rYnujkPq|dDEdZhIj|DRrfvugG|yEGfDxVV|YMYdJWuP|PHUQZNWM|AmKNrLis|zTxndVfn|FPsHoJnc|EIulZTua|KlAPhdzg|ScHJJCLt|NtTfMzME|eMCwuFdo|SEpJVJbR|cdhXZeCx|sAVtBwRh|kVFEVcMI|jzJrxraA|tGLHTell|NNWoeSaw|DcOKSetX|UXZAJyka|THpMphDP|rizheevl|kDCBRidd|pCZZRqyu|pSygkitl|SwZGkAaW|wILOrfNX|QkwVOerj|kHOMxPDr|EwOVycJv|AJvtzQFS|yEOjKYYB|LizIINLL|JBRSsfcG|YPiUqqNl|IsdEbvee|MjEpGcBm|OxXZVgEQ|xClXGuxa|UzRCGFEb|buJbvfvA|IPZQxRet|oFYShsMc|oBHffuHO|bzzKrcBR|KAjzrGCl|IPUsAVls|OGMUMbIU|gyDccHuR|bjlalnDd|ZLWjeMna|fdsuIlxQ|dVXtiomV|XxedTjNg|XWMHlNoA|nnyqArQX|opfkWGhb|wYtnhdYb))", "(?i:(AAAAAAAAAAAAAAAAAAAAAAAA|BBBBBBBBBBBBBBBBBBBBBBBB|cccccccccccccccccccccccC|ſſſſſſſſſſſſſſſſſſſſſſſſS|SSSSSSSSSSSSSSSSSSSSSSSSſ))", + // A short case insensitive alternation where each entry ends with ".*". + "(?i:(zQPbMkNO.*|NNSPdvMi.*|iWuuSoAl.*))", // A long case insensitive alternation where each entry ends with ".*". "(?i:(zQPbMkNO.*|NNSPdvMi.*|iWuuSoAl.*|qbvKMimS.*|IecrXtPa.*|seTckYqt.*|NxnyHkgB.*|fIDlOgKb.*|UhlWIygH.*|OtNoJxHG.*|cUTkFVIV.*|mTgFIHjr.*|jQkoIDtE.*|PPMKxRXl.*|AwMfwVkQ.*|CQyMrTQJ.*|BzrqxVSi.*|nTpcWuhF.*|PertdywG.*|ZZDgCtXN.*|WWdDPyyE.*|uVtNQsKk.*|BdeCHvPZ.*|wshRnFlH.*|aOUIitIp.*|RxZeCdXT.*|CFZMslCj.*|AVBZRDxl.*|IzIGCnhw.*|ythYuWiz.*|oztXVXhl.*|VbLkwqQx.*|qvaUgyVC.*|VawUjPWC.*|ecloYJuj.*|boCLTdSU.*|uPrKeAZx.*|hrMWLWBq.*|JOnUNHRM.*|rYnujkPq.*|dDEdZhIj.*|DRrfvugG.*|yEGfDxVV.*|YMYdJWuP.*|PHUQZNWM.*|AmKNrLis.*|zTxndVfn.*|FPsHoJnc.*|EIulZTua.*|KlAPhdzg.*|ScHJJCLt.*|NtTfMzME.*|eMCwuFdo.*|SEpJVJbR.*|cdhXZeCx.*|sAVtBwRh.*|kVFEVcMI.*|jzJrxraA.*|tGLHTell.*|NNWoeSaw.*|DcOKSetX.*|UXZAJyka.*|THpMphDP.*|rizheevl.*|kDCBRidd.*|pCZZRqyu.*|pSygkitl.*|SwZGkAaW.*|wILOrfNX.*|QkwVOerj.*|kHOMxPDr.*|EwOVycJv.*|AJvtzQFS.*|yEOjKYYB.*|LizIINLL.*|JBRSsfcG.*|YPiUqqNl.*|IsdEbvee.*|MjEpGcBm.*|OxXZVgEQ.*|xClXGuxa.*|UzRCGFEb.*|buJbvfvA.*|IPZQxRet.*|oFYShsMc.*|oBHffuHO.*|bzzKrcBR.*|KAjzrGCl.*|IPUsAVls.*|OGMUMbIU.*|gyDccHuR.*|bjlalnDd.*|ZLWjeMna.*|fdsuIlxQ.*|dVXtiomV.*|XxedTjNg.*|XWMHlNoA.*|nnyqArQX.*|opfkWGhb.*|wYtnhdYb.*))", // A long case insensitive alternation where each entry starts with ".*". @@ -686,7 +688,15 @@ func randStrings(randGenerator *rand.Rand, many, length int) []string { return out } -func TestOptimizeEqualStringMatchers(t *testing.T) { +func randStringsWithSuffix(randGenerator *rand.Rand, many, length int, suffix string) []string { + out := randStrings(randGenerator, many, length) + for i := range out { + out[i] += suffix + } + return out +} + +func TestOptimizeEqualOrPrefixStringMatchers(t *testing.T) { tests := map[string]struct { input StringMatcher expectedValues []string @@ -767,7 +777,7 @@ func TestOptimizeEqualStringMatchers(t *testing.T) { for testName, testData := range tests { t.Run(testName, func(t *testing.T) { - actualMatcher := optimizeEqualStringMatchers(testData.input, 0) + actualMatcher := optimizeEqualOrPrefixStringMatchers(testData.input, 0) if testData.expectedValues == nil { require.IsType(t, testData.input, actualMatcher) @@ -782,10 +792,12 @@ func TestOptimizeEqualStringMatchers(t *testing.T) { func TestNewEqualMultiStringMatcher(t *testing.T) { tests := map[string]struct { - values []string - caseSensitive bool - expectedValuesMap map[string]struct{} - expectedValuesList []string + values []string + caseSensitivePrefixes []*literalPrefixSensitiveStringMatcher + caseSensitive bool + expectedValuesMap map[string]struct{} + expectedPrefixesMap map[string][]StringMatcher + expectedValuesList []string }{ "few case sensitive values": { values: []string{"a", "B"}, @@ -797,27 +809,47 @@ func TestNewEqualMultiStringMatcher(t *testing.T) { caseSensitive: false, expectedValuesList: []string{"a", "B"}, }, + "few case sensitive values and prefixes": { + values: []string{"a"}, + caseSensitivePrefixes: []*literalPrefixSensitiveStringMatcher{{prefix: "B", right: anyStringWithoutNewlineMatcher{}}}, + caseSensitive: true, + expectedValuesMap: map[string]struct{}{"a": {}}, + expectedPrefixesMap: map[string][]StringMatcher{"B": {&literalPrefixSensitiveStringMatcher{prefix: "B", right: anyStringWithoutNewlineMatcher{}}}}, + }, "many case sensitive values": { - values: []string{"a", "B", "c", "D", "e", "F", "g", "H", "i", "L", "m", "N", "o", "P", "q", "r"}, - caseSensitive: true, - expectedValuesMap: map[string]struct{}{"a": {}, "B": {}, "c": {}, "D": {}, "e": {}, "F": {}, "g": {}, "H": {}, "i": {}, "L": {}, "m": {}, "N": {}, "o": {}, "P": {}, "q": {}, "r": {}}, + values: []string{"a", "B", "c", "D", "e", "F", "g", "H", "i", "L", "m", "N", "o", "P", "q", "r"}, + caseSensitive: true, + expectedValuesMap: map[string]struct{}{"a": {}, "B": {}, "c": {}, "D": {}, "e": {}, "F": {}, "g": {}, "H": {}, "i": {}, "L": {}, "m": {}, "N": {}, "o": {}, "P": {}, "q": {}, "r": {}}, + expectedPrefixesMap: map[string][]StringMatcher{}, }, "many case insensitive values": { - values: []string{"a", "B", "c", "D", "e", "F", "g", "H", "i", "L", "m", "N", "o", "P", "q", "r"}, - caseSensitive: false, - expectedValuesMap: map[string]struct{}{"a": {}, "b": {}, "c": {}, "d": {}, "e": {}, "f": {}, "g": {}, "h": {}, "i": {}, "l": {}, "m": {}, "n": {}, "o": {}, "p": {}, "q": {}, "r": {}}, + values: []string{"a", "B", "c", "D", "e", "F", "g", "H", "i", "L", "m", "N", "o", "P", "q", "r"}, + caseSensitive: false, + expectedValuesMap: map[string]struct{}{"a": {}, "b": {}, "c": {}, "d": {}, "e": {}, "f": {}, "g": {}, "h": {}, "i": {}, "l": {}, "m": {}, "n": {}, "o": {}, "p": {}, "q": {}, "r": {}}, + expectedPrefixesMap: map[string][]StringMatcher{}, }, } for testName, testData := range tests { t.Run(testName, func(t *testing.T) { - matcher := newEqualMultiStringMatcher(testData.caseSensitive, len(testData.values)) + // To keep this test simple, we always assume a min prefix length of 1. + minPrefixLength := 0 + if len(testData.caseSensitivePrefixes) > 0 { + minPrefixLength = 1 + } + + matcher := newEqualMultiStringMatcher(testData.caseSensitive, len(testData.values), len(testData.caseSensitivePrefixes), minPrefixLength) for _, v := range testData.values { matcher.add(v) } - if testData.expectedValuesMap != nil { + for _, p := range testData.caseSensitivePrefixes { + matcher.addPrefix(p.prefix, true, p) + } + + if testData.expectedValuesMap != nil || testData.expectedPrefixesMap != nil { require.IsType(t, &equalMultiStringMapMatcher{}, matcher) require.Equal(t, testData.expectedValuesMap, matcher.(*equalMultiStringMapMatcher).values) + require.Equal(t, testData.expectedPrefixesMap, matcher.(*equalMultiStringMapMatcher).prefixes) require.Equal(t, testData.caseSensitive, matcher.(*equalMultiStringMapMatcher).caseSensitive) } if testData.expectedValuesList != nil { @@ -829,9 +861,32 @@ func TestNewEqualMultiStringMatcher(t *testing.T) { } } +func TestEqualMultiStringMapMatcher_addPrefix(t *testing.T) { + t.Run("should panic if the matcher is case sensitive but the prefix is not case sensitive", func(t *testing.T) { + matcher := newEqualMultiStringMatcher(true, 0, 1, 1) + + require.Panics(t, func() { + matcher.addPrefix("a", false, &literalPrefixInsensitiveStringMatcher{ + prefix: "a", + }) + }) + }) + + t.Run("should panic if the matcher is not case sensitive but the prefix is case sensitive", func(t *testing.T) { + matcher := newEqualMultiStringMatcher(false, 0, 1, 1) + + require.Panics(t, func() { + matcher.addPrefix("a", true, &literalPrefixSensitiveStringMatcher{ + prefix: "a", + }) + }) + }) +} + func TestEqualMultiStringMatcher_Matches(t *testing.T) { tests := map[string]struct { values []string + prefixes []StringMatcher caseSensitive bool expectedMatches []string expectedNotMatches []string @@ -848,6 +903,24 @@ func TestEqualMultiStringMatcher_Matches(t *testing.T) { expectedMatches: []string{"a", "A", "b", "B"}, expectedNotMatches: []string{"c", "C"}, }, + "few case sensitive prefixes": { + prefixes: []StringMatcher{ + &literalPrefixSensitiveStringMatcher{prefix: "a", right: anyStringWithoutNewlineMatcher{}}, + &literalPrefixSensitiveStringMatcher{prefix: "B", right: anyStringWithoutNewlineMatcher{}}, + }, + caseSensitive: true, + expectedMatches: []string{"a", "aX", "B", "BX"}, + expectedNotMatches: []string{"A", "b"}, + }, + "few case insensitive prefixes": { + prefixes: []StringMatcher{ + &literalPrefixInsensitiveStringMatcher{prefix: "a", right: anyStringWithoutNewlineMatcher{}}, + &literalPrefixInsensitiveStringMatcher{prefix: "B", right: anyStringWithoutNewlineMatcher{}}, + }, + caseSensitive: false, + expectedMatches: []string{"a", "aX", "A", "AX", "b", "bX", "B", "BX"}, + expectedNotMatches: []string{"c", "cX", "C", "CX"}, + }, "many case sensitive values": { values: []string{"a", "B", "c", "D", "e", "F", "g", "H", "i", "L", "m", "N", "o", "P", "q", "r"}, caseSensitive: true, @@ -860,14 +933,37 @@ func TestEqualMultiStringMatcher_Matches(t *testing.T) { expectedMatches: []string{"a", "A", "b", "B"}, expectedNotMatches: []string{"x", "X"}, }, + "mixed values and prefixes": { + values: []string{"a"}, + prefixes: []StringMatcher{&literalPrefixSensitiveStringMatcher{prefix: "B", right: anyStringWithoutNewlineMatcher{}}}, + caseSensitive: true, + expectedMatches: []string{"a", "B", "BX"}, + expectedNotMatches: []string{"aX", "A", "b", "bX"}, + }, } for testName, testData := range tests { t.Run(testName, func(t *testing.T) { - matcher := newEqualMultiStringMatcher(testData.caseSensitive, len(testData.values)) + // To keep this test simple, we always assume a min prefix length of 1. + minPrefixLength := 0 + if len(testData.prefixes) > 0 { + minPrefixLength = 1 + } + + matcher := newEqualMultiStringMatcher(testData.caseSensitive, len(testData.values), len(testData.prefixes), minPrefixLength) for _, v := range testData.values { matcher.add(v) } + for _, p := range testData.prefixes { + switch m := p.(type) { + case *literalPrefixSensitiveStringMatcher: + matcher.addPrefix(m.prefix, true, p) + case *literalPrefixInsensitiveStringMatcher: + matcher.addPrefix(m.prefix, false, p) + default: + panic("Unexpected type in test case") + } + } for _, v := range testData.expectedMatches { require.True(t, matcher.Matches(v), "value: %s", v) @@ -879,29 +975,33 @@ func TestEqualMultiStringMatcher_Matches(t *testing.T) { } } -func TestFindEqualStringMatchers(t *testing.T) { +func TestFindEqualOrPrefixStringMatchers(t *testing.T) { type match struct { s string caseSensitive bool } - // Utility to call findEqualStringMatchers() and collect all callback invocations. - findEqualStringMatchersAndCollectMatches := func(input StringMatcher) (matches []match, ok bool) { - ok = findEqualStringMatchers(input, func(matcher *equalStringMatcher) bool { + // Utility to call findEqualOrPrefixStringMatchers() and collect all callback invocations. + findEqualOrPrefixStringMatchersAndCollectMatches := func(input StringMatcher) (matches []match, ok bool) { + ok = findEqualOrPrefixStringMatchers(input, func(matcher *equalStringMatcher) bool { matches = append(matches, match{matcher.s, matcher.caseSensitive}) return true + }, func(prefix string, prefixCaseSensitive bool, right StringMatcher) bool { + matches = append(matches, match{prefix, prefixCaseSensitive}) + return true }) + return } t.Run("empty matcher", func(t *testing.T) { - actualMatches, actualOk := findEqualStringMatchersAndCollectMatches(emptyStringMatcher{}) + actualMatches, actualOk := findEqualOrPrefixStringMatchersAndCollectMatches(emptyStringMatcher{}) require.False(t, actualOk) require.Empty(t, actualMatches) }) t.Run("concat of literal matchers (case sensitive)", func(t *testing.T) { - actualMatches, actualOk := findEqualStringMatchersAndCollectMatches( + actualMatches, actualOk := findEqualOrPrefixStringMatchersAndCollectMatches( orStringMatcher{ &equalStringMatcher{s: "test-1", caseSensitive: true}, &equalStringMatcher{s: "test-2", caseSensitive: true}, @@ -913,7 +1013,7 @@ func TestFindEqualStringMatchers(t *testing.T) { }) t.Run("concat of literal matchers (case insensitive)", func(t *testing.T) { - actualMatches, actualOk := findEqualStringMatchersAndCollectMatches( + actualMatches, actualOk := findEqualOrPrefixStringMatchersAndCollectMatches( orStringMatcher{ &equalStringMatcher{s: "test-1", caseSensitive: false}, &equalStringMatcher{s: "test-2", caseSensitive: false}, @@ -925,7 +1025,7 @@ func TestFindEqualStringMatchers(t *testing.T) { }) t.Run("concat of literal matchers (mixed case)", func(t *testing.T) { - actualMatches, actualOk := findEqualStringMatchersAndCollectMatches( + actualMatches, actualOk := findEqualOrPrefixStringMatchersAndCollectMatches( orStringMatcher{ &equalStringMatcher{s: "test-1", caseSensitive: false}, &equalStringMatcher{s: "test-2", caseSensitive: true}, @@ -935,11 +1035,59 @@ func TestFindEqualStringMatchers(t *testing.T) { require.True(t, actualOk) require.Equal(t, []match{{"test-1", false}, {"test-2", true}}, actualMatches) }) + + t.Run("concat of literal prefix matchers (case sensitive)", func(t *testing.T) { + actualMatches, actualOk := findEqualOrPrefixStringMatchersAndCollectMatches( + orStringMatcher{ + &literalPrefixSensitiveStringMatcher{prefix: "test-1"}, + &literalPrefixSensitiveStringMatcher{prefix: "test-2"}, + }, + ) + + require.True(t, actualOk) + require.Equal(t, []match{{"test-1", true}, {"test-2", true}}, actualMatches) + }) + + t.Run("concat of literal prefix matchers (case insensitive)", func(t *testing.T) { + actualMatches, actualOk := findEqualOrPrefixStringMatchersAndCollectMatches( + orStringMatcher{ + &literalPrefixInsensitiveStringMatcher{prefix: "test-1"}, + &literalPrefixInsensitiveStringMatcher{prefix: "test-2"}, + }, + ) + + require.True(t, actualOk) + require.Equal(t, []match{{"test-1", false}, {"test-2", false}}, actualMatches) + }) + + t.Run("concat of literal prefix matchers (mixed case)", func(t *testing.T) { + actualMatches, actualOk := findEqualOrPrefixStringMatchersAndCollectMatches( + orStringMatcher{ + &literalPrefixInsensitiveStringMatcher{prefix: "test-1"}, + &literalPrefixSensitiveStringMatcher{prefix: "test-2"}, + }, + ) + + require.True(t, actualOk) + require.Equal(t, []match{{"test-1", false}, {"test-2", true}}, actualMatches) + }) + + t.Run("concat of literal string and prefix matchers (case sensitive)", func(t *testing.T) { + actualMatches, actualOk := findEqualOrPrefixStringMatchersAndCollectMatches( + orStringMatcher{ + &equalStringMatcher{s: "test-1", caseSensitive: true}, + &literalPrefixSensitiveStringMatcher{prefix: "test-2"}, + }, + ) + + require.True(t, actualOk) + require.Equal(t, []match{{"test-1", true}, {"test-2", true}}, actualMatches) + }) } // This benchmark is used to find a good threshold to use to apply the optimization -// done by optimizeEqualStringMatchers(). -func BenchmarkOptimizeEqualStringMatchers(b *testing.B) { +// done by optimizeEqualOrPrefixStringMatchers(). +func BenchmarkOptimizeEqualOrPrefixStringMatchers(b *testing.B) { randGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) // Generate variable lengths random texts to match against. @@ -949,42 +1097,51 @@ func BenchmarkOptimizeEqualStringMatchers(b *testing.B) { for numAlternations := 2; numAlternations <= 256; numAlternations *= 2 { for _, caseSensitive := range []bool{true, false} { - b.Run(fmt.Sprintf("alternations: %d case sensitive: %t", numAlternations, caseSensitive), func(b *testing.B) { - // Generate a regex with the expected number of alternations. - re := strings.Join(randStrings(randGenerator, numAlternations, 10), "|") - if !caseSensitive { - re = "(?i:(" + re + "))" - } - - parsed, err := syntax.Parse(re, syntax.Perl) - require.NoError(b, err) - - unoptimized := stringMatcherFromRegexpInternal(parsed) - require.IsType(b, orStringMatcher{}, unoptimized) - - optimized := optimizeEqualStringMatchers(unoptimized, 0) - if numAlternations < minEqualMultiStringMatcherMapThreshold { - require.IsType(b, &equalMultiStringSliceMatcher{}, optimized) - } else { - require.IsType(b, &equalMultiStringMapMatcher{}, optimized) - } - - b.Run("without optimizeEqualStringMatchers()", func(b *testing.B) { - for n := 0; n < b.N; n++ { - for _, t := range texts { - unoptimized.Matches(t) - } + for _, prefixMatcher := range []bool{true, false} { + b.Run(fmt.Sprintf("alternations: %d case sensitive: %t prefix matcher: %t", numAlternations, caseSensitive, prefixMatcher), func(b *testing.B) { + // If the test should run on prefix matchers, we add a wildcard matcher as suffix (prefix will be a literal). + suffix := "" + if prefixMatcher { + suffix = ".*" } - }) - b.Run("with optimizeEqualStringMatchers()", func(b *testing.B) { - for n := 0; n < b.N; n++ { - for _, t := range texts { - optimized.Matches(t) - } + // Generate a regex with the expected number of alternations. + re := strings.Join(randStringsWithSuffix(randGenerator, numAlternations, 10, suffix), "|") + if !caseSensitive { + re = "(?i:(" + re + "))" } + b.Logf("regexp: %s", re) + + parsed, err := syntax.Parse(re, syntax.Perl) + require.NoError(b, err) + + unoptimized := stringMatcherFromRegexpInternal(parsed) + require.IsType(b, orStringMatcher{}, unoptimized) + + optimized := optimizeEqualOrPrefixStringMatchers(unoptimized, 0) + if numAlternations < minEqualMultiStringMatcherMapThreshold && !prefixMatcher { + require.IsType(b, &equalMultiStringSliceMatcher{}, optimized) + } else { + require.IsType(b, &equalMultiStringMapMatcher{}, optimized) + } + + b.Run("without optimizeEqualOrPrefixStringMatchers()", func(b *testing.B) { + for n := 0; n < b.N; n++ { + for _, t := range texts { + unoptimized.Matches(t) + } + } + }) + + b.Run("with optimizeEqualOrPrefixStringMatchers()", func(b *testing.B) { + for n := 0; n < b.N; n++ { + for _, t := range texts { + optimized.Matches(t) + } + } + }) }) - }) + } } } } @@ -1204,10 +1361,16 @@ func visitStringMatcher(matcher StringMatcher, callback func(matcher StringMatch } // No nested matchers for the following ones. + case *equalMultiStringMapMatcher: + for _, prefixes := range casted.prefixes { + for _, matcher := range prefixes { + visitStringMatcher(matcher, callback) + } + } + case emptyStringMatcher: case *equalStringMatcher: case *equalMultiStringSliceMatcher: - case *equalMultiStringMapMatcher: case anyStringWithoutNewlineMatcher: case *anyNonEmptyStringMatcher: case trueMatcher: From c94c5b64c35b20e7b9062ae325f91a169ddf8ffc Mon Sep 17 00:00:00 2001 From: JuanJo Ciarlante Date: Wed, 3 Jul 2024 17:18:57 -0300 Subject: [PATCH 16/29] feat: add limitk() and limit_ratio() operators (#12503) * rebase 2024-07-01, picks previous renaming to `limitk()` and `limit_ratio()` Signed-off-by: JuanJo Ciarlante * gofumpt -d -extra Signed-off-by: JuanJo Ciarlante * more lint fixes Signed-off-by: JuanJo Ciarlante * more lint fixes+ Signed-off-by: JuanJo Ciarlante * put limitk() and limit_ratio() behind --enable-feature=promql-experimental-functions Signed-off-by: JuanJo Ciarlante * EnableExperimentalFunctions for TestConcurrentRangeQueries() also Signed-off-by: JuanJo Ciarlante * use testutil.RequireEqual to fix tests, WIP equivalent thingie for require.Contains Signed-off-by: JuanJo Ciarlante * lint fix Signed-off-by: JuanJo Ciarlante * moar linting Signed-off-by: JuanJo Ciarlante * rebase 2024-06-19 Signed-off-by: JuanJo Ciarlante * re-add limit(2, metric) testing for N=2 common series subset Signed-off-by: JuanJo Ciarlante * move `ratio = param` to default switch case, for better readability Signed-off-by: JuanJo Ciarlante * gofumpt -d -extra util/testutil/cmp.go Signed-off-by: JuanJo Ciarlante * early break when reaching k elems in limitk(), should have always been so (!) Signed-off-by: JuanJo Ciarlante * small typo fix Signed-off-by: JuanJo Ciarlante * no-change small break-loop rearrange for readability Signed-off-by: JuanJo Ciarlante * remove IsNan(ratio) condition in switch-case, already handled as input validation Signed-off-by: JuanJo Ciarlante * no-change adding some comments Signed-off-by: JuanJo Ciarlante * no-change simplify fullMatrix() helper functions used for tests Signed-off-by: JuanJo Ciarlante * add `limitk(-1, metric)` testcase, which is handled as any k < 1 case Signed-off-by: JuanJo Ciarlante * engine_test.go: no-change create `requireCommonSeries() helper func (moving code into it) for readability Signed-off-by: JuanJo Ciarlante * rebase 2024-06-21 Signed-off-by: JuanJo Ciarlante * engine_test.go: HAPPY NOW about its code -> reorg, create and use simpleRangeQuery() function, less lines and more readable ftW \o/ Signed-off-by: JuanJo Ciarlante * move limitk(), limit_ratio() testing to promql/promqltest/testdata/limit.test Signed-off-by: JuanJo Ciarlante * remove stale leftover after moving tests from engine_test.go to testdata/ Signed-off-by: JuanJo Ciarlante * fix flaky `limit_ratio(0.5, ...)` test case Signed-off-by: JuanJo Ciarlante * Update promql/engine.go Co-authored-by: Julius Volz Signed-off-by: JuanJo Ciarlante * Update promql/engine.go Co-authored-by: Julius Volz Signed-off-by: JuanJo Ciarlante * Update promql/engine.go Co-authored-by: Julius Volz Signed-off-by: JuanJo Ciarlante * fix AddRatioSample() implementation to use a single conditional (instead of switch/case + fallback return) Signed-off-by: JuanJo Ciarlante * docs/querying/operators.md: document r < 0 Signed-off-by: JuanJo Ciarlante * add negative limit_ratio() example to docs/querying/examples.md Signed-off-by: JuanJo Ciarlante * move more extensive docu examples to docs/querying/operators.md Signed-off-by: JuanJo Ciarlante * typo Signed-off-by: JuanJo Ciarlante * small docu fix for poor-mans-normality-check, add it to limit.test ;) Signed-off-by: JuanJo Ciarlante * limit.test: expand "Poor man's normality check" to whole eval range Signed-off-by: JuanJo Ciarlante * restore mistakenly removed existing small comment Signed-off-by: JuanJo Ciarlante * expand poors-man-normality-check case(s) Signed-off-by: JuanJo Ciarlante * Revert "expand poors-man-normality-check case(s)" This reverts commit f69e1603b2ebe69c0a100197cfbcf6f81644b564, indeed too flaky 0:) Signed-off-by: JuanJo Ciarlante * remove humor from docs/querying/operators.md Signed-off-by: JuanJo Ciarlante * fix signoff Signed-off-by: JuanJo Ciarlante * add web/ui missing changes Signed-off-by: JuanJo Ciarlante * expand limit_ratio test cases, cross-fingering they'll not be flaky Signed-off-by: JuanJo Ciarlante * remove flaky test Signed-off-by: JuanJo Ciarlante * add missing warnings.Merge(ws) in instant-query return shortcut Signed-off-by: JuanJo Ciarlante * add missing LimitK||LimitRatio case to codemirror-promql/src/parser/parser.ts Signed-off-by: JuanJo Ciarlante * fix ui-lint Signed-off-by: JuanJo Ciarlante * actually fix returned warnings :] Signed-off-by: JuanJo Ciarlante --------- Signed-off-by: JuanJo Ciarlante Co-authored-by: Julius Volz --- docs/querying/examples.md | 10 + docs/querying/operators.md | 38 +- promql/bench_test.go | 15 + promql/engine.go | 152 +++- promql/engine_test.go | 2 + promql/parser/generated_parser.y | 8 +- promql/parser/generated_parser.y.go | 807 +++++++++--------- promql/parser/lex.go | 4 +- promql/parser/parse.go | 6 +- promql/promql_test.go | 3 + promql/promqltest/testdata/limit.test | 119 +++ util/annotations/annotations.go | 10 + .../src/complete/promql.terms.ts | 12 + .../codemirror-promql/src/parser/parser.ts | 10 +- web/ui/module/lezer-promql/src/highlight.js | 2 +- web/ui/module/lezer-promql/src/promql.grammar | 4 + web/ui/module/lezer-promql/src/tokens.js | 4 + 17 files changed, 785 insertions(+), 421 deletions(-) create mode 100644 promql/promqltest/testdata/limit.test diff --git a/docs/querying/examples.md b/docs/querying/examples.md index 957c2f5f98..8287ff6f62 100644 --- a/docs/querying/examples.md +++ b/docs/querying/examples.md @@ -95,3 +95,13 @@ Assuming this metric contains one time series per running instance, you could count the number of running instances per application like this: count by (app) (instance_cpu_time_ns) + +If we are exploring some metrics for their labels, to e.g. be able to aggregate +over some of them, we could use the following: + + limitk(10, app_foo_metric_bar) + +Alternatively, if we wanted the returned timeseries to be more evenly sampled, +we could use the following to get approximately 10% of them: + + limit_ratio(0.1, app_foo_metric_bar) diff --git a/docs/querying/operators.md b/docs/querying/operators.md index b92bdd94ac..f5f217ff63 100644 --- a/docs/querying/operators.md +++ b/docs/querying/operators.md @@ -230,6 +230,8 @@ vector of fewer elements with aggregated values: * `bottomk` (smallest k elements by sample value) * `topk` (largest k elements by sample value) * `quantile` (calculate φ-quantile (0 ≤ φ ≤ 1) over dimensions) +* `limitk` (sample n elements) +* `limit_ratio` (sample elements with approximately 𝑟 ratio if `𝑟 > 0`, and the complement of such samples if `𝑟 = -(1.0 - 𝑟)`) These operators can either be used to aggregate over **all** label dimensions or preserve distinct dimensions by including a `without` or `by` clause. These @@ -249,8 +251,8 @@ all other labels are preserved in the output. `by` does the opposite and drops labels that are not listed in the `by` clause, even if their label values are identical between all elements of the vector. -`parameter` is only required for `count_values`, `quantile`, `topk` and -`bottomk`. +`parameter` is only required for `count_values`, `quantile`, `topk`, +`bottomk`, `limitk` and `limit_ratio`. `count_values` outputs one time series per unique sample value. Each series has an additional label. The name of that label is given by the aggregation @@ -261,11 +263,16 @@ time series is the number of times that sample value was present. the input samples, including the original labels, are returned in the result vector. `by` and `without` are only used to bucket the input vector. +`limitk` and `limit_ratio` also return a subset of the input samples, +including the original labels in the result vector, these are experimental +operators that must be enabled with `--enable-feature=promql-experimental-functions`. + `quantile` calculates the φ-quantile, the value that ranks at number φ*N among the N metric values of the dimensions aggregated over. φ is provided as the aggregation parameter. For example, `quantile(0.5, ...)` calculates the median, `quantile(0.95, ...)` the 95th percentile. For φ = `NaN`, `NaN` is returned. For φ < 0, `-Inf` is returned. For φ > 1, `+Inf` is returned. + Example: If the metric `http_requests_total` had time series that fan out by @@ -291,6 +298,33 @@ To get the 5 largest HTTP requests counts across all instances we could write: topk(5, http_requests_total) +To sample 10 timeseries, for example to inspect labels and their values, we +could write: + + limitk(10, http_requests_total) + +To deterministically sample approximately 10% of timeseries we could write: + + limit_ratio(0.1, http_requests_total) + +Given that `limit_ratio()` implements a deterministic sampling algorithm (based +on labels' hash), you can get the _complement_ of the above samples, i.e. +approximately 90%, but precisely those not returned by `limit_ratio(0.1, ...)` +with: + + limit_ratio(-0.9, http_requests_total) + +You can also use this feature to e.g. verify that `avg()` is a representative +aggregation for your samples' values, by checking that the difference between +averaging two samples' subsets is "small" when compared to the standard +deviation. + + abs( + avg(limit_ratio(0.5, http_requests_total)) + - + avg(limit_ratio(-0.5, http_requests_total)) + ) <= bool stddev(http_requests_total) + ## Binary operator precedence The following list shows the precedence of binary operators in Prometheus, from diff --git a/promql/bench_test.go b/promql/bench_test.go index fb3b6ac74b..bd67280294 100644 --- a/promql/bench_test.go +++ b/promql/bench_test.go @@ -187,6 +187,21 @@ func rangeQueryCases() []benchCase { { expr: "topk(5, a_X)", }, + { + expr: "limitk(1, a_X)", + }, + { + expr: "limitk(5, a_X)", + }, + { + expr: "limit_ratio(0.1, a_X)", + }, + { + expr: "limit_ratio(0.5, a_X)", + }, + { + expr: "limit_ratio(-0.5, a_X)", + }, // Combinations. { expr: "rate(a_X[1m]) + rate(b_X[1m])", diff --git a/promql/engine.go b/promql/engine.go index a5377d1645..bf19aac8bc 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1318,7 +1318,7 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping index, ok := groupToResultIndex[groupingKey] // Add a new group if it doesn't exist. if !ok { - if aggExpr.Op != parser.TOPK && aggExpr.Op != parser.BOTTOMK { + if aggExpr.Op != parser.TOPK && aggExpr.Op != parser.BOTTOMK && aggExpr.Op != parser.LIMITK && aggExpr.Op != parser.LIMIT_RATIO { m := generateGroupingLabels(enh, series.Metric, aggExpr.Without, sortedGrouping) result = append(result, Series{Metric: m}) } @@ -1331,9 +1331,10 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping groups := make([]groupedAggregation, groupCount) var k int + var ratio float64 var seriess map[uint64]Series switch aggExpr.Op { - case parser.TOPK, parser.BOTTOMK: + case parser.TOPK, parser.BOTTOMK, parser.LIMITK: if !convertibleToInt64(param) { ev.errorf("Scalar value %v overflows int64", param) } @@ -1345,6 +1346,23 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping return nil, warnings } seriess = make(map[uint64]Series, len(inputMatrix)) // Output series by series hash. + case parser.LIMIT_RATIO: + if math.IsNaN(param) { + ev.errorf("Ratio value %v is NaN", param) + } + switch { + case param == 0: + return nil, warnings + case param < -1.0: + ratio = -1.0 + warnings.Add(annotations.NewInvalidRatioWarning(param, ratio, aggExpr.Param.PositionRange())) + case param > 1.0: + ratio = 1.0 + warnings.Add(annotations.NewInvalidRatioWarning(param, ratio, aggExpr.Param.PositionRange())) + default: + ratio = param + } + seriess = make(map[uint64]Series, len(inputMatrix)) // Output series by series hash. case parser.QUANTILE: if math.IsNaN(param) || param < 0 || param > 1 { warnings.Add(annotations.NewInvalidQuantileWarning(param, aggExpr.Param.PositionRange())) @@ -1362,11 +1380,12 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping enh.Ts = ts var ws annotations.Annotations switch aggExpr.Op { - case parser.TOPK, parser.BOTTOMK: - result, ws = ev.aggregationK(aggExpr, k, inputMatrix, seriesToResult, groups, enh, seriess) + case parser.TOPK, parser.BOTTOMK, parser.LIMITK, parser.LIMIT_RATIO: + result, ws = ev.aggregationK(aggExpr, k, ratio, inputMatrix, seriesToResult, groups, enh, seriess) // If this could be an instant query, shortcut so as not to change sort order. if ev.endTimestamp == ev.startTimestamp { - return result, ws + warnings.Merge(ws) + return result, warnings } default: ws = ev.aggregation(aggExpr, param, inputMatrix, result, seriesToResult, groups, enh) @@ -1381,7 +1400,7 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping // Assemble the output matrix. By the time we get here we know we don't have too many samples. switch aggExpr.Op { - case parser.TOPK, parser.BOTTOMK: + case parser.TOPK, parser.BOTTOMK, parser.LIMITK, parser.LIMIT_RATIO: result = make(Matrix, 0, len(seriess)) for _, ss := range seriess { result = append(result, ss) @@ -2754,14 +2773,15 @@ func vectorElemBinop(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram } type groupedAggregation struct { - seen bool // Was this output groups seen in the input at this timestamp. - hasFloat bool // Has at least 1 float64 sample aggregated. - hasHistogram bool // Has at least 1 histogram sample aggregated. - floatValue float64 - histogramValue *histogram.FloatHistogram - floatMean float64 // Mean, or "compensating value" for Kahan summation. - groupCount int - heap vectorByValueHeap + seen bool // Was this output groups seen in the input at this timestamp. + hasFloat bool // Has at least 1 float64 sample aggregated. + hasHistogram bool // Has at least 1 histogram sample aggregated. + floatValue float64 + histogramValue *histogram.FloatHistogram + floatMean float64 // Mean, or "compensating value" for Kahan summation. + groupCount int + groupAggrComplete bool // Used by LIMITK to short-cut series loop when we've reached K elem on every group + heap vectorByValueHeap } // aggregation evaluates sum, avg, count, stdvar, stddev or quantile at one timestep on inputMatrix. @@ -2958,19 +2978,22 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix return annos } -// aggregationK evaluates topk or bottomk at one timestep on inputMatrix. +// aggregationK evaluates topk, bottomk, limitk, or limit_ratio at one timestep on inputMatrix. // Output that has the same labels as the input, but just k of them per group. // seriesToResult maps inputMatrix indexes to groups indexes. -// For an instant query, returns a Matrix in descending order for topk or ascending for bottomk. +// For an instant query, returns a Matrix in descending order for topk or ascending for bottomk, or without any order for limitk / limit_ratio. // For a range query, aggregates output in the seriess map. -func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Matrix, seriesToResult []int, groups []groupedAggregation, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { +func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, r float64, inputMatrix Matrix, seriesToResult []int, groups []groupedAggregation, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { op := e.Op var s Sample var annos annotations.Annotations + // Used to short-cut the loop for LIMITK if we already collected k elements for every group + groupsRemaining := len(groups) for i := range groups { groups[i].seen = false } +seriesLoop: for si := range inputMatrix { f, _, ok := ev.nextValues(enh.Ts, &inputMatrix[si]) if !ok { @@ -2981,11 +3004,23 @@ func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Ma group := &groups[seriesToResult[si]] // Initialize this group if it's the first time we've seen it. if !group.seen { - *group = groupedAggregation{ - seen: true, - heap: make(vectorByValueHeap, 1, k), + // LIMIT_RATIO is a special case, as we may not add this very sample to the heap, + // while we also don't know the final size of it. + if op == parser.LIMIT_RATIO { + *group = groupedAggregation{ + seen: true, + heap: make(vectorByValueHeap, 0), + } + if ratiosampler.AddRatioSample(r, &s) { + heap.Push(&group.heap, &s) + } + } else { + *group = groupedAggregation{ + seen: true, + heap: make(vectorByValueHeap, 1, k), + } + group.heap[0] = s } - group.heap[0] = s continue } @@ -3016,6 +3051,26 @@ func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Ma } } + case parser.LIMITK: + if len(group.heap) < k { + heap.Push(&group.heap, &s) + } + // LIMITK optimization: early break if we've added K elem to _every_ group, + // especially useful for large timeseries where the user is exploring labels via e.g. + // limitk(10, my_metric) + if !group.groupAggrComplete && len(group.heap) == k { + group.groupAggrComplete = true + groupsRemaining-- + if groupsRemaining == 0 { + break seriesLoop + } + } + + case parser.LIMIT_RATIO: + if ratiosampler.AddRatioSample(r, &s) { + heap.Push(&group.heap, &s) + } + default: panic(fmt.Errorf("expected aggregation operator but got %q", op)) } @@ -3065,6 +3120,11 @@ func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Ma for _, v := range aggr.heap { add(v.Metric, v.F) } + + case parser.LIMITK, parser.LIMIT_RATIO: + for _, v := range aggr.heap { + add(v.Metric, v.F) + } } } @@ -3419,6 +3479,56 @@ func makeInt64Pointer(val int64) *int64 { return valp } +// Add RatioSampler interface to allow unit-testing (previously: Randomizer). +type RatioSampler interface { + // Return this sample "offset" between [0.0, 1.0] + sampleOffset(ts int64, sample *Sample) float64 + AddRatioSample(r float64, sample *Sample) bool +} + +// Use Hash(labels.String()) / maxUint64 as a "deterministic" +// value in [0.0, 1.0]. +type HashRatioSampler struct{} + +var ratiosampler RatioSampler = NewHashRatioSampler() + +func NewHashRatioSampler() *HashRatioSampler { + return &HashRatioSampler{} +} + +func (s *HashRatioSampler) sampleOffset(ts int64, sample *Sample) float64 { + const ( + float64MaxUint64 = float64(math.MaxUint64) + ) + return float64(sample.Metric.Hash()) / float64MaxUint64 +} + +func (s *HashRatioSampler) AddRatioSample(ratioLimit float64, sample *Sample) bool { + // If ratioLimit >= 0: add sample if sampleOffset is lesser than ratioLimit + // + // 0.0 ratioLimit 1.0 + // [---------|--------------------------] + // [#########...........................] + // + // e.g.: + // sampleOffset==0.3 && ratioLimit==0.4 + // 0.3 < 0.4 ? --> add sample + // + // Else if ratioLimit < 0: add sample if rand() return the "complement" of ratioLimit>=0 case + // (loosely similar behavior to negative array index in other programming languages) + // + // 0.0 1+ratioLimit 1.0 + // [---------|--------------------------] + // [.........###########################] + // + // e.g.: + // sampleOffset==0.3 && ratioLimit==-0.6 + // 0.3 >= 0.4 ? --> don't add sample + sampleOffset := s.sampleOffset(sample.T, sample) + return (ratioLimit >= 0 && sampleOffset < ratioLimit) || + (ratioLimit < 0 && sampleOffset >= (1.0+ratioLimit)) +} + type histogramStatsSeries struct { storage.Series } diff --git a/promql/engine_test.go b/promql/engine_test.go index b144c11747..99f3fc5bdd 100644 --- a/promql/engine_test.go +++ b/promql/engine_test.go @@ -49,6 +49,8 @@ const ( ) func TestMain(m *testing.M) { + // Enable experimental functions testing + parser.EnableExperimentalFunctions = true goleak.VerifyTestMain(m) } diff --git a/promql/parser/generated_parser.y b/promql/parser/generated_parser.y index b39c1150a5..d84acc37c5 100644 --- a/promql/parser/generated_parser.y +++ b/promql/parser/generated_parser.y @@ -126,6 +126,8 @@ STDDEV STDVAR SUM TOPK +LIMITK +LIMIT_RATIO %token aggregatorsEnd // Keywords. @@ -609,7 +611,7 @@ metric : metric_identifier label_set ; -metric_identifier: AVG | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | IDENTIFIER | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | QUANTILE | STDDEV | STDVAR | SUM | TOPK | WITHOUT | START | END; +metric_identifier: AVG | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | IDENTIFIER | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | QUANTILE | STDDEV | STDVAR | SUM | TOPK | WITHOUT | START | END | LIMITK | LIMIT_RATIO; label_set : LEFT_BRACE label_set_list RIGHT_BRACE { $$ = labels.New($2...) } @@ -851,10 +853,10 @@ bucket_set_list : bucket_set_list SPACE number * Keyword lists. */ -aggregate_op : AVG | BOTTOMK | COUNT | COUNT_VALUES | GROUP | MAX | MIN | QUANTILE | STDDEV | STDVAR | SUM | TOPK ; +aggregate_op : AVG | BOTTOMK | COUNT | COUNT_VALUES | GROUP | MAX | MIN | QUANTILE | STDDEV | STDVAR | SUM | TOPK | LIMITK | LIMIT_RATIO; // Inside of grouping options label names can be recognized as keywords by the lexer. This is a list of keywords that could also be a label name. -maybe_label : AVG | BOOL | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | GROUP_LEFT | GROUP_RIGHT | IDENTIFIER | IGNORING | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | ON | QUANTILE | STDDEV | STDVAR | SUM | TOPK | START | END | ATAN2; +maybe_label : AVG | BOOL | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | GROUP_LEFT | GROUP_RIGHT | IDENTIFIER | IGNORING | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | ON | QUANTILE | STDDEV | STDVAR | SUM | TOPK | START | END | ATAN2 | LIMITK | LIMIT_RATIO; unary_op : ADD | SUB; diff --git a/promql/parser/generated_parser.y.go b/promql/parser/generated_parser.y.go index d9a312a137..07899c0a00 100644 --- a/promql/parser/generated_parser.y.go +++ b/promql/parser/generated_parser.y.go @@ -103,27 +103,29 @@ const STDDEV = 57411 const STDVAR = 57412 const SUM = 57413 const TOPK = 57414 -const aggregatorsEnd = 57415 -const keywordsStart = 57416 -const BOOL = 57417 -const BY = 57418 -const GROUP_LEFT = 57419 -const GROUP_RIGHT = 57420 -const IGNORING = 57421 -const OFFSET = 57422 -const ON = 57423 -const WITHOUT = 57424 -const keywordsEnd = 57425 -const preprocessorStart = 57426 -const START = 57427 -const END = 57428 -const preprocessorEnd = 57429 -const startSymbolsStart = 57430 -const START_METRIC = 57431 -const START_SERIES_DESCRIPTION = 57432 -const START_EXPRESSION = 57433 -const START_METRIC_SELECTOR = 57434 -const startSymbolsEnd = 57435 +const LIMITK = 57415 +const LIMIT_RATIO = 57416 +const aggregatorsEnd = 57417 +const keywordsStart = 57418 +const BOOL = 57419 +const BY = 57420 +const GROUP_LEFT = 57421 +const GROUP_RIGHT = 57422 +const IGNORING = 57423 +const OFFSET = 57424 +const ON = 57425 +const WITHOUT = 57426 +const keywordsEnd = 57427 +const preprocessorStart = 57428 +const START = 57429 +const END = 57430 +const preprocessorEnd = 57431 +const startSymbolsStart = 57432 +const START_METRIC = 57433 +const START_SERIES_DESCRIPTION = 57434 +const START_EXPRESSION = 57435 +const START_METRIC_SELECTOR = 57436 +const startSymbolsEnd = 57437 var yyToknames = [...]string{ "$end", @@ -198,6 +200,8 @@ var yyToknames = [...]string{ "STDVAR", "SUM", "TOPK", + "LIMITK", + "LIMIT_RATIO", "aggregatorsEnd", "keywordsStart", "BOOL", @@ -231,279 +235,298 @@ var yyExca = [...]int16{ -1, 1, 1, -1, -2, 0, - -1, 35, - 1, 134, - 10, 134, - 24, 134, + -1, 37, + 1, 136, + 10, 136, + 24, 136, -2, 0, - -1, 58, - 2, 172, - 15, 172, - 76, 172, - 82, 172, - -2, 100, - -1, 59, - 2, 173, - 15, 173, - 76, 173, - 82, 173, - -2, 101, -1, 60, 2, 174, 15, 174, - 76, 174, - 82, 174, - -2, 103, + 78, 174, + 84, 174, + -2, 100, -1, 61, 2, 175, 15, 175, - 76, 175, - 82, 175, - -2, 104, + 78, 175, + 84, 175, + -2, 101, -1, 62, 2, 176, 15, 176, - 76, 176, - 82, 176, - -2, 105, + 78, 176, + 84, 176, + -2, 103, -1, 63, 2, 177, 15, 177, - 76, 177, - 82, 177, - -2, 110, + 78, 177, + 84, 177, + -2, 104, -1, 64, 2, 178, 15, 178, - 76, 178, - 82, 178, - -2, 112, + 78, 178, + 84, 178, + -2, 105, -1, 65, 2, 179, 15, 179, - 76, 179, - 82, 179, - -2, 114, + 78, 179, + 84, 179, + -2, 110, -1, 66, 2, 180, 15, 180, - 76, 180, - 82, 180, - -2, 115, + 78, 180, + 84, 180, + -2, 112, -1, 67, 2, 181, 15, 181, - 76, 181, - 82, 181, - -2, 116, + 78, 181, + 84, 181, + -2, 114, -1, 68, 2, 182, 15, 182, - 76, 182, - 82, 182, - -2, 117, + 78, 182, + 84, 182, + -2, 115, -1, 69, 2, 183, 15, 183, - 76, 183, - 82, 183, + 78, 183, + 84, 183, + -2, 116, + -1, 70, + 2, 184, + 15, 184, + 78, 184, + 84, 184, + -2, 117, + -1, 71, + 2, 185, + 15, 185, + 78, 185, + 84, 185, -2, 118, - -1, 195, - 12, 231, - 13, 231, - 18, 231, - 19, 231, - 25, 231, - 40, 231, - 46, 231, - 47, 231, - 50, 231, - 56, 231, - 61, 231, - 62, 231, - 63, 231, - 64, 231, - 65, 231, - 66, 231, - 67, 231, - 68, 231, - 69, 231, - 70, 231, - 71, 231, - 72, 231, - 76, 231, - 80, 231, - 82, 231, - 85, 231, - 86, 231, + -1, 72, + 2, 186, + 15, 186, + 78, 186, + 84, 186, + -2, 122, + -1, 73, + 2, 187, + 15, 187, + 78, 187, + 84, 187, + -2, 123, + -1, 199, + 12, 237, + 13, 237, + 18, 237, + 19, 237, + 25, 237, + 40, 237, + 46, 237, + 47, 237, + 50, 237, + 56, 237, + 61, 237, + 62, 237, + 63, 237, + 64, 237, + 65, 237, + 66, 237, + 67, 237, + 68, 237, + 69, 237, + 70, 237, + 71, 237, + 72, 237, + 73, 237, + 74, 237, + 78, 237, + 82, 237, + 84, 237, + 87, 237, + 88, 237, -2, 0, - -1, 196, - 12, 231, - 13, 231, - 18, 231, - 19, 231, - 25, 231, - 40, 231, - 46, 231, - 47, 231, - 50, 231, - 56, 231, - 61, 231, - 62, 231, - 63, 231, - 64, 231, - 65, 231, - 66, 231, - 67, 231, - 68, 231, - 69, 231, - 70, 231, - 71, 231, - 72, 231, - 76, 231, - 80, 231, - 82, 231, - 85, 231, - 86, 231, + -1, 200, + 12, 237, + 13, 237, + 18, 237, + 19, 237, + 25, 237, + 40, 237, + 46, 237, + 47, 237, + 50, 237, + 56, 237, + 61, 237, + 62, 237, + 63, 237, + 64, 237, + 65, 237, + 66, 237, + 67, 237, + 68, 237, + 69, 237, + 70, 237, + 71, 237, + 72, 237, + 73, 237, + 74, 237, + 78, 237, + 82, 237, + 84, 237, + 87, 237, + 88, 237, -2, 0, - -1, 217, - 21, 229, + -1, 221, + 21, 235, -2, 0, - -1, 286, - 21, 230, + -1, 292, + 21, 236, -2, 0, } const yyPrivate = 57344 -const yyLast = 778 +const yyLast = 793 var yyAct = [...]int16{ - 151, 324, 322, 268, 329, 148, 221, 37, 187, 144, - 282, 281, 152, 113, 77, 173, 104, 102, 101, 6, - 223, 193, 105, 194, 195, 196, 128, 262, 260, 155, - 233, 103, 342, 293, 100, 319, 239, 116, 146, 318, - 315, 263, 156, 123, 106, 147, 284, 114, 295, 116, - 156, 341, 175, 259, 340, 253, 57, 264, 157, 114, - 117, 108, 313, 109, 235, 236, 157, 112, 237, 107, - 323, 174, 117, 175, 155, 96, 250, 99, 293, 224, - 226, 228, 229, 230, 238, 240, 243, 244, 245, 246, - 247, 177, 145, 225, 227, 231, 232, 234, 241, 242, - 98, 176, 178, 248, 249, 104, 2, 3, 4, 5, - 158, 105, 177, 110, 168, 162, 165, 302, 150, 160, - 191, 161, 176, 178, 189, 155, 213, 343, 106, 330, - 72, 179, 192, 33, 181, 155, 190, 197, 198, 199, - 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, - 210, 211, 185, 301, 258, 212, 156, 214, 215, 188, - 256, 183, 290, 191, 252, 164, 155, 289, 300, 218, - 223, 79, 157, 217, 7, 299, 312, 257, 163, 251, - 233, 78, 288, 255, 182, 254, 239, 156, 216, 180, - 220, 124, 172, 120, 147, 311, 314, 171, 119, 261, - 287, 153, 154, 157, 279, 280, 79, 147, 283, 310, - 170, 118, 159, 10, 235, 236, 78, 309, 237, 147, - 308, 307, 306, 74, 76, 305, 250, 286, 304, 224, - 226, 228, 229, 230, 238, 240, 243, 244, 245, 246, - 247, 303, 81, 225, 227, 231, 232, 234, 241, 242, - 48, 34, 1, 248, 249, 122, 73, 121, 285, 47, - 291, 292, 294, 56, 296, 8, 9, 9, 46, 35, - 45, 44, 297, 298, 127, 129, 130, 131, 132, 133, - 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, - 43, 42, 41, 125, 166, 40, 316, 317, 126, 39, - 38, 49, 186, 321, 338, 265, 326, 327, 328, 80, - 325, 184, 219, 332, 331, 334, 333, 75, 115, 149, - 335, 336, 100, 51, 72, 337, 53, 55, 222, 22, - 52, 339, 50, 167, 111, 0, 54, 0, 0, 0, - 0, 344, 0, 0, 0, 0, 0, 0, 82, 84, - 0, 70, 0, 0, 0, 0, 0, 18, 19, 93, - 94, 20, 0, 96, 97, 99, 83, 71, 0, 0, - 0, 0, 58, 59, 60, 61, 62, 63, 64, 65, - 66, 67, 68, 69, 0, 0, 0, 13, 98, 0, - 0, 24, 0, 30, 0, 0, 31, 32, 36, 100, - 51, 72, 0, 53, 267, 0, 22, 52, 0, 0, - 0, 266, 0, 54, 0, 270, 271, 269, 276, 278, - 275, 277, 272, 273, 274, 0, 84, 0, 70, 0, - 0, 0, 0, 0, 18, 19, 93, 94, 20, 0, - 96, 0, 99, 83, 71, 0, 0, 0, 0, 58, - 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, - 69, 0, 0, 0, 13, 98, 0, 0, 24, 0, - 30, 0, 0, 31, 32, 51, 72, 0, 53, 320, - 0, 22, 52, 0, 0, 0, 0, 0, 54, 0, - 270, 271, 269, 276, 278, 275, 277, 272, 273, 274, - 0, 0, 0, 70, 0, 0, 17, 72, 0, 18, - 19, 0, 22, 20, 0, 0, 0, 0, 0, 71, - 0, 0, 0, 0, 58, 59, 60, 61, 62, 63, - 64, 65, 66, 67, 68, 69, 0, 0, 0, 13, - 18, 19, 0, 24, 20, 30, 0, 0, 31, 32, - 0, 0, 0, 0, 0, 11, 12, 14, 15, 16, - 21, 23, 25, 26, 27, 28, 29, 17, 33, 0, - 13, 0, 0, 22, 24, 0, 30, 0, 0, 31, - 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 155, 330, 328, 274, 335, 152, 225, 39, 191, 148, + 288, 287, 156, 117, 81, 177, 227, 106, 105, 6, + 154, 108, 107, 197, 132, 198, 237, 109, 199, 200, + 159, 59, 243, 325, 324, 110, 321, 159, 189, 268, + 348, 301, 265, 127, 159, 192, 349, 264, 290, 195, + 176, 160, 159, 269, 308, 175, 319, 195, 160, 347, + 239, 240, 346, 112, 241, 113, 299, 161, 174, 270, + 263, 111, 254, 160, 161, 228, 230, 232, 233, 234, + 242, 244, 247, 248, 249, 250, 251, 255, 256, 161, + 114, 229, 231, 235, 236, 238, 245, 246, 108, 266, + 258, 252, 253, 329, 109, 157, 158, 159, 2, 3, + 4, 5, 307, 160, 162, 257, 262, 299, 172, 166, + 169, 217, 104, 164, 110, 165, 150, 306, 193, 161, + 178, 104, 179, 151, 305, 183, 196, 179, 185, 261, + 194, 201, 202, 203, 204, 205, 206, 207, 208, 209, + 210, 211, 212, 213, 214, 215, 128, 227, 88, 216, + 120, 218, 219, 100, 336, 103, 168, 237, 97, 98, + 118, 181, 100, 243, 103, 87, 181, 224, 259, 167, + 149, 180, 182, 121, 187, 76, 180, 182, 120, 260, + 102, 35, 124, 7, 10, 296, 151, 123, 118, 102, + 295, 239, 240, 267, 78, 241, 116, 186, 285, 286, + 122, 121, 289, 254, 318, 294, 228, 230, 232, 233, + 234, 242, 244, 247, 248, 249, 250, 251, 255, 256, + 317, 292, 229, 231, 235, 236, 238, 245, 246, 316, + 315, 314, 252, 253, 133, 134, 135, 136, 137, 138, + 139, 140, 141, 142, 143, 144, 145, 146, 147, 313, + 312, 311, 310, 309, 320, 293, 297, 298, 300, 273, + 302, 222, 151, 8, 85, 221, 272, 37, 303, 304, + 276, 277, 275, 282, 284, 281, 283, 278, 279, 280, + 220, 163, 126, 50, 125, 36, 1, 291, 151, 77, + 83, 49, 322, 323, 48, 83, 47, 104, 46, 327, + 82, 131, 332, 333, 334, 82, 331, 45, 184, 338, + 337, 340, 339, 80, 44, 43, 341, 342, 129, 53, + 76, 343, 55, 86, 88, 22, 54, 345, 170, 171, + 42, 130, 56, 41, 97, 98, 40, 350, 100, 101, + 103, 87, 58, 51, 190, 9, 9, 74, 344, 271, + 84, 188, 223, 18, 19, 79, 119, 20, 153, 57, + 226, 52, 115, 75, 0, 102, 0, 0, 60, 61, + 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, + 72, 73, 0, 0, 0, 13, 0, 0, 0, 24, + 0, 30, 0, 0, 31, 32, 38, 0, 53, 76, + 0, 55, 326, 0, 22, 54, 0, 0, 0, 0, + 0, 56, 0, 276, 277, 275, 282, 284, 281, 283, + 278, 279, 280, 0, 0, 0, 74, 0, 0, 0, + 0, 0, 18, 19, 0, 0, 20, 0, 0, 0, + 0, 0, 75, 0, 0, 0, 0, 60, 61, 62, + 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, + 73, 0, 0, 0, 13, 0, 0, 0, 24, 0, + 30, 0, 0, 31, 32, 53, 76, 0, 55, 0, + 0, 22, 54, 0, 0, 0, 0, 0, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 18, 19, 0, 0, 20, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 11, 12, 14, 15, - 16, 21, 23, 25, 26, 27, 28, 29, 100, 0, - 0, 13, 0, 0, 0, 24, 169, 30, 0, 0, - 31, 32, 0, 0, 0, 0, 0, 100, 0, 0, - 0, 0, 0, 0, 82, 84, 85, 0, 86, 87, - 88, 89, 90, 91, 92, 93, 94, 95, 0, 96, - 97, 99, 83, 82, 84, 85, 0, 86, 87, 88, - 89, 90, 91, 92, 93, 94, 95, 0, 96, 97, - 99, 83, 100, 0, 98, 0, 0, 0, 0, 0, + 0, 0, 0, 74, 0, 17, 76, 0, 0, 18, + 19, 22, 0, 20, 0, 0, 0, 0, 0, 75, + 0, 0, 0, 0, 60, 61, 62, 63, 64, 65, + 66, 67, 68, 69, 70, 71, 72, 73, 0, 18, + 19, 13, 0, 20, 0, 24, 0, 30, 0, 0, + 31, 32, 0, 0, 11, 12, 14, 15, 16, 21, + 23, 25, 26, 27, 28, 29, 33, 34, 17, 35, + 0, 13, 0, 0, 22, 24, 0, 30, 0, 0, + 31, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 100, 0, 98, 0, 0, 0, 0, 82, 84, - 85, 0, 86, 87, 88, 0, 90, 91, 92, 93, - 94, 95, 0, 96, 97, 99, 83, 82, 84, 85, - 0, 86, 87, 0, 0, 90, 91, 0, 93, 94, - 95, 0, 96, 97, 99, 83, 0, 0, 98, 0, + 0, 0, 18, 19, 0, 0, 20, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 11, 12, 14, + 15, 16, 21, 23, 25, 26, 27, 28, 29, 33, + 34, 104, 0, 0, 13, 0, 0, 0, 24, 173, + 30, 0, 0, 31, 32, 0, 0, 0, 0, 0, + 104, 0, 0, 0, 0, 0, 0, 86, 88, 89, + 0, 90, 91, 92, 93, 94, 95, 96, 97, 98, + 99, 0, 100, 101, 103, 87, 86, 88, 89, 0, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + 0, 100, 101, 103, 87, 104, 0, 0, 0, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 98, + 0, 0, 0, 0, 104, 0, 0, 0, 102, 0, + 0, 86, 88, 89, 0, 90, 91, 92, 0, 94, + 95, 96, 97, 98, 99, 0, 100, 101, 103, 87, + 86, 88, 89, 0, 90, 91, 0, 0, 94, 95, + 0, 97, 98, 99, 0, 100, 101, 103, 87, 0, + 0, 0, 0, 102, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 102, } var yyPact = [...]int16{ - 17, 164, 555, 555, 388, 494, -1000, -1000, -1000, 120, + 17, 183, 566, 566, 396, 503, -1000, -1000, -1000, 178, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, - -1000, -1000, -1000, 204, -1000, 240, -1000, 633, -1000, -1000, + -1000, -1000, -1000, -1000, -1000, 303, -1000, 272, -1000, 646, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, - 29, 113, -1000, 463, -1000, 463, 117, -1000, -1000, -1000, + -1000, -1000, 20, 109, -1000, 473, -1000, 473, 172, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, - -1000, -1000, 47, -1000, -1000, 191, -1000, -1000, 253, -1000, - 19, -1000, -49, -49, -49, -49, -49, -49, -49, -49, - -49, -49, -49, -49, -49, -49, -49, -49, 36, 116, - 210, 113, -60, -1000, 163, 163, 311, -1000, 614, 20, - -1000, 190, -1000, -1000, 69, 48, -1000, -1000, -1000, 169, - -1000, 159, -1000, 147, 463, -1000, -58, -53, -1000, 463, - 463, 463, 463, 463, 463, 463, 463, 463, 463, 463, - 463, 463, 463, 463, -1000, 185, -1000, -1000, -1000, 111, - -1000, -1000, -1000, -1000, -1000, -1000, 55, 55, 167, -1000, - -1000, -1000, -1000, 168, -1000, -1000, 157, -1000, 633, -1000, - -1000, 35, -1000, 158, -1000, -1000, -1000, -1000, -1000, 152, - -1000, -1000, -1000, -1000, -1000, 27, 2, 1, -1000, -1000, - -1000, 387, 385, 163, 163, 163, 163, 20, 20, 308, - 308, 308, 697, 678, 308, 308, 697, 20, 20, 308, - 20, 385, -1000, 24, -1000, -1000, -1000, 198, -1000, 160, + -1000, -1000, -1000, -1000, -1000, -1000, 186, -1000, -1000, 190, + -1000, -1000, 290, -1000, 19, -1000, -53, -53, -53, -53, + -53, -53, -53, -53, -53, -53, -53, -53, -53, -53, + -53, -53, 124, 18, 289, 109, -57, -1000, 164, 164, + 317, -1000, 627, 108, -1000, 48, -1000, -1000, 128, 133, + -1000, -1000, -1000, 298, -1000, 182, -1000, 33, 473, -1000, + -58, -51, -1000, 473, 473, 473, 473, 473, 473, 473, + 473, 473, 473, 473, 473, 473, 473, 473, -1000, 187, + -1000, -1000, -1000, 106, -1000, -1000, -1000, -1000, -1000, -1000, + 88, 88, 269, -1000, -1000, -1000, -1000, 155, -1000, -1000, + 93, -1000, 646, -1000, -1000, 158, -1000, 114, -1000, -1000, + -1000, -1000, -1000, 45, -1000, -1000, -1000, -1000, -1000, 16, + 73, 13, -1000, -1000, -1000, 252, 117, 164, 164, 164, + 164, 108, 108, 293, 293, 293, 710, 691, 293, 293, + 710, 108, 108, 293, 108, 117, -1000, 26, -1000, -1000, + -1000, 263, -1000, 193, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, - -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, - -1000, -1000, 463, -1000, -1000, -1000, -1000, -1000, -1000, 59, - 59, 22, 59, 104, 104, 151, 100, -1000, -1000, 235, - 222, 219, 216, 215, 214, 211, 203, 189, 170, -1000, - -1000, -1000, -1000, -1000, -1000, 41, 194, -1000, -1000, 18, - -1000, 633, -1000, -1000, -1000, 59, -1000, 13, 9, 462, - -1000, -1000, -1000, 14, 10, 55, 55, 55, 115, 115, - 14, 115, 14, -1000, -1000, -1000, -1000, -1000, 59, 59, - -1000, -1000, -1000, 59, -1000, -1000, -1000, -1000, -1000, -1000, - 55, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 30, -1000, - 106, -1000, -1000, -1000, -1000, + -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 473, -1000, + -1000, -1000, -1000, -1000, -1000, 98, 98, 15, 98, 41, + 41, 110, 37, -1000, -1000, 257, 256, 255, 254, 253, + 235, 234, 233, 224, 208, -1000, -1000, -1000, -1000, -1000, + -1000, 35, 262, -1000, -1000, 14, -1000, 646, -1000, -1000, + -1000, 98, -1000, 8, 7, 395, -1000, -1000, -1000, 47, + 11, 88, 88, 88, 150, 150, 47, 150, 47, -1000, + -1000, -1000, -1000, -1000, 98, 98, -1000, -1000, -1000, 98, + -1000, -1000, -1000, -1000, -1000, -1000, 88, -1000, -1000, -1000, + -1000, -1000, -1000, -1000, 38, -1000, 25, -1000, -1000, -1000, + -1000, } var yyPgo = [...]int16{ - 0, 334, 13, 332, 6, 15, 328, 263, 327, 319, - 318, 213, 265, 317, 14, 312, 10, 11, 311, 309, - 8, 305, 3, 4, 304, 2, 1, 0, 302, 12, - 5, 301, 300, 18, 191, 299, 298, 7, 295, 294, - 17, 293, 56, 292, 291, 290, 274, 271, 270, 268, - 259, 250, 9, 258, 252, 251, + 0, 372, 13, 371, 6, 15, 370, 352, 369, 368, + 366, 194, 273, 365, 14, 362, 10, 11, 361, 360, + 8, 359, 3, 4, 358, 2, 1, 0, 354, 12, + 5, 353, 346, 18, 156, 343, 341, 7, 340, 338, + 17, 328, 31, 325, 324, 317, 311, 308, 306, 304, + 301, 293, 9, 297, 296, 295, } var yyR1 = [...]int8{ @@ -519,18 +542,18 @@ var yyR1 = [...]int8{ 1, 2, 2, 2, 2, 2, 2, 2, 12, 12, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, - 7, 7, 11, 11, 11, 11, 13, 13, 13, 14, - 14, 14, 14, 55, 19, 19, 19, 19, 18, 18, - 18, 18, 18, 18, 18, 18, 18, 28, 28, 28, - 20, 20, 20, 20, 21, 21, 21, 22, 22, 22, - 22, 22, 22, 22, 22, 22, 22, 23, 23, 24, - 24, 24, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 11, 11, 11, 11, 13, 13, + 13, 14, 14, 14, 14, 55, 19, 19, 19, 19, + 18, 18, 18, 18, 18, 18, 18, 18, 18, 28, + 28, 28, 20, 20, 20, 20, 21, 21, 21, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, + 23, 24, 24, 24, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, - 6, 8, 8, 5, 5, 5, 5, 44, 27, 29, - 29, 30, 30, 26, 25, 25, 52, 48, 10, 53, - 53, 17, 17, + 6, 6, 6, 6, 6, 6, 6, 8, 8, 5, + 5, 5, 5, 44, 27, 29, 29, 30, 30, 26, + 25, 25, 52, 48, 10, 53, 53, 17, 17, } var yyR2 = [...]int8{ @@ -546,94 +569,96 @@ var yyR2 = [...]int8{ 2, 3, 3, 1, 3, 3, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 3, 4, 2, 0, 3, 1, 2, 3, - 3, 2, 1, 2, 0, 3, 2, 1, 1, 3, - 1, 3, 4, 1, 3, 5, 5, 1, 1, 1, - 4, 3, 3, 2, 3, 1, 2, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, - 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 3, 4, 2, 0, 3, 1, + 2, 3, 3, 2, 1, 2, 0, 3, 2, 1, + 1, 3, 1, 3, 4, 1, 3, 5, 5, 1, + 1, 1, 4, 3, 3, 2, 3, 1, 2, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, + 3, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, - 2, 1, 1, 1, 2, 1, 1, 1, 1, 0, - 1, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, + 2, 1, 1, 1, 1, 0, 1, 0, 1, } var yyChk = [...]int16{ - -1000, -54, 89, 90, 91, 92, 2, 10, -12, -7, - -11, 61, 62, 76, 63, 64, 65, 12, 46, 47, - 50, 66, 18, 67, 80, 68, 69, 70, 71, 72, - 82, 85, 86, 13, -55, -12, 10, -37, -32, -35, - -38, -43, -44, -45, -47, -48, -49, -50, -51, -31, - -3, 12, 19, 15, 25, -8, -7, -42, 61, 62, - 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, - 40, 56, 13, -51, -11, -13, 20, -14, 12, 2, - -19, 2, 40, 58, 41, 42, 44, 45, 46, 47, - 48, 49, 50, 51, 52, 53, 55, 56, 80, 57, - 14, -33, -40, 2, 76, 82, 15, -40, -37, -37, - -42, -1, 20, -2, 12, -10, 2, 25, 20, 7, - 2, 4, 2, 24, -34, -41, -36, -46, 75, -34, - -34, -34, -34, -34, -34, -34, -34, -34, -34, -34, - -34, -34, -34, -34, -52, 56, 2, 9, -30, -9, - 2, -27, -29, 85, 86, 19, 40, 56, -52, 2, - -40, -33, -16, 15, 2, -16, -39, 22, -37, 22, - 20, 7, 2, -5, 2, 4, 53, 43, 54, -5, - 20, -14, 25, 2, -18, 5, -28, -20, 12, -27, - -29, 16, -37, 79, 81, 77, 78, -37, -37, -37, - -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, - -37, -37, -52, 15, -27, -27, 21, 6, 2, -15, - 22, -4, -6, 2, 61, 75, 62, 76, 63, 64, - 65, 77, 78, 12, 79, 46, 47, 50, 66, 18, - 67, 80, 81, 68, 69, 70, 71, 72, 85, 86, - 58, 22, 7, 20, -2, 25, 2, 25, 2, 26, - 26, -29, 26, 40, 56, -21, 24, 17, -22, 30, - 28, 29, 35, 36, 37, 33, 31, 34, 32, -16, - -16, -17, -16, -17, 22, -53, -52, 2, 22, 7, - 2, -37, -26, 19, -26, 26, -26, -20, -20, 24, - 17, 2, 17, 6, 6, 6, 6, 6, 6, 6, - 6, 6, 6, 21, 2, 22, -4, -26, 26, 26, - 17, -22, -25, 56, -26, -30, -27, -27, -27, -23, - 14, -23, -25, -23, -25, -26, -26, -26, -24, -27, - 24, 21, 2, 21, -27, + -1000, -54, 91, 92, 93, 94, 2, 10, -12, -7, + -11, 61, 62, 78, 63, 64, 65, 12, 46, 47, + 50, 66, 18, 67, 82, 68, 69, 70, 71, 72, + 84, 87, 88, 73, 74, 13, -55, -12, 10, -37, + -32, -35, -38, -43, -44, -45, -47, -48, -49, -50, + -51, -31, -3, 12, 19, 15, 25, -8, -7, -42, + 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 40, 56, 13, -51, -11, -13, + 20, -14, 12, 2, -19, 2, 40, 58, 41, 42, + 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, + 55, 56, 82, 57, 14, -33, -40, 2, 78, 84, + 15, -40, -37, -37, -42, -1, 20, -2, 12, -10, + 2, 25, 20, 7, 2, 4, 2, 24, -34, -41, + -36, -46, 77, -34, -34, -34, -34, -34, -34, -34, + -34, -34, -34, -34, -34, -34, -34, -34, -52, 56, + 2, 9, -30, -9, 2, -27, -29, 87, 88, 19, + 40, 56, -52, 2, -40, -33, -16, 15, 2, -16, + -39, 22, -37, 22, 20, 7, 2, -5, 2, 4, + 53, 43, 54, -5, 20, -14, 25, 2, -18, 5, + -28, -20, 12, -27, -29, 16, -37, 81, 83, 79, + 80, -37, -37, -37, -37, -37, -37, -37, -37, -37, + -37, -37, -37, -37, -37, -37, -52, 15, -27, -27, + 21, 6, 2, -15, 22, -4, -6, 2, 61, 77, + 62, 78, 63, 64, 65, 79, 80, 12, 81, 46, + 47, 50, 66, 18, 67, 82, 83, 68, 69, 70, + 71, 72, 87, 88, 58, 73, 74, 22, 7, 20, + -2, 25, 2, 25, 2, 26, 26, -29, 26, 40, + 56, -21, 24, 17, -22, 30, 28, 29, 35, 36, + 37, 33, 31, 34, 32, -16, -16, -17, -16, -17, + 22, -53, -52, 2, 22, 7, 2, -37, -26, 19, + -26, 26, -26, -20, -20, 24, 17, 2, 17, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 21, + 2, 22, -4, -26, 26, 26, 17, -22, -25, 56, + -26, -30, -27, -27, -27, -23, 14, -23, -25, -23, + -25, -26, -26, -26, -24, -27, 24, 21, 2, 21, + -27, } var yyDef = [...]int16{ - 0, -2, 125, 125, 0, 0, 7, 6, 1, 125, + 0, -2, 127, 127, 0, 0, 7, 6, 1, 127, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, - 119, 120, 121, 0, 2, -2, 3, 4, 8, 9, - 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, - 0, 106, 217, 0, 227, 0, 83, 84, -2, -2, + 119, 120, 121, 122, 123, 0, 2, -2, 3, 4, + 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 0, 106, 223, 0, 233, 0, 83, 84, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, - 211, 212, 0, 5, 98, 0, 124, 127, 0, 132, - 133, 137, 43, 43, 43, 43, 43, 43, 43, 43, - 43, 43, 43, 43, 43, 43, 43, 43, 0, 0, - 0, 0, 22, 23, 0, 0, 0, 60, 0, 81, - 82, 0, 87, 89, 0, 93, 97, 228, 122, 0, - 128, 0, 131, 136, 0, 42, 47, 48, 44, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 67, 0, 69, 226, 70, 0, - 72, 221, 222, 73, 74, 218, 0, 0, 0, 80, - 20, 21, 24, 0, 54, 25, 0, 62, 64, 66, - 85, 0, 90, 0, 96, 213, 214, 215, 216, 0, - 123, 126, 129, 130, 135, 138, 140, 143, 147, 148, - 149, 0, 26, 0, 0, -2, -2, 27, 28, 29, - 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, - 40, 41, 68, 0, 219, 220, 75, -2, 79, 0, - 53, 56, 58, 59, 184, 185, 186, 187, 188, 189, + -2, -2, -2, -2, 217, 218, 0, 5, 98, 0, + 126, 129, 0, 134, 135, 139, 43, 43, 43, 43, + 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, + 43, 43, 0, 0, 0, 0, 22, 23, 0, 0, + 0, 60, 0, 81, 82, 0, 87, 89, 0, 93, + 97, 234, 124, 0, 130, 0, 133, 138, 0, 42, + 47, 48, 44, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 67, 0, + 69, 232, 70, 0, 72, 227, 228, 73, 74, 224, + 0, 0, 0, 80, 20, 21, 24, 0, 54, 25, + 0, 62, 64, 66, 85, 0, 90, 0, 96, 219, + 220, 221, 222, 0, 125, 128, 131, 132, 137, 140, + 142, 145, 149, 150, 151, 0, 26, 0, 0, -2, + -2, 27, 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 68, 0, 225, 226, + 75, -2, 79, 0, 53, 56, 58, 59, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, - 210, 61, 65, 86, 88, 91, 95, 92, 94, 0, - 0, 0, 0, 0, 0, 0, 0, 153, 155, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, - 46, 49, 232, 50, 71, 0, -2, 78, 51, 0, - 57, 63, 139, 223, 141, 0, 144, 0, 0, 0, - 151, 156, 152, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 76, 77, 52, 55, 142, 0, 0, - 150, 154, 157, 0, 225, 158, 159, 160, 161, 162, - 0, 163, 164, 165, 166, 145, 146, 224, 0, 170, - 0, 168, 171, 167, 169, + 210, 211, 212, 213, 214, 215, 216, 61, 65, 86, + 88, 91, 95, 92, 94, 0, 0, 0, 0, 0, + 0, 0, 0, 155, 157, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 45, 46, 49, 238, 50, + 71, 0, -2, 78, 51, 0, 57, 63, 141, 229, + 143, 0, 146, 0, 0, 0, 153, 158, 154, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 76, + 77, 52, 55, 144, 0, 0, 152, 156, 159, 0, + 231, 160, 161, 162, 163, 164, 0, 165, 166, 167, + 168, 147, 148, 230, 0, 172, 0, 170, 173, 169, + 171, } var yyTok1 = [...]int8{ @@ -650,7 +675,7 @@ var yyTok2 = [...]int8{ 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, - 92, 93, + 92, 93, 94, 95, } var yyTok3 = [...]int8{ @@ -1506,66 +1531,66 @@ yydefault: { yyVAL.labels = yyDollar[1].labels } - case 122: + case 124: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.labels = labels.New(yyDollar[2].lblList...) } - case 123: + case 125: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.labels = labels.New(yyDollar[2].lblList...) } - case 124: + case 126: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.labels = labels.New() } - case 125: + case 127: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.labels = labels.New() } - case 126: + case 128: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.lblList = append(yyDollar[1].lblList, yyDollar[3].label) } - case 127: + case 129: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.lblList = []labels.Label{yyDollar[1].label} } - case 128: + case 130: yyDollar = yyS[yypt-2 : yypt+1] { yylex.(*parser).unexpected("label set", "\",\" or \"}\"") yyVAL.lblList = yyDollar[1].lblList } - case 129: + case 131: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.label = labels.Label{Name: yyDollar[1].item.Val, Value: yylex.(*parser).unquoteString(yyDollar[3].item.Val)} } - case 130: + case 132: yyDollar = yyS[yypt-3 : yypt+1] { yylex.(*parser).unexpected("label set", "string") yyVAL.label = labels.Label{} } - case 131: + case 133: yyDollar = yyS[yypt-2 : yypt+1] { yylex.(*parser).unexpected("label set", "\"=\"") yyVAL.label = labels.Label{} } - case 132: + case 134: yyDollar = yyS[yypt-1 : yypt+1] { yylex.(*parser).unexpected("label set", "identifier or \"}\"") yyVAL.label = labels.Label{} } - case 133: + case 135: yyDollar = yyS[yypt-2 : yypt+1] { yylex.(*parser).generatedParserResult = &seriesDescription{ @@ -1573,33 +1598,33 @@ yydefault: values: yyDollar[2].series, } } - case 134: + case 136: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.series = []SequenceValue{} } - case 135: + case 137: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.series = append(yyDollar[1].series, yyDollar[3].series...) } - case 136: + case 138: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.series = yyDollar[1].series } - case 137: + case 139: yyDollar = yyS[yypt-1 : yypt+1] { yylex.(*parser).unexpected("series values", "") yyVAL.series = nil } - case 138: + case 140: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.series = []SequenceValue{{Omitted: true}} } - case 139: + case 141: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.series = []SequenceValue{} @@ -1607,12 +1632,12 @@ yydefault: yyVAL.series = append(yyVAL.series, SequenceValue{Omitted: true}) } } - case 140: + case 142: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.series = []SequenceValue{{Value: yyDollar[1].float}} } - case 141: + case 143: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.series = []SequenceValue{} @@ -1621,7 +1646,7 @@ yydefault: yyVAL.series = append(yyVAL.series, SequenceValue{Value: yyDollar[1].float}) } } - case 142: + case 144: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.series = []SequenceValue{} @@ -1631,12 +1656,12 @@ yydefault: yyDollar[1].float += yyDollar[2].float } } - case 143: + case 145: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.series = []SequenceValue{{Histogram: yyDollar[1].histogram}} } - case 144: + case 146: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.series = []SequenceValue{} @@ -1646,7 +1671,7 @@ yydefault: //$1 += $2 } } - case 145: + case 147: yyDollar = yyS[yypt-5 : yypt+1] { val, err := yylex.(*parser).histogramsIncreaseSeries(yyDollar[1].histogram, yyDollar[3].histogram, yyDollar[5].uint) @@ -1655,7 +1680,7 @@ yydefault: } yyVAL.series = val } - case 146: + case 148: yyDollar = yyS[yypt-5 : yypt+1] { val, err := yylex.(*parser).histogramsDecreaseSeries(yyDollar[1].histogram, yyDollar[3].histogram, yyDollar[5].uint) @@ -1664,7 +1689,7 @@ yydefault: } yyVAL.series = val } - case 147: + case 149: yyDollar = yyS[yypt-1 : yypt+1] { if yyDollar[1].item.Val != "stale" { @@ -1672,124 +1697,124 @@ yydefault: } yyVAL.float = math.Float64frombits(value.StaleNaN) } - case 150: + case 152: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.histogram = yylex.(*parser).buildHistogramFromMap(&yyDollar[2].descriptors) } - case 151: + case 153: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.histogram = yylex.(*parser).buildHistogramFromMap(&yyDollar[2].descriptors) } - case 152: + case 154: yyDollar = yyS[yypt-3 : yypt+1] { m := yylex.(*parser).newMap() yyVAL.histogram = yylex.(*parser).buildHistogramFromMap(&m) } - case 153: + case 155: yyDollar = yyS[yypt-2 : yypt+1] { m := yylex.(*parser).newMap() yyVAL.histogram = yylex.(*parser).buildHistogramFromMap(&m) } - case 154: + case 156: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.descriptors = *(yylex.(*parser).mergeMaps(&yyDollar[1].descriptors, &yyDollar[3].descriptors)) } - case 155: + case 157: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.descriptors = yyDollar[1].descriptors } - case 156: + case 158: yyDollar = yyS[yypt-2 : yypt+1] { yylex.(*parser).unexpected("histogram description", "histogram description key, e.g. buckets:[5 10 7]") } - case 157: - yyDollar = yyS[yypt-3 : yypt+1] - { - yyVAL.descriptors = yylex.(*parser).newMap() - yyVAL.descriptors["schema"] = yyDollar[3].int - } - case 158: - yyDollar = yyS[yypt-3 : yypt+1] - { - yyVAL.descriptors = yylex.(*parser).newMap() - yyVAL.descriptors["sum"] = yyDollar[3].float - } case 159: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.descriptors = yylex.(*parser).newMap() - yyVAL.descriptors["count"] = yyDollar[3].float + yyVAL.descriptors["schema"] = yyDollar[3].int } case 160: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.descriptors = yylex.(*parser).newMap() - yyVAL.descriptors["z_bucket"] = yyDollar[3].float + yyVAL.descriptors["sum"] = yyDollar[3].float } case 161: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.descriptors = yylex.(*parser).newMap() - yyVAL.descriptors["z_bucket_w"] = yyDollar[3].float + yyVAL.descriptors["count"] = yyDollar[3].float } case 162: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.descriptors = yylex.(*parser).newMap() - yyVAL.descriptors["custom_values"] = yyDollar[3].bucket_set + yyVAL.descriptors["z_bucket"] = yyDollar[3].float } case 163: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.descriptors = yylex.(*parser).newMap() - yyVAL.descriptors["buckets"] = yyDollar[3].bucket_set + yyVAL.descriptors["z_bucket_w"] = yyDollar[3].float } case 164: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.descriptors = yylex.(*parser).newMap() - yyVAL.descriptors["offset"] = yyDollar[3].int + yyVAL.descriptors["custom_values"] = yyDollar[3].bucket_set } case 165: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.descriptors = yylex.(*parser).newMap() - yyVAL.descriptors["n_buckets"] = yyDollar[3].bucket_set + yyVAL.descriptors["buckets"] = yyDollar[3].bucket_set } case 166: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.descriptors = yylex.(*parser).newMap() - yyVAL.descriptors["n_offset"] = yyDollar[3].int + yyVAL.descriptors["offset"] = yyDollar[3].int } case 167: - yyDollar = yyS[yypt-4 : yypt+1] + yyDollar = yyS[yypt-3 : yypt+1] { - yyVAL.bucket_set = yyDollar[2].bucket_set + yyVAL.descriptors = yylex.(*parser).newMap() + yyVAL.descriptors["n_buckets"] = yyDollar[3].bucket_set } case 168: yyDollar = yyS[yypt-3 : yypt+1] { - yyVAL.bucket_set = yyDollar[2].bucket_set + yyVAL.descriptors = yylex.(*parser).newMap() + yyVAL.descriptors["n_offset"] = yyDollar[3].int } case 169: + yyDollar = yyS[yypt-4 : yypt+1] + { + yyVAL.bucket_set = yyDollar[2].bucket_set + } + case 170: + yyDollar = yyS[yypt-3 : yypt+1] + { + yyVAL.bucket_set = yyDollar[2].bucket_set + } + case 171: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.bucket_set = append(yyDollar[1].bucket_set, yyDollar[3].float) } - case 170: + case 172: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.bucket_set = []float64{yyDollar[1].float} } - case 217: + case 223: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.node = &NumberLiteral{ @@ -1797,22 +1822,22 @@ yydefault: PosRange: yyDollar[1].item.PositionRange(), } } - case 218: + case 224: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.float = yylex.(*parser).number(yyDollar[1].item.Val) } - case 219: + case 225: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.float = yyDollar[2].float } - case 220: + case 226: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.float = -yyDollar[2].float } - case 223: + case 229: yyDollar = yyS[yypt-1 : yypt+1] { var err error @@ -1821,17 +1846,17 @@ yydefault: yylex.(*parser).addParseErrf(yyDollar[1].item.PositionRange(), "invalid repetition in series values: %s", err) } } - case 224: + case 230: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.int = -int64(yyDollar[2].uint) } - case 225: + case 231: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.int = int64(yyDollar[1].uint) } - case 226: + case 232: yyDollar = yyS[yypt-1 : yypt+1] { var err error @@ -1840,7 +1865,7 @@ yydefault: yylex.(*parser).addParseErr(yyDollar[1].item.PositionRange(), err) } } - case 227: + case 233: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.node = &StringLiteral{ @@ -1848,7 +1873,7 @@ yydefault: PosRange: yyDollar[1].item.PositionRange(), } } - case 228: + case 234: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.item = Item{ @@ -1857,12 +1882,12 @@ yydefault: Val: yylex.(*parser).unquoteString(yyDollar[1].item.Val), } } - case 229: + case 235: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.duration = 0 } - case 231: + case 237: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.strings = nil diff --git a/promql/parser/lex.go b/promql/parser/lex.go index c8ea4c46e8..8c7fbb89b9 100644 --- a/promql/parser/lex.go +++ b/promql/parser/lex.go @@ -65,7 +65,7 @@ func (i ItemType) IsAggregator() bool { return i > aggregatorsStart && i < aggre // IsAggregatorWithParam returns true if the Item is an aggregator that takes a parameter. // Returns false otherwise. func (i ItemType) IsAggregatorWithParam() bool { - return i == TOPK || i == BOTTOMK || i == COUNT_VALUES || i == QUANTILE + return i == TOPK || i == BOTTOMK || i == COUNT_VALUES || i == QUANTILE || i == LIMITK || i == LIMIT_RATIO } // IsKeyword returns true if the Item corresponds to a keyword. @@ -118,6 +118,8 @@ var key = map[string]ItemType{ "bottomk": BOTTOMK, "count_values": COUNT_VALUES, "quantile": QUANTILE, + "limitk": LIMITK, + "limit_ratio": LIMIT_RATIO, // Keywords. "offset": OFFSET, diff --git a/promql/parser/parse.go b/promql/parser/parse.go index f3fa27f84e..6f73e2427b 100644 --- a/promql/parser/parse.go +++ b/promql/parser/parse.go @@ -447,6 +447,10 @@ func (p *parser) newAggregateExpr(op Item, modifier, args Node) (ret *AggregateE desiredArgs := 1 if ret.Op.IsAggregatorWithParam() { + if !EnableExperimentalFunctions && (ret.Op == LIMITK || ret.Op == LIMIT_RATIO) { + p.addParseErrf(ret.PositionRange(), "limitk() and limit_ratio() are experimental and must be enabled with --enable-feature=promql-experimental-functions") + return + } desiredArgs = 2 ret.Param = arguments[0] @@ -672,7 +676,7 @@ func (p *parser) checkAST(node Node) (typ ValueType) { p.addParseErrf(n.PositionRange(), "aggregation operator expected in aggregation expression but got %q", n.Op) } p.expectType(n.Expr, ValueTypeVector, "aggregation expression") - if n.Op == TOPK || n.Op == BOTTOMK || n.Op == QUANTILE { + if n.Op == TOPK || n.Op == BOTTOMK || n.Op == QUANTILE || n.Op == LIMITK || n.Op == LIMIT_RATIO { p.expectType(n.Param, ValueTypeScalar, "aggregation parameter") } if n.Op == COUNT_VALUES { diff --git a/promql/promql_test.go b/promql/promql_test.go index 7bafc02e3b..a423f90ee8 100644 --- a/promql/promql_test.go +++ b/promql/promql_test.go @@ -23,6 +23,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/promql/promqltest" "github.com/prometheus/prometheus/util/teststorage" ) @@ -45,6 +46,8 @@ func TestConcurrentRangeQueries(t *testing.T) { MaxSamples: 50000000, Timeout: 100 * time.Second, } + // Enable experimental functions testing + parser.EnableExperimentalFunctions = true engine := promql.NewEngine(opts) const interval = 10000 // 10s interval. diff --git a/promql/promqltest/testdata/limit.test b/promql/promqltest/testdata/limit.test new file mode 100644 index 0000000000..0ab363f9ae --- /dev/null +++ b/promql/promqltest/testdata/limit.test @@ -0,0 +1,119 @@ +# Tests for limitk +# +# NB: those many `and http_requests` are to ensure that the series _are_ indeed +# a subset of the original series. +load 5m + http_requests{job="api-server", instance="0", group="production"} 0+10x10 + http_requests{job="api-server", instance="1", group="production"} 0+20x10 + http_requests{job="api-server", instance="0", group="canary"} 0+30x10 + http_requests{job="api-server", instance="1", group="canary"} 0+40x10 + http_requests{job="api-server", instance="2", group="canary"} 0+50x10 + http_requests{job="api-server", instance="3", group="canary"} 0+60x10 + +eval instant at 50m count(limitk by (group) (0, http_requests)) +# empty + +eval instant at 50m count(limitk by (group) (-1, http_requests)) +# empty + +# Exercise k==1 special case (as sample is added before the main series loop +eval instant at 50m count(limitk by (group) (1, http_requests) and http_requests) + {} 2 + +eval instant at 50m count(limitk by (group) (2, http_requests) and http_requests) + {} 4 + +eval instant at 50m count(limitk(100, http_requests) and http_requests) + {} 6 + +# Exercise k==1 special case (as sample is added before the main series loop +eval instant at 50m count(limitk by (group) (1, http_requests) and http_requests) + {} 2 + +eval instant at 50m count(limitk by (group) (2, http_requests) and http_requests) + {} 4 + +eval instant at 50m count(limitk(100, http_requests) and http_requests) + {} 6 + +# limit_ratio +eval range from 0 to 50m step 5m count(limit_ratio(0.0, http_requests)) +# empty + +# limitk(2, ...) should always return a 2-count subset of the timeseries (hence the AND'ing) +eval range from 0 to 50m step 5m count(limitk(2, http_requests) and http_requests) + {} 2+0x10 + +# Tests for limit_ratio +# +# NB: below 0.5 ratio will depend on some hashing "luck" (also there's no guarantee that +# an integer comes from: total number of series * ratio), as it depends on: +# +# * ratioLimit = [0.0, 1.0]: +# float64(sample.Metric.Hash()) / float64MaxUint64 < Ratio ? +# * ratioLimit = [-1.0, 1.0): +# float64(sample.Metric.Hash()) / float64MaxUint64 >= (1.0 + Ratio) ? +# +# See `AddRatioSample()` in promql/engine.go for more details. + +# Half~ish samples: verify we get "near" 3 (of 0.5 * 6) +eval range from 0 to 50m step 5m count(limit_ratio(0.5, http_requests) and http_requests) <= bool (3+1) + {} 1+0x10 + +eval range from 0 to 50m step 5m count(limit_ratio(0.5, http_requests) and http_requests) >= bool (3-1) + {} 1+0x10 + +# All samples +eval range from 0 to 50m step 5m count(limit_ratio(1.0, http_requests) and http_requests) + {} 6+0x10 + +# All samples +eval range from 0 to 50m step 5m count(limit_ratio(-1.0, http_requests) and http_requests) + {} 6+0x10 + +# Capped to 1.0 -> all samples +eval_warn range from 0 to 50m step 5m count(limit_ratio(1.1, http_requests) and http_requests) + {} 6+0x10 + +# Capped to -1.0 -> all samples +eval_warn range from 0 to 50m step 5m count(limit_ratio(-1.1, http_requests) and http_requests) + {} 6+0x10 + +# Verify that limit_ratio(value) and limit_ratio(1.0-value) return the "complement" of each other +# Complement below for [0.2, -0.8] +# +# Complement 1of2: `or` should return all samples +eval range from 0 to 50m step 5m count(limit_ratio(0.2, http_requests) or limit_ratio(-0.8, http_requests)) + {} 6+0x10 + +# Complement 2of2: `and` should return no samples +eval range from 0 to 50m step 5m count(limit_ratio(0.2, http_requests) and limit_ratio(-0.8, http_requests)) +# empty + +# Complement below for [0.5, -0.5] +eval range from 0 to 50m step 5m count(limit_ratio(0.5, http_requests) or limit_ratio(-0.5, http_requests)) + {} 6+0x10 + +eval range from 0 to 50m step 5m count(limit_ratio(0.5, http_requests) and limit_ratio(-0.5, http_requests)) +# empty + +# Complement below for [0.8, -0.2] +eval range from 0 to 50m step 5m count(limit_ratio(0.8, http_requests) or limit_ratio(-0.2, http_requests)) + {} 6+0x10 + +eval range from 0 to 50m step 5m count(limit_ratio(0.8, http_requests) and limit_ratio(-0.2, http_requests)) +# empty + +# Complement below for [some_ratio, 1.0 - some_ratio], some_ratio derived from time(), +# using a small prime number to avoid rounded ratio values, and a small set of them. +eval range from 0 to 50m step 5m count(limit_ratio(time() % 17/17, http_requests) or limit_ratio(1.0 - (time() % 17/17), http_requests)) + {} 6+0x10 + +eval range from 0 to 50m step 5m count(limit_ratio(time() % 17/17, http_requests) and limit_ratio(1.0 - (time() % 17/17), http_requests)) +# empty + +# Poor man's normality check: ok (loaded samples follow a nice linearity over labels and time) +# The check giving: 1 (i.e. true) +eval range from 0 to 50m step 5m abs(avg(limit_ratio(0.5, http_requests)) - avg(limit_ratio(-0.5, http_requests))) <= bool stddev(http_requests) + {} 1+0x10 + diff --git a/util/annotations/annotations.go b/util/annotations/annotations.go index 6415f44744..40a20e4b92 100644 --- a/util/annotations/annotations.go +++ b/util/annotations/annotations.go @@ -116,6 +116,7 @@ var ( PromQLInfo = errors.New("PromQL info") PromQLWarning = errors.New("PromQL warning") + InvalidRatioWarning = fmt.Errorf("%w: ratio value should be between -1 and 1", PromQLWarning) InvalidQuantileWarning = fmt.Errorf("%w: quantile value should be between 0 and 1", PromQLWarning) BadBucketLabelWarning = fmt.Errorf("%w: bucket label %q is missing or has a malformed value", PromQLWarning, model.BucketLabel) MixedFloatsHistogramsWarning = fmt.Errorf("%w: encountered a mix of histograms and floats for", PromQLWarning) @@ -155,6 +156,15 @@ func NewInvalidQuantileWarning(q float64, pos posrange.PositionRange) error { } } +// NewInvalidQuantileWarning is used when the user specifies an invalid ratio +// value, i.e. a float that is outside the range [-1, 1] or NaN. +func NewInvalidRatioWarning(q, to float64, pos posrange.PositionRange) error { + return annoErr{ + PositionRange: pos, + Err: fmt.Errorf("%w, got %g, capping to %g", InvalidRatioWarning, q, to), + } +} + // NewBadBucketLabelWarning is used when there is an error parsing the bucket label // of a classic histogram. func NewBadBucketLabelWarning(metricName, label string, pos posrange.PositionRange) error { diff --git a/web/ui/module/codemirror-promql/src/complete/promql.terms.ts b/web/ui/module/codemirror-promql/src/complete/promql.terms.ts index 9d5d55f60b..f4f934f500 100644 --- a/web/ui/module/codemirror-promql/src/complete/promql.terms.ts +++ b/web/ui/module/codemirror-promql/src/complete/promql.terms.ts @@ -544,6 +544,18 @@ export const aggregateOpTerms = [ info: 'Group series, while setting the sample value to 1', type: 'keyword', }, + { + label: 'limitk', + detail: 'aggregation', + info: 'Sample k elements', + type: 'keyword', + }, + { + label: 'limit_ratio', + detail: 'aggregation', + info: 'Sample given ratio of elements', + type: 'keyword', + }, { label: 'max', detail: 'aggregation', diff --git a/web/ui/module/codemirror-promql/src/parser/parser.ts b/web/ui/module/codemirror-promql/src/parser/parser.ts index fba7b7b6ba..351183d6b5 100644 --- a/web/ui/module/codemirror-promql/src/parser/parser.ts +++ b/web/ui/module/codemirror-promql/src/parser/parser.ts @@ -28,6 +28,8 @@ import { Gtr, Identifier, LabelMatchers, + LimitK, + LimitRatio, Lss, Lte, MatrixSelector, @@ -167,7 +169,13 @@ export class Parser { } this.expectType(params[params.length - 1], ValueType.vector, 'aggregation expression'); // get the parameter of the aggregation operator - if (aggregateOp.type.id === Topk || aggregateOp.type.id === Bottomk || aggregateOp.type.id === Quantile) { + if ( + aggregateOp.type.id === Topk || + aggregateOp.type.id === Bottomk || + aggregateOp.type.id === LimitK || + aggregateOp.type.id === LimitRatio || + aggregateOp.type.id === Quantile + ) { this.expectType(params[0], ValueType.scalar, 'aggregation parameter'); } if (aggregateOp.type.id === CountValues) { diff --git a/web/ui/module/lezer-promql/src/highlight.js b/web/ui/module/lezer-promql/src/highlight.js index 53321c75e3..b8bdab76d1 100644 --- a/web/ui/module/lezer-promql/src/highlight.js +++ b/web/ui/module/lezer-promql/src/highlight.js @@ -22,7 +22,7 @@ export const promQLHighLight = styleTags({ Identifier: tags.variableName, 'Abs Absent AbsentOverTime Acos Acosh Asin Asinh Atan Atanh AvgOverTime Ceil Changes Clamp ClampMax ClampMin Cos Cosh CountOverTime DaysInMonth DayOfMonth DayOfWeek DayOfYear Deg Delta Deriv Exp Floor HistogramAvg HistogramCount HistogramFraction HistogramQuantile HistogramSum HoltWinters Hour Idelta Increase Irate LabelReplace LabelJoin LastOverTime Ln Log10 Log2 MaxOverTime MinOverTime Minute Month Pi PredictLinear PresentOverTime QuantileOverTime Rad Rate Resets Round Scalar Sgn Sin Sinh Sort SortDesc SortByLabel SortByLabelDesc Sqrt StddevOverTime StdvarOverTime SumOverTime Tan Tanh Time Timestamp Vector Year': tags.function(tags.variableName), - 'Avg Bottomk Count Count_values Group Max Min Quantile Stddev Stdvar Sum Topk': tags.operatorKeyword, + 'Avg Bottomk Count Count_values Group LimitK LimitRatio Max Min Quantile Stddev Stdvar Sum Topk': tags.operatorKeyword, 'By Without Bool On Ignoring GroupLeft GroupRight Offset Start End': tags.modifier, 'And Unless Or': tags.logicOperator, 'Sub Add Mul Mod Div Atan2 Eql Neq Lte Lss Gte Gtr EqlRegex EqlSingle NeqRegex Pow At': tags.operator, diff --git a/web/ui/module/lezer-promql/src/promql.grammar b/web/ui/module/lezer-promql/src/promql.grammar index 89aa23c79a..735fede24f 100644 --- a/web/ui/module/lezer-promql/src/promql.grammar +++ b/web/ui/module/lezer-promql/src/promql.grammar @@ -54,6 +54,8 @@ AggregateOp { Max | Min | Quantile | + LimitK | + LimitRatio | Stddev | Stdvar | Sum | @@ -330,6 +332,8 @@ NumberLiteral { Max, Min, Quantile, + LimitK, + LimitRatio, Stddev, Stdvar, Sum, diff --git a/web/ui/module/lezer-promql/src/tokens.js b/web/ui/module/lezer-promql/src/tokens.js index 551040969a..d9e7b1d9b4 100644 --- a/web/ui/module/lezer-promql/src/tokens.js +++ b/web/ui/module/lezer-promql/src/tokens.js @@ -33,6 +33,8 @@ import { On, Or, Quantile, + LimitK, + LimitRatio, Start, Stddev, Stdvar, @@ -67,6 +69,8 @@ const contextualKeywordTokens = { max: Max, min: Min, quantile: Quantile, + limitk: LimitK, + limit_ratio: LimitRatio, stddev: Stddev, stdvar: Stdvar, sum: Sum, From c9bc1c2be0e1532203a2d159cc70a10547d29fcc Mon Sep 17 00:00:00 2001 From: Manik Rana Date: Thu, 4 Jul 2024 17:31:09 +0530 Subject: [PATCH 17/29] Update histogram math (#13680) ui: Fix many edge cases of rendering a histogram --------- Signed-off-by: Manik Rana --- .../src/pages/graph/HistogramChart.tsx | 354 +++++++++++++++--- .../src/pages/graph/HistogramHelpers.ts | 126 +++++++ 2 files changed, 427 insertions(+), 53 deletions(-) create mode 100644 web/ui/react-app/src/pages/graph/HistogramHelpers.ts diff --git a/web/ui/react-app/src/pages/graph/HistogramChart.tsx b/web/ui/react-app/src/pages/graph/HistogramChart.tsx index ae171c5e43..3c6efa38bf 100644 --- a/web/ui/react-app/src/pages/graph/HistogramChart.tsx +++ b/web/ui/react-app/src/pages/graph/HistogramChart.tsx @@ -2,22 +2,104 @@ import React, { FC } from 'react'; import { UncontrolledTooltip } from 'reactstrap'; import { Histogram } from '../../types/types'; import { bucketRangeString } from './DataTable'; +import { + calculateDefaultExpBucketWidth, + findMinPositive, + findMaxNegative, + findZeroAxisLeft, + showZeroAxis, + findZeroBucket, + ScaleType, +} from './HistogramHelpers'; -type ScaleType = 'linear' | 'exponential'; +interface HistogramChartProps { + histogram: Histogram; + index: number; + scale: ScaleType; +} -const HistogramChart: FC<{ histogram: Histogram; index: number; scale: ScaleType }> = ({ index, histogram, scale }) => { +const HistogramChart: FC = ({ index, histogram, scale }) => { const { buckets } = histogram; - const rangeMax = buckets ? parseFloat(buckets[buckets.length - 1][2]) : 0; - const countMax = buckets ? buckets.map((b) => parseFloat(b[3])).reduce((a, b) => Math.max(a, b)) : 0; + if (!buckets || buckets.length === 0) { + return
No data
; + } const formatter = Intl.NumberFormat('en', { notation: 'compact' }); - const positiveBuckets = buckets?.filter((b) => parseFloat(b[1]) >= 0); // we only want to show buckets with range >= 0 - const xLabelTicks = scale === 'linear' ? [0.25, 0.5, 0.75, 1] : [1]; + + // For linear scales, the count of a histogram bucket is represented by its area rather than its height. This means it considers + // both the count and the range (width) of the bucket. For this, we can set the height of the bucket proportional + // to its frequency density (fd). The fd is the count of the bucket divided by the width of the bucket. + const fds = []; + for (const bucket of buckets) { + const left = parseFloat(bucket[1]); + const right = parseFloat(bucket[2]); + const count = parseFloat(bucket[3]); + const width = right - left; + + // This happens when a user want observations of precisely zero to be included in the zero bucket + if (width === 0) { + fds.push(0); + continue; + } + fds.push(count / width); + } + const fdMax = Math.max(...fds); + + const first = buckets[0]; + const last = buckets[buckets.length - 1]; + + const rangeMax = parseFloat(last[2]); + const rangeMin = parseFloat(first[1]); + const countMax = Math.max(...buckets.map((b) => parseFloat(b[3]))); + + const defaultExpBucketWidth = calculateDefaultExpBucketWidth(last, buckets); + + const maxPositive = rangeMax > 0 ? rangeMax : 0; + const minPositive = findMinPositive(buckets); + const maxNegative = findMaxNegative(buckets); + const minNegative = parseFloat(first[1]) < 0 ? parseFloat(first[1]) : 0; + + // Calculate the borders of positive and negative buckets in the exponential scale from left to right + const startNegative = minNegative !== 0 ? -Math.log(Math.abs(minNegative)) : 0; + const endNegative = maxNegative !== 0 ? -Math.log(Math.abs(maxNegative)) : 0; + const startPositive = minPositive !== 0 ? Math.log(minPositive) : 0; + const endPositive = maxPositive !== 0 ? Math.log(maxPositive) : 0; + console.log( + 'startNegative', + startNegative, + 'endNegative', + endNegative, + 'startPositive', + startPositive, + 'endPositive', + endPositive + ); + + // Calculate the width of negative, positive, and all exponential bucket ranges on the x-axis + const xWidthNegative = endNegative - startNegative; + const xWidthPositive = endPositive - startPositive; + const xWidthTotal = xWidthNegative + defaultExpBucketWidth + xWidthPositive; + console.log('xWidthNegative', xWidthNegative, 'xWidthPositive', xWidthPositive, 'xWidthTotal', xWidthTotal); + + const zeroBucketIdx = findZeroBucket(buckets); + const zeroAxisLeft = findZeroAxisLeft( + scale, + rangeMin, + rangeMax, + minPositive, + maxNegative, + zeroBucketIdx, + xWidthNegative, + xWidthTotal, + defaultExpBucketWidth + ); + const zeroAxis = showZeroAxis(zeroAxisLeft); + return (
{[1, 0.75, 0.5, 0.25].map((i) => (
- {formatter.format(countMax * i)} + {scale === 'linear' ? '' : formatter.format(countMax * i)}
))}
@@ -31,62 +113,228 @@ const HistogramChart: FC<{ histogram: Histogram; index: number; scale: ScaleType
-
))} - {positiveBuckets?.map((b, bIdx) => { - const bucketIdx = `bucket-${index}-${bIdx}-${Math.ceil(parseFloat(b[3]) * 100)}`; - const bucketLeft = - scale === 'linear' ? (parseFloat(b[1]) / rangeMax) * 100 + '%' : (bIdx / positiveBuckets.length) * 100 + '%'; - const bucketWidth = - scale === 'linear' - ? ((parseFloat(b[2]) - parseFloat(b[1])) / rangeMax) * 100 + '%' - : 100 / positiveBuckets.length + '%'; - return ( - -
-
- - range: {bucketRangeString(b)} -
- count: {b[3]} -
-
-
- ); - })} +
+
+
+
+ + +
-
- 0 +
+ +
{formatter.format(rangeMin)}
+ {rangeMin < 0 && zeroAxis &&
0
} +
{formatter.format(rangeMax)}
+
- {xLabelTicks.map((i) => ( -
-
{formatter.format(rangeMax * i)}
-
- ))}
); }; +interface RenderHistogramProps { + buckets: [number, string, string, string][]; + scale: ScaleType; + rangeMin: number; + rangeMax: number; + index: number; + fds: number[]; + fdMax: number; + countMax: number; + defaultExpBucketWidth: number; + minPositive: number; + maxNegative: number; + startPositive: number; + startNegative: number; + xWidthNegative: number; + xWidthPositive: number; + xWidthTotal: number; +} + +const RenderHistogramBars: FC = ({ + buckets, + scale, + rangeMin, + rangeMax, + index, + fds, + fdMax, + countMax, + defaultExpBucketWidth, + minPositive, + maxNegative, + startPositive, + startNegative, + xWidthNegative, + xWidthPositive, + xWidthTotal, +}) => { + return ( + + {buckets.map((b, bIdx) => { + const left = parseFloat(b[1]); + const right = parseFloat(b[2]); + const count = parseFloat(b[3]); + const bucketIdx = `bucket-${index}-${bIdx}-${Math.ceil(parseFloat(b[3]) * 100)}`; + + const logWidth = Math.abs(Math.log(Math.abs(right)) - Math.log(Math.abs(left))); + const expBucketWidth = logWidth === 0 ? defaultExpBucketWidth : logWidth; + + let bucketWidth = ''; + let bucketLeft = ''; + let bucketHeight = ''; + + switch (scale) { + case 'linear': + bucketWidth = ((right - left) / (rangeMax - rangeMin)) * 100 + '%'; + bucketLeft = ((left - rangeMin) / (rangeMax - rangeMin)) * 100 + '%'; + if (left === 0 && right === 0) { + bucketLeft = '0%'; // do not render zero-width zero bucket + bucketWidth = '0%'; + } + bucketHeight = (fds[bIdx] / fdMax) * 100 + '%'; + break; + case 'exponential': + let adjust = 0; // if buckets are all positive/negative, we need to remove the width of the zero bucket + if (minPositive === 0 || maxNegative === 0) { + adjust = defaultExpBucketWidth; + } + bucketWidth = (expBucketWidth / (xWidthTotal - adjust)) * 100 + '%'; + if (left < 0) { + // negative buckets boundary + bucketLeft = (-(Math.log(Math.abs(left)) + startNegative) / (xWidthTotal - adjust)) * 100 + '%'; + } else { + // positive buckets boundary + bucketLeft = + ((Math.log(left) - startPositive + defaultExpBucketWidth + xWidthNegative - adjust) / + (xWidthTotal - adjust)) * + 100 + + '%'; + } + if (left < 0 && right > 0) { + // if the bucket crosses the zero axis + bucketLeft = (xWidthNegative / xWidthTotal) * 100 + '%'; + } + if (left === 0 && right === 0) { + // do not render zero width zero bucket + bucketLeft = '0%'; + bucketWidth = '0%'; + } + + bucketHeight = (count / countMax) * 100 + '%'; + break; + default: + throw new Error('Invalid scale'); + } + + console.log( + '[', + left, + ',', + right, + ']', + '\n', + 'fds[bIdx]', + fds[bIdx], + '\n', + 'fdMax', + fdMax, + '\n', + 'bucketIdx', + bucketIdx, + '\n', + 'bucketLeft', + bucketLeft, + '\n', + 'bucketWidth', + bucketWidth, + '\n', + 'bucketHeight', + bucketHeight, + '\n', + 'defaultExpBucketWidth', + defaultExpBucketWidth, + '\n', + 'expBucketWidth', + expBucketWidth, + '\n', + 'startNegative', + startNegative, + '\n', + 'startPositive', + startPositive, + '\n', + 'minPositive', + minPositive, + '\n', + 'maxNegative', + maxNegative, + 'xWidthNegative', + xWidthNegative, + '\n', + 'xWidthTotal', + xWidthTotal, + '\n', + 'xWidthPositive', + xWidthPositive, + '\n' + ); + + return ( + +
+
+ + range: {bucketRangeString(b)} +
+ count: {count} +
+
+
+ ); + })} +
+ ); +}; + export default HistogramChart; diff --git a/web/ui/react-app/src/pages/graph/HistogramHelpers.ts b/web/ui/react-app/src/pages/graph/HistogramHelpers.ts new file mode 100644 index 0000000000..ae1efd308a --- /dev/null +++ b/web/ui/react-app/src/pages/graph/HistogramHelpers.ts @@ -0,0 +1,126 @@ +export type ScaleType = 'linear' | 'exponential'; + +// Calculates a default width of exponential histogram bucket ranges. If the last bucket is [0, 0], +// the width is calculated using the second to last bucket. returns error if the last bucket is [-0, 0], +export function calculateDefaultExpBucketWidth( + last: [number, string, string, string], + buckets: [number, string, string, string][] +): number { + if (parseFloat(last[2]) === 0 || parseFloat(last[1]) === 0) { + if (buckets.length > 1) { + return Math.abs( + Math.log(Math.abs(parseFloat(buckets[buckets.length - 2][2]))) - + Math.log(Math.abs(parseFloat(buckets[buckets.length - 2][1]))) + ); + } else { + throw new Error('Only one bucket in histogram ([-0, 0]). Cannot calculate defaultExpBucketWidth.'); + } + } else { + return Math.abs(Math.log(Math.abs(parseFloat(last[2]))) - Math.log(Math.abs(parseFloat(last[1])))); + } +} + +// Finds the lowest positive value from the bucket ranges +// Returns 0 if no positive values are found or if there are no buckets. +export function findMinPositive(buckets: [number, string, string, string][]) { + if (!buckets || buckets.length === 0) { + return 0; // no buckets + } + for (let i = 0; i < buckets.length; i++) { + const right = parseFloat(buckets[i][2]); + const left = parseFloat(buckets[i][1]); + + if (left > 0) { + return left; + } + if (left < 0 && right > 0) { + return right; + } + if (i === buckets.length - 1) { + if (right > 0) { + return right; + } + } + } + return 0; // all buckets are negative +} + +// Finds the lowest negative value from the bucket ranges +// Returns 0 if no negative values are found or if there are no buckets. +export function findMaxNegative(buckets: [number, string, string, string][]) { + if (!buckets || buckets.length === 0) { + return 0; // no buckets + } + for (let i = 0; i < buckets.length; i++) { + const right = parseFloat(buckets[i][2]); + const left = parseFloat(buckets[i][1]); + const prevRight = i > 0 ? parseFloat(buckets[i - 1][2]) : 0; + + if (right >= 0) { + if (i === 0) { + if (left < 0) { + return left; // return the first negative bucket + } + return 0; // all buckets are positive + } + return prevRight; // return the last negative bucket + } + } + console.log('findmaxneg returning: ', buckets[buckets.length - 1][2]); + return parseFloat(buckets[buckets.length - 1][2]); // all buckets are negative +} + +// Calculates the left position of the zero axis as a percentage string. +export function findZeroAxisLeft( + scale: ScaleType, + rangeMin: number, + rangeMax: number, + minPositive: number, + maxNegative: number, + zeroBucketIdx: number, + widthNegative: number, + widthTotal: number, + expBucketWidth: number +): string { + if (scale === 'linear') { + return ((0 - rangeMin) / (rangeMax - rangeMin)) * 100 + '%'; + } else { + if (maxNegative === 0) { + return '0%'; + } + if (minPositive === 0) { + return '100%'; + } + if (zeroBucketIdx === -1) { + // if there is no zero bucket, we must zero axis between buckets around zero + return (widthNegative / widthTotal) * 100 + '%'; + } + if ((widthNegative + 0.5 * expBucketWidth) / widthTotal > 0) { + return ((widthNegative + 0.5 * expBucketWidth) / widthTotal) * 100 + '%'; + } else { + return '0%'; + } + } +} + +// Determines if the zero axis should be shown such that the zero label does not overlap with the range labels. +// The zero axis is shown if it is between 5% and 95% of the graph. +export function showZeroAxis(zeroAxisLeft: string) { + const axisNumber = parseFloat(zeroAxisLeft.slice(0, -1)); + if (5 < axisNumber && axisNumber < 95) { + return true; + } + return false; +} + +// Finds the index of the bucket whose range includes zero +export function findZeroBucket(buckets: [number, string, string, string][]): number { + for (let i = 0; i < buckets.length; i++) { + const left = parseFloat(buckets[i][1]); + const right = parseFloat(buckets[i][2]); + if (left <= 0 && right >= 0) { + return i; + } + } + return -1; +} From 9198952f7c6eddb6c2cd64eb5cb488599a5a9f23 Mon Sep 17 00:00:00 2001 From: Bartlomiej Plotka Date: Thu, 4 Jul 2024 23:29:20 +0200 Subject: [PATCH 18/29] [PRW 2.0] Merging `remote-write-2.0` feature branch to main (PRW 2.0 support + metadata in WAL) (#14395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remote Write 1.1: e2e benchmarks (#13102) * Remote Write e2e benchmarks Signed-off-by: Nicolás Pazos * Prometheus ports automatically assigned Signed-off-by: Nicolás Pazos * make dashboard editable + more modular to different job label values Signed-off-by: Callum Styan * Dashboard improvements * memory stats * diffs look at counter increases Signed-off-by: Nicolás Pazos * run script: absolute path for config templates Signed-off-by: Nicolás Pazos * grafana dashboard improvements * show actual values of metrics * add memory stats and diff Signed-off-by: Nicolás Pazos * dashboard changes Signed-off-by: Callum Styan --------- Signed-off-by: Nicolás Pazos Signed-off-by: Callum Styan Co-authored-by: Callum Styan * replace snappy encoding library Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * add new proto types Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * add decode function for new write request proto Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * add lookup table struct that is used to build the symbol table in new write request format Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * Implement code paths for new proto format Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * update example server to include handler for new format Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * Add new test client Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * tests and new -> original proto mapping util Signed-off-by: Nicolás Pazos * add new proto support on receiver end Signed-off-by: Nicolás Pazos * Fix test Signed-off-by: Nicolás Pazos * no-brainer copypaste but more performance write support Signed-off-by: Nicolás Pazos * remove some comented code Signed-off-by: Nicolás Pazos * fix mocks and fixture Signed-off-by: Nicolás Pazos * add basic reduce remote write handler benchmark Signed-off-by: Nicolás Pazos * refactor out common code between write methods Signed-off-by: Nicolás Pazos * fix: queue manager to include float histograms in new requests Signed-off-by: Nicolás Pazos * add sender-side tests and fix failing ones Signed-off-by: Nicolás Pazos * refactor queue manager code to remove some duplication Signed-off-by: Nicolás Pazos * fix build Signed-off-by: Nicolás Pazos * Improve sender benchmarks and some allocations Signed-off-by: Nicolás Pazos * Use github.com/golang/snappy Signed-off-by: Nicolás Pazos * cleanup: remove hardcoded fake url for testing Signed-off-by: Nicolás Pazos * Add 1.1 version handling code Signed-off-by: Nicolás Pazos * Remove config, update proto Signed-off-by: Nicolás Pazos * gofmt Signed-off-by: Nicolás Pazos * fix NewWriteClient and change new flags wording Signed-off-by: Nicolás Pazos * fields rewording in handler Signed-off-by: Nicolás Pazos * remote write handler to checks version header Signed-off-by: Nicolás Pazos * fix typo in log Signed-off-by: Nicolás Pazos * lint Signed-off-by: Nicolás Pazos * Add minmized remote write proto format Co-authored-by: Marco Pracucci Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * add functions for translating between new proto formats symbol table and actual prometheus labels Co-authored-by: Marco Pracucci Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * add functionality for new minimized remote write request format Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * fix minor things Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * Make LabelSymbols a fixed32 Signed-off-by: Nicolás Pazos * remove unused proto type Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * update tests Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * fix build for stringlabels tag Signed-off-by: Nicolás Pazos * Use two uint32 to encode (offset,leng) Signed-off-by: Nicolás Pazos * manually optimize varint marshaling Signed-off-by: Nicolás Pazos * Use unsafe []byte->string cast to reuse buffer Signed-off-by: Nicolás Pazos * fix writeRequestMinimizedFixture Signed-off-by: Nicolás Pazos * remove all code from previous interning approach the 'minimized' version is now the only v1.1 version Signed-off-by: Nicolás Pazos * minimally-tested exemplar support for rw 1.1 Signed-off-by: Nicolás Pazos * refactor new version flag to make it easier to pick a specific format instead of having multiple flags, plus add new formats for testing Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * use exp slices for backwards compat. to go 1.20 plus add copyright header to test file Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * fix label ranging Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * Add bytes slice (instead of slice of 32bit vars) format for testing Co-authored-by: Nicolás Pazos Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * test additional len and lenbytes formats Co-authored-by: Nicolás Pazos Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * remove mistaken package lock changes Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * remove formats we've decided not to use Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * remove more format types we probably won't use Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * More cleanup Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * use require instead of assert in custom marshal test Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * cleanup; remove some unused functions Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * more cleanup, mostly linting fixes Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * remove package-lock.json change again Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * more cleanup, address review comments Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * fix test panic Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * fix minor lint issue + use labels Range function since it looks like the tests fail to do `range labels.Labels` on CI Signed-off-by: Callum Styan Signed-off-by: Nicolás Pazos * new interning format based on []string indeces Co-authored-by: bwplotka Signed-off-by: Nicolás Pazos * remove all new rw formats but the []string one also adapt tests to the new format Signed-off-by: Nicolás Pazos * cleanup rwSymbolTable Signed-off-by: Nicolás Pazos * add some TODOs for later Signed-off-by: Nicolás Pazos * don't reserve field 3 for new proto and add TODO Signed-off-by: Nicolás Pazos * fix custom marshaling Signed-off-by: Nicolás Pazos * lint Signed-off-by: Nicolás Pazos * additional merge fixes Signed-off-by: Nicolás Pazos * lint fixes Signed-off-by: Nicolás Pazos * fix server example Signed-off-by: Nicolás Pazos * revert package-lock.json changes Signed-off-by: Nicolás Pazos * update example prometheus version Signed-off-by: Nicolás Pazos * define separate proto types for remote write 2.0 Signed-off-by: Nicolás Pazos * lint Signed-off-by: Nicolás Pazos * rename new proto types and move to separate pkg Signed-off-by: Nicolás Pazos * update prometheus version for example Signed-off-by: Nicolás Pazos * make proto Signed-off-by: Nicolás Pazos * make Metadata not nullable Signed-off-by: Nicolás Pazos * remove old MinSample proto message Signed-off-by: Nicolás Pazos * change enum names to fit buf build recommend enum naming and lint rules Signed-off-by: Callum Styan * remote: Added test for classic histogram grouping when sending rw; Fixed queue manager test delay. (#13421) Signed-off-by: bwplotka * Remote write v2: metadata support in every write request (#13394) * Approach bundling metadata along with samples and exemplars Signed-off-by: Paschalis Tsilias * Add first test; rebase with main Signed-off-by: Paschalis Tsilias * Alternative approach: bundle metadata in TimeSeries protobuf Signed-off-by: Paschalis Tsilias * update go mod to match main branch Signed-off-by: Callum Styan * fix after rebase Signed-off-by: Callum Styan * we're not going to modify the 1.X format anymore Signed-off-by: Callum Styan * Modify AppendMetadata based on the fact that we be putting metadata into timeseries Signed-off-by: Callum Styan * Rename enums for remote write versions to something that makes more sense + remove the added `sendMetadata` flag. Signed-off-by: Callum Styan * rename flag that enables writing of metadata records to the WAL Signed-off-by: Callum Styan * additional clean up Signed-off-by: Callum Styan * lint Signed-off-by: Callum Styan * fix usage of require.Len Signed-off-by: Callum Styan * some clean up from review comments Signed-off-by: Callum Styan * more review fixes Signed-off-by: Callum Styan --------- Signed-off-by: Paschalis Tsilias Signed-off-by: Callum Styan Co-authored-by: Paschalis Tsilias * remote write 2.0: sync with `main` branch (#13510) * consoles: exclude iowait and steal from CPU Utilisation 'iowait' and 'steal' indicate specific idle/wait states, which shouldn't be counted into CPU Utilisation. Also see https://github.com/prometheus-operator/kube-prometheus/pull/796 and https://github.com/kubernetes-monitoring/kubernetes-mixin/pull/667. Per the iostat man page: %idle Show the percentage of time that the CPU or CPUs were idle and the system did not have an outstanding disk I/O request. %iowait Show the percentage of time that the CPU or CPUs were idle during which the system had an outstanding disk I/O request. %steal Show the percentage of time spent in involuntary wait by the virtual CPU or CPUs while the hypervisor was servicing another virtual processor. Signed-off-by: Julian Wiedmann * tsdb: shrink txRing with smaller integers 4 billion active transactions ought to be enough for anyone. Signed-off-by: Bryan Boreham * tsdb: create isolation transaction slice on demand When Prometheus restarts it creates every series read in from the WAL, but many of those series will be finished, and never receive any more samples. By defering allocation of the txRing slice to when it is first needed, we save 32 bytes per stale series. Signed-off-by: Bryan Boreham * add cluster variable to Overview dashboard Signed-off-by: Erik Sommer * promql: simplify Native Histogram arithmetics Signed-off-by: Linas Medziunas * Cut 2.49.0-rc.0 (#13270) * Cut 2.49.0-rc.0 Signed-off-by: bwplotka * Removed the duplicate. Signed-off-by: bwplotka --------- Signed-off-by: bwplotka * Add unit protobuf parser Signed-off-by: Arianna Vespri * Go on adding protobuf parsing for unit Signed-off-by: Arianna Vespri * ui: create a reproduction for https://github.com/prometheus/prometheus/issues/13292 Signed-off-by: machine424 * Get conditional right Signed-off-by: Arianna Vespri * Get VM Scale Set NIC (#13283) Calling `*armnetwork.InterfacesClient.Get()` doesn't work for Scale Set VM NIC, because these use a different Resource ID format. Use `*armnetwork.InterfacesClient.GetVirtualMachineScaleSetNetworkInterface()` instead. This needs both the scale set name and the instance ID, so add an `InstanceID` field to the `virtualMachine` struct. `InstanceID` is empty for a VM that isn't a ScaleSetVM. Signed-off-by: Daniel Nicholls * Cut v2.49.0-rc.1 Signed-off-by: bwplotka * Delete debugging lines, amend error message for unit Signed-off-by: Arianna Vespri * Correct order in error message Signed-off-by: Arianna Vespri * Consider storage.ErrTooOldSample as non-retryable Signed-off-by: Daniel Kerbel * scrape_test.go: Increase scrape interval in TestScrapeLoopCache to reduce potential flakiness Signed-off-by: machine424 * Avoid creating string for suffix, consider counters without _total suffix Signed-off-by: Arianna Vespri * build(deps): bump github.com/prometheus/client_golang Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.17.0 to 1.18.0. - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.17.0...v1.18.0) --- updated-dependencies: - dependency-name: github.com/prometheus/client_golang dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * build(deps): bump actions/setup-node from 3.8.1 to 4.0.1 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3.8.1 to 4.0.1. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d...b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * scripts: sort file list in embed directive Otherwise the resulting string depends on find, which afaict depends on the underlying filesystem. A stable file list make it easier to detect UI changes in downstreams that need to track UI assets. Signed-off-by: Jan Fajerski * Fix DataTableProps['data'] for resultType string Signed-off-by: Kevin Mingtarja * Fix handling of scalar and string in isHeatmapData Signed-off-by: Kevin Mingtarja * build(deps): bump github.com/influxdata/influxdb Bumps [github.com/influxdata/influxdb](https://github.com/influxdata/influxdb) from 1.11.2 to 1.11.4. - [Release notes](https://github.com/influxdata/influxdb/releases) - [Commits](https://github.com/influxdata/influxdb/compare/v1.11.2...v1.11.4) --- updated-dependencies: - dependency-name: github.com/influxdata/influxdb dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * build(deps): bump github.com/prometheus/prometheus Bumps [github.com/prometheus/prometheus](https://github.com/prometheus/prometheus) from 0.48.0 to 0.48.1. - [Release notes](https://github.com/prometheus/prometheus/releases) - [Changelog](https://github.com/prometheus/prometheus/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/prometheus/compare/v0.48.0...v0.48.1) --- updated-dependencies: - dependency-name: github.com/prometheus/prometheus dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump client_golang to v1.18.0 (#13373) Signed-off-by: Paschalis Tsilias * Drop old inmemory samples (#13002) * Drop old inmemory samples Co-authored-by: Paschalis Tsilias Signed-off-by: Paschalis Tsilias Signed-off-by: Marc Tuduri * Avoid copying timeseries when the feature is disabled Signed-off-by: Paschalis Tsilias Signed-off-by: Marc Tuduri * Run gofmt Signed-off-by: Paschalis Tsilias Signed-off-by: Marc Tuduri * Clarify docs Signed-off-by: Marc Tuduri * Add more logging info Signed-off-by: Marc Tuduri * Remove loggers Signed-off-by: Marc Tuduri * optimize function and add tests Signed-off-by: Marc Tuduri * Simplify filter Signed-off-by: Marc Tuduri * rename var Signed-off-by: Marc Tuduri * Update help info from metrics Signed-off-by: Marc Tuduri * use metrics to keep track of drop elements during buildWriteRequest Signed-off-by: Marc Tuduri * rename var in tests Signed-off-by: Marc Tuduri * pass time.Now as parameter Signed-off-by: Marc Tuduri * Change buildwriterequest during retries Signed-off-by: Marc Tuduri * Revert "Remove loggers" This reverts commit 54f91dfcae20488944162335ab4ad8be459df1ab. Signed-off-by: Marc Tuduri * use log level debug for loggers Signed-off-by: Marc Tuduri * Fix linter Signed-off-by: Paschalis Tsilias * Remove noisy debug-level logs; add 'reason' label to drop metrics Signed-off-by: Paschalis Tsilias * Remove accidentally committed files Signed-off-by: Paschalis Tsilias * Propagate logger to buildWriteRequest to log dropped data Signed-off-by: Paschalis Tsilias * Fix docs comment Signed-off-by: Paschalis Tsilias * Make drop reason more specific Signed-off-by: Paschalis Tsilias * Remove unnecessary pass of logger Signed-off-by: Paschalis Tsilias * Use snake_case for reason label Signed-off-by: Paschalis Tsilias * Fix dropped samples metric Signed-off-by: Paschalis Tsilias --------- Signed-off-by: Paschalis Tsilias Signed-off-by: Marc Tuduri Signed-off-by: Paschalis Tsilias Co-authored-by: Paschalis Tsilias Co-authored-by: Paschalis Tsilias * fix(discovery): allow requireUpdate util to timeout in discovery/file/file_test.go. The loop ran indefinitely if the condition isn't met. Before, each iteration created a new timer channel which was always outpaced by the other timer channel with smaller duration. minor detail: There was a memory leak: resources of the ~10 previous timers were constantly kept. With the fix, we may keep the resources of one timer around for defaultWait but this isn't worth the changes to make it right. Signed-off-by: machine424 * Merge pull request #13371 from kevinmingtarja/fix-isHeatmapData ui: fix handling of scalar and string in isHeatmapData * tsdb/{index,compact}: allow using custom postings encoding format (#13242) * tsdb/{index,compact}: allow using custom postings encoding format We would like to experiment with a different postings encoding format in Thanos so in this change I am proposing adding another argument to `NewWriter` which would allow users to change the format if needed. Also, wire the leveled compactor so that it would be possible to change the format there too. Signed-off-by: Giedrius Statkevičius * tsdb/compact: use a struct for leveled compactor options As discussed on Slack, let's use a struct for the options in leveled compactor. Signed-off-by: Giedrius Statkevičius * tsdb: make changes after Bryan's review - Make changes less intrusive - Turn the postings encoder type into a function - Add NewWriterWithEncoder() Signed-off-by: Giedrius Statkevičius --------- Signed-off-by: Giedrius Statkevičius * Cut 2.49.0-rc.2 Signed-off-by: bwplotka * build(deps): bump actions/setup-go from 3.5.0 to 5.0.0 in /scripts (#13362) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3.5.0 to 5.0.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/6edd4406fa81c3da01a34fa6f6343087c207a568...0c52d547c9bc32b1aa3301fd7a9cb496313a4491) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github/codeql-action from 2.22.8 to 3.22.12 (#13358) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.22.8 to 3.22.12. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/407ffafae6a767df3e0230c3df91b6443ae8df75...012739e5082ff0c22ca6d6ab32e07c36df03c4a4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * put @nexucis has a release shepherd (#13383) Signed-off-by: Augustin Husson * Add analyze histograms command to promtool (#12331) Add `query analyze` command to promtool This command analyzes the buckets of classic and native histograms, based on data queried from the Prometheus query API, i.e. it doesn't require direct access to the TSDB files. Signed-off-by: Jeanette Tan --------- Signed-off-by: Jeanette Tan * included instance in all necessary descriptions Signed-off-by: Erik Sommer * tsdb/compact: fix passing merge func Fixing a very small logical problem I've introduced :(. Signed-off-by: Giedrius Statkevičius * tsdb: add enable overlapping compaction This functionality is needed in downstream projects because they have a separate component that does compaction. Upstreaming https://github.com/grafana/mimir-prometheus/blob/7c8e9a2a76fc729e9078889782928b2fdfe240e9/tsdb/compact.go#L323-L325. Signed-off-by: Giedrius Statkevičius * Cut 2.49.0 Signed-off-by: bwplotka * promtool: allow setting multiple matchers to "promtool tsdb dump" command. (#13296) Conditions are ANDed inside the same matcher but matchers are ORed Including unit tests for "promtool tsdb dump". Refactor some matchers scraping utils. Signed-off-by: machine424 * Fixed changelog Signed-off-by: bwplotka * tsdb/main: wire "EnableOverlappingCompaction" to tsdb.Options (#13398) This added the https://github.com/prometheus/prometheus/pull/13393 "EnableOverlappingCompaction" parameter to the compactor code but not to the tsdb.Options. I forgot about that. Add it to `tsdb.Options` too and set it to `true` in Prometheus. Copy/paste the description from https://github.com/prometheus/prometheus/pull/13393#issuecomment-1891787986 Signed-off-by: Giedrius Statkevičius * Issue #13268: fix quality value in accept header Signed-off-by: Kumar Kalpadiptya Roy * Cut 2.49.1 with scrape q= bugfix. Signed-off-by: bwplotka * Cut 2.49.1 web package. Signed-off-by: bwplotka * Restore more efficient version of NewPossibleNonCounterInfo annotation (#13022) Restore more efficient version of NewPossibleNonCounterInfo annotation Signed-off-by: Jeanette Tan --------- Signed-off-by: Jeanette Tan * Fix regressions introduced by #13242 Signed-off-by: Marco Pracucci * fix slice copy in 1.20 (#13389) The slices package is added to the standard library in Go 1.21; we need to import from the exp area to maintain compatibility with Go 1.20. Signed-off-by: tyltr * Docs: Query Basics: link to rate (#10538) Co-authored-by: Julien Pivotto * chore(kubernetes): check preconditions earlier and avoid unnecessary checks or iterations Signed-off-by: machine424 * Examples: link to `rate` for new users (#10535) * Examples: link to `rate` for new users Signed-off-by: Ted Robertson 10043369+tredondo@users.noreply.github.com Co-authored-by: Bryan Boreham * promql: use natural sort in sort_by_label and sort_by_label_desc (#13411) These functions are intended for humans, as robots can already sort the results however they please. Humans like things sorted "naturally": * https://blog.codinghorror.com/sorting-for-humans-natural-sort-order/ A similar thing has been done to Grafana, which is also used by humans: * https://github.com/grafana/grafana/pull/78024 * https://github.com/grafana/grafana/pull/78494 Signed-off-by: Ivan Babrou * TestLabelValuesWithMatchers: Add test case Signed-off-by: Arve Knudsen * remove obsolete build tag Signed-off-by: tyltr * Upgrade some golang dependencies for resty 2.11 Signed-off-by: Israel Blancas * Native Histograms: support `native_histogram_min_bucket_factor` in scrape_config (#13222) Native Histograms: support native_histogram_min_bucket_factor in scrape_config --------- Signed-off-by: Ziqi Zhao Signed-off-by: Björn Rabenstein Co-authored-by: George Krajcsovits Co-authored-by: Björn Rabenstein * Add warnings for histogramRate applied with isCounter not matching counter/gauge histogram (#13392) Add warnings for histogramRate applied with isCounter not matching counter/gauge histogram --------- Signed-off-by: Jeanette Tan * Minor fixes to otlp vendor update script Signed-off-by: Goutham * build(deps): bump github.com/hetznercloud/hcloud-go/v2 Bumps [github.com/hetznercloud/hcloud-go/v2](https://github.com/hetznercloud/hcloud-go) from 2.4.0 to 2.6.0. - [Release notes](https://github.com/hetznercloud/hcloud-go/releases) - [Changelog](https://github.com/hetznercloud/hcloud-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/hetznercloud/hcloud-go/compare/v2.4.0...v2.6.0) --- updated-dependencies: - dependency-name: github.com/hetznercloud/hcloud-go/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Enhanced visibility for `promtool test rules` with JSON colored formatting (#13342) * Added diff flag for unit test to improvise readability & debugging Signed-off-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> * Removed blank spaces Signed-off-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> * Fixed linting error Signed-off-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> * Added cli flags to documentation Signed-off-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> * Revert unrrelated linting fixes Signed-off-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> * Fixed review suggestions Signed-off-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> * Cleanup Signed-off-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> * Updated flag description Signed-off-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> * Updated flag description Signed-off-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> --------- Signed-off-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> * storage: skip merging when no remote storage configured Prometheus is hard-coded to use a fanout storage between TSDB and a remote storage which by default is empty. This change detects the empty storage and skips merging between result sets, which would make `Select()` sort results. Bottom line: we skip a sort unless there really is some remote storage configured. Signed-off-by: Bryan Boreham * Remove csmarchbanks from remote write owners (#13432) I have not had the time to keep up with remote write and have no plans to work on it in the near future so I am withdrawing my maintainership of that part of the codebase. I continue to focus on client_python. Signed-off-by: Chris Marchbanks * add more context cancellation check at evaluation time Signed-off-by: Ben Ye * Optimize label values with matchers by taking shortcuts (#13426) Don't calculate postings beforehand: we may not need them. If all matchers are for the requested label, we can just filter its values. Also, if there are no values at all, no need to run any kind of logic. Also add more labelValuesWithMatchers benchmarks Signed-off-by: Oleg Zaytsev * Add automatic memory limit handling Enable automatic detection of memory limits and configure GOMEMLIMIT to match. * Also includes a flag to allow controlling the reserved ratio. Signed-off-by: SuperQ * Update OSSF badge link (#13433) Provide a more user friendly interface Signed-off-by: Matthieu MOREL * SD Managers taking over responsibility for registration of debug metrics (#13375) SD Managers take over responsibility for SD metrics registration --------- Signed-off-by: Paulin Todev Signed-off-by: Björn Rabenstein Co-authored-by: Björn Rabenstein * Optimize histogram iterators (#13340) Optimize histogram iterators Histogram iterators allocate new objects in the AtHistogram and AtFloatHistogram methods, which makes calculating rates over long ranges expensive. In #13215 we allowed an existing object to be reused when converting an integer histogram to a float histogram. This commit follows the same idea and allows injecting an existing object in the AtHistogram and AtFloatHistogram methods. When the injected value is nil, iterators allocate new histograms, otherwise they populate and return the injected object. The commit also adds a CopyTo method to Histogram and FloatHistogram which is used in the BufferedIterator to overwrite items in the ring instead of making new copies. Note that a specialized HPoint pool is needed for all of this to work (`matrixSelectorHPool`). --------- Signed-off-by: Filip Petkovski Co-authored-by: George Krajcsovits * doc: Mark `mad_over_time` as experimental (#13440) We forgot to do that in https://github.com/prometheus/prometheus/pull/13059 Signed-off-by: beorn7 * Change metric label for Puppetdb from 'http' to 'puppetdb' Signed-off-by: Paulin Todev * mirror metrics.proto change & generate code Signed-off-by: Ziqi Zhao * TestHeadLabelValuesWithMatchers: Add test case (#13414) Add test case to TestHeadLabelValuesWithMatchers, while fixing a couple of typos in other test cases. Also enclosing some implicit sub-tests in a `t.Run` call to make them explicitly sub-tests. Signed-off-by: Arve Knudsen * update all go dependencies (#13438) Signed-off-by: Augustin Husson * build(deps): bump the k8s-io group with 2 updates (#13454) Bumps the k8s-io group with 2 updates: [k8s.io/api](https://github.com/kubernetes/api) and [k8s.io/client-go](https://github.com/kubernetes/client-go). Updates `k8s.io/api` from 0.28.4 to 0.29.1 - [Commits](https://github.com/kubernetes/api/compare/v0.28.4...v0.29.1) Updates `k8s.io/client-go` from 0.28.4 to 0.29.1 - [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/kubernetes/client-go/compare/v0.28.4...v0.29.1) --- updated-dependencies: - dependency-name: k8s.io/api dependency-type: direct:production update-type: version-update:semver-minor dependency-group: k8s-io - dependency-name: k8s.io/client-go dependency-type: direct:production update-type: version-update:semver-minor dependency-group: k8s-io ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump the go-opentelemetry-io group with 1 update (#13453) Bumps the go-opentelemetry-io group with 1 update: [go.opentelemetry.io/collector/semconv](https://github.com/open-telemetry/opentelemetry-collector). Updates `go.opentelemetry.io/collector/semconv` from 0.92.0 to 0.93.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-collector/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-collector/blob/main/CHANGELOG-API.md) - [Commits](https://github.com/open-telemetry/opentelemetry-collector/compare/v0.92.0...v0.93.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/collector/semconv dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-opentelemetry-io ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump actions/upload-artifact from 3.1.3 to 4.0.0 (#13355) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3.1.3 to 4.0.0. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/a8a3f3ad30e3422c9c7b888a15615d19a852ae32...c7d193f32edcb7bfad88892161225aeda64e9392) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump bufbuild/buf-push-action (#13357) Bumps [bufbuild/buf-push-action](https://github.com/bufbuild/buf-push-action) from 342fc4cdcf29115a01cf12a2c6dd6aac68dc51e1 to a654ff18effe4641ebea4a4ce242c49800728459. - [Release notes](https://github.com/bufbuild/buf-push-action/releases) - [Commits](https://github.com/bufbuild/buf-push-action/compare/342fc4cdcf29115a01cf12a2c6dd6aac68dc51e1...a654ff18effe4641ebea4a4ce242c49800728459) --- updated-dependencies: - dependency-name: bufbuild/buf-push-action dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Labels: Add DropMetricName function, used in PromQL (#13446) This function is called very frequently when executing PromQL functions, and we can do it much more efficiently inside Labels. In the common case that `__name__` comes first in the labels, we simply re-point to start at the next label, which is nearly free. `DropMetricName` is now so cheap I removed the cache - benchmarks show everything still goes faster. Signed-off-by: Bryan Boreham * tsdb: simplify internal series delete function (#13261) Lifting an optimisation from Agent code, `seriesHashmap.del` can use the unique series reference, doesn't need to check Labels. Also streamline the logic for deleting from `unique` and `conflicts` maps, and add some comments to help the next person. Signed-off-by: Bryan Boreham * otlptranslator/update-copy.sh: Fix sed command lines Signed-off-by: Arve Knudsen * Rollback k8s.io requirements (#13462) Rollback k8s.io Go modules to v0.28.6 to avoid forcing upgrade of Go to 1.21. This allows us to keep compatibility with the currently supported upstream Go releases. Signed-off-by: SuperQ * Make update-copy.sh work for both OSX and GNU sed Signed-off-by: Arve Knudsen * Name @beorn7 and @krajorama as maintainers for native histograms I have been the de-facto maintainer for native histograms from the beginning. So let's put this into MAINTAINERS.md. In addition, I hereby proposose George Krajcsovits AKA Krajo as a co-maintainer. He has contributed a lot of native histogram code, but more importantly, he has contributed substantially to reviewing other contributors' native histogram code, up to a point where I was merely rubberstamping the PRs he had already reviewed. I'm confident that he is ready to to be granted commit rights as outlined in the "Maintainers" section of the governance: https://prometheus.io/governance/#maintainers According to the same section of the governance, I will announce the proposed change on the developers mailing list and will give some time for lazy consensus before merging this PR. Signed-off-by: beorn7 * ui/fix: correct url handling for stacked graphs (#13460) Signed-off-by: Yury Moladau * tsdb: use cheaper Mutex on series Mutex is 8 bytes; RWMutex is 24 bytes and much more complicated. Since `RLock` is only used in two places, `UpdateMetadata` and `Delete`, neither of which are hotspots, we should use the cheaper one. Signed-off-by: Bryan Boreham * Fix last_over_time for native histograms The last_over_time retains a histogram sample without making a copy. This sample is now coming from the buffered iterator used for windowing functions, and can be reused for reading subsequent samples as the iterator progresses. I would propose copying the sample in the last_over_time function, similar to how it is done for rate, sum_over_time and others. Signed-off-by: Filip Petkovski * Implementation NOTE: Rebased from main after refactor in #13014 Signed-off-by: Danny Kopping * Add feature flag Signed-off-by: Danny Kopping * Refactor concurrency control Signed-off-by: Danny Kopping * Optimising dependencies/dependents funcs to not produce new slices each request Signed-off-by: Danny Kopping * Refactoring Signed-off-by: Danny Kopping * Rename flag Signed-off-by: Danny Kopping * Refactoring for performance, and to allow controller to be overridden Signed-off-by: Danny Kopping * Block until all rules, both sync & async, have completed evaluating Updated & added tests Review feedback nits Return empty map if not indeterminate Use highWatermark to track inflight requests counter Appease the linter Clarify feature flag Signed-off-by: Danny Kopping * Fix typo in CLI flag description Signed-off-by: Marco Pracucci * Fixed auto-generated doc Signed-off-by: Marco Pracucci * Improve doc Signed-off-by: Marco Pracucci * Simplify the design to update concurrency controller once the rule evaluation has done Signed-off-by: Marco Pracucci * Add more test cases to TestDependenciesEdgeCases Signed-off-by: Marco Pracucci * Added more test cases to TestDependenciesEdgeCases Signed-off-by: Marco Pracucci * Improved RuleConcurrencyController interface doc Signed-off-by: Marco Pracucci * Introduced sequentialRuleEvalController Signed-off-by: Marco Pracucci * Remove superfluous nil check in Group.metrics Signed-off-by: Marco Pracucci * api: Serialize discovered and target labels into JSON directly (#13469) Converted maps into labels.Labels to avoid a lot of copying of data which leads to very high memory consumption while opening the /service-discovery endpoint in the Prometheus UI Signed-off-by: Leegin <114397475+Leegin-darknight@users.noreply.github.com> * api: Serialize discovered labels into JSON directly in dropped targets (#13484) Converted maps into labels.Labels to avoid a lot of copying of data which leads to very high memory consumption while opening the /service-discovery endpoint in the Prometheus UI Signed-off-by: Leegin <114397475+Leegin-darknight@users.noreply.github.com> * Add ShardedPostings() support to TSDB (#10421) This PR is a reference implementation of the proposal described in #10420. In addition to what described in #10420, in this PR I've introduced labels.StableHash(). The idea is to offer an hashing function which doesn't change over time, and that's used by query sharding in order to get a stable behaviour over time. The implementation of labels.StableHash() is the hashing function used by Prometheus before stringlabels, and what's used by Grafana Mimir for query sharding (because built before stringlabels was a thing). Follow up work As mentioned in #10420, if this PR is accepted I'm also open to upload another foundamental piece used by Grafana Mimir query sharding to accelerate the query execution: an optional, configurable and fast in-memory cache for the series hashes. Signed-off-by: Marco Pracucci * storage/remote: document why two benchmarks are skipped One was silently doing nothing; one was doing something but the work didn't go up linearly with iteration count. Signed-off-by: Bryan Boreham * Pod status changes not discovered by Kube Endpoints SD (#13337) * fix(discovery/kubernetes/endpoints): react to changes on Pods because some modifications can occur on them without triggering an update on the related Endpoints (The Pod phase changing from Pending to Running e.g.). --------- Signed-off-by: machine424 Co-authored-by: Guillermo Sanchez Gavier * Small improvements, add const, remove copypasta (#8106) Signed-off-by: Mikhail Fesenko Signed-off-by: Jesus Vazquez * Proposal to improve FPointSlice and HPointSlice allocation. (#13448) * Reusing points slice from previous series when the slice is under utilized * Adding comments on the bench test Signed-off-by: Alan Protasio * lint Signed-off-by: Nicolás Pazos * go mod tidy Signed-off-by: Nicolás Pazos --------- Signed-off-by: Julian Wiedmann Signed-off-by: Bryan Boreham Signed-off-by: Erik Sommer Signed-off-by: Linas Medziunas Signed-off-by: bwplotka Signed-off-by: Arianna Vespri Signed-off-by: machine424 Signed-off-by: Daniel Nicholls Signed-off-by: Daniel Kerbel Signed-off-by: dependabot[bot] Signed-off-by: Jan Fajerski Signed-off-by: Kevin Mingtarja Signed-off-by: Paschalis Tsilias Signed-off-by: Marc Tuduri Signed-off-by: Paschalis Tsilias Signed-off-by: Giedrius Statkevičius Signed-off-by: Augustin Husson Signed-off-by: Jeanette Tan Signed-off-by: Bartlomiej Plotka Signed-off-by: Kumar Kalpadiptya Roy Signed-off-by: Marco Pracucci Signed-off-by: tyltr Signed-off-by: Ted Robertson 10043369+tredondo@users.noreply.github.com Signed-off-by: Ivan Babrou Signed-off-by: Arve Knudsen Signed-off-by: Israel Blancas Signed-off-by: Ziqi Zhao Signed-off-by: Björn Rabenstein Signed-off-by: Goutham Signed-off-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> Signed-off-by: Chris Marchbanks Signed-off-by: Ben Ye Signed-off-by: Oleg Zaytsev Signed-off-by: SuperQ Signed-off-by: Ben Kochie Signed-off-by: Matthieu MOREL Signed-off-by: Paulin Todev Signed-off-by: Filip Petkovski Signed-off-by: beorn7 Signed-off-by: Augustin Husson Signed-off-by: Yury Moladau Signed-off-by: Danny Kopping Signed-off-by: Leegin <114397475+Leegin-darknight@users.noreply.github.com> Signed-off-by: Mikhail Fesenko Signed-off-by: Jesus Vazquez Signed-off-by: Alan Protasio Signed-off-by: Nicolás Pazos Co-authored-by: Julian Wiedmann Co-authored-by: Bryan Boreham Co-authored-by: Erik Sommer Co-authored-by: Linas Medziunas Co-authored-by: Bartlomiej Plotka Co-authored-by: Arianna Vespri Co-authored-by: machine424 Co-authored-by: daniel-resdiary <109083091+daniel-resdiary@users.noreply.github.com> Co-authored-by: Daniel Kerbel Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jan Fajerski Co-authored-by: Kevin Mingtarja Co-authored-by: Paschalis Tsilias Co-authored-by: Marc Tudurí Co-authored-by: Paschalis Tsilias Co-authored-by: Giedrius Statkevičius Co-authored-by: Augustin Husson Co-authored-by: Björn Rabenstein Co-authored-by: zenador Co-authored-by: gotjosh Co-authored-by: Ben Kochie Co-authored-by: Kumar Kalpadiptya Roy Co-authored-by: Marco Pracucci Co-authored-by: tyltr Co-authored-by: Ted Robertson <10043369+tredondo@users.noreply.github.com> Co-authored-by: Julien Pivotto Co-authored-by: Matthias Loibl Co-authored-by: Ivan Babrou Co-authored-by: Arve Knudsen Co-authored-by: Israel Blancas Co-authored-by: Ziqi Zhao Co-authored-by: George Krajcsovits Co-authored-by: Björn Rabenstein Co-authored-by: Goutham Co-authored-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> Co-authored-by: Chris Marchbanks Co-authored-by: Ben Ye Co-authored-by: Oleg Zaytsev Co-authored-by: Matthieu MOREL Co-authored-by: Paulin Todev Co-authored-by: Filip Petkovski Co-authored-by: Yury Molodov Co-authored-by: Danny Kopping Co-authored-by: Leegin <114397475+Leegin-darknight@users.noreply.github.com> Co-authored-by: Guillermo Sanchez Gavier Co-authored-by: Mikhail Fesenko Co-authored-by: Alan Protasio * remote write 2.0 - follow up improvements (#13478) * move remote write proto version config from a remote storage config to a per remote write configuration option Signed-off-by: Callum Styan * rename scrape config for metadata, fix 2.0 header var name/value (was 1.1), and more clean up Signed-off-by: Callum Styan * address review comments, mostly lint fixes Signed-off-by: Callum Styan * another lint fix Signed-off-by: Callum Styan * lint imports Signed-off-by: Callum Styan --------- Signed-off-by: Callum Styan * go mod tidy Signed-off-by: Nicolás Pazos * Added commmentary to RW 2.0 protocol for easier adoption and explicit semantics. (#13502) * Added commmentary to RW 2.0 protocol for easier adoption and explicit semantics. Signed-off-by: bwplotka * Apply suggestions from code review Co-authored-by: Nico Pazos <32206519+npazosmendez@users.noreply.github.com> Signed-off-by: Callum Styan --------- Signed-off-by: bwplotka Signed-off-by: Callum Styan Co-authored-by: Callum Styan Co-authored-by: Nico Pazos <32206519+npazosmendez@users.noreply.github.com> * prw2.0: Added support for "custom" layouts for native histogram proto (#13558) * prw2.0: Added support for "custom" layouts for native histogram. Result of the discussions: * https://github.com/prometheus/prometheus/issues/13475#issuecomment-1931496924 * https://cloud-native.slack.com/archives/C02KR205UMU/p1707301006347199 Signed-off-by: bwplotka * prw2.0: Added support for "custom" layouts for native histogram. Result of the discussions: * https://github.com/prometheus/prometheus/issues/13475#issuecomment-1931496924 * https://cloud-native.slack.com/archives/C02KR205UMU/p1707301006347199 Signed-off-by: bwplotka # Conflicts: # prompb/write/v2/types.pb.go * Update prompb/write/v2/types.proto Co-authored-by: George Krajcsovits Signed-off-by: Bartlomiej Plotka * Addressed comments, fixed test. Signed-off-by: bwplotka --------- Signed-off-by: bwplotka Signed-off-by: Bartlomiej Plotka Co-authored-by: George Krajcsovits * first draft of content negotiation Signed-off-by: Alex Greenbank * Lint Signed-off-by: Alex Greenbank * Fix race in test Signed-off-by: Alex Greenbank * Fix another test race Signed-off-by: Alex Greenbank * Almost done with lint Signed-off-by: Alex Greenbank * Fix todos around 405 HEAD handling Signed-off-by: Alex Greenbank * Changes based on review comments Signed-off-by: Alex Greenbank * Update storage/remote/client.go Co-authored-by: Bartlomiej Plotka Signed-off-by: Alex Greenbank * Latest updates to review comments Signed-off-by: Alex Greenbank * latest tweaks Signed-off-by: Alex Greenbank * remote write 2.0 - content negotiation remediation (#13921) * Consolidate renegotiation error into one, fix tests Signed-off-by: Alex Greenbank * fix metric name and actuall increment counter Signed-off-by: Alex Greenbank --------- Signed-off-by: Alex Greenbank * Fixes after main sync. Signed-off-by: bwplotka * [PRW 2.0] Moved rw2 proto to the full path (both package name and placement) (#13973) undefined * [PRW2.0] Remove benchmark scripts (#13949) See rationales on https://docs.google.com/document/d/1Bpf7mYjrHUhPHkie0qlnZFxzgqf_L32kM8ZOknSdJrU/edit Signed-off-by: bwplotka * rw20: Update prw commentary after Callum spec review (#14136) * rw20: Update prw commentary after Callum spec review Signed-off-by: Bartlomiej Plotka * Update types.proto Signed-off-by: Bartlomiej Plotka --------- Signed-off-by: Bartlomiej Plotka * [PRW 2.0] Updated spec proto (2.0-rc.1); deterministic v1 interop; to be sympathetic with implementation. (#14330) * [PRW 2.0] Updated spec proto (2.0-rc.1); deterministic v1 interop; to be sympathetic with implementation. Signed-off-by: bwplotka * update custom marshalling Signed-off-by: bwplotka * Removed confusing comments. Signed-off-by: bwplotka --------- Signed-off-by: bwplotka * [PRW-2.0] (chain1) New Remote Write 2.0 Config options for 2.0-rc.1 spec. (#14335) NOTE: For simple review this change does not touch remote/ packages, only main and configs. Spec: https://prometheus.io/docs/specs/remote_write_spec_2_0 Supersedes https://github.com/prometheus/prometheus/pull/13968 Signed-off-by: bwplotka * [PRW-2.0] (part 2) Removed automatic negotiation, updates for the latest spec semantics in remote pkg (#14329) * [PRW-2.0] (part2) Moved to latest basic negotiation & spec semantics. Spec: https://github.com/prometheus/docs/pull/2462 Supersedes https://github.com/prometheus/prometheus/pull/13968 Signed-off-by: bwplotka # Conflicts: # config/config.go # docs/configuration/configuration.md # storage/remote/queue_manager_test.go # storage/remote/write.go # web/api/v1/api.go * Addressed comments. Signed-off-by: bwplotka --------- Signed-off-by: bwplotka * lint Signed-off-by: Nicolás Pazos * storage/remote tests: refactor: extract function newTestQueueManager To reduce repetition. Signed-off-by: Bryan Boreham Signed-off-by: Nicolás Pazos * use newTestQueueManager for test Signed-off-by: Nicolás Pazos * go mod tidy Signed-off-by: Nicolás Pazos * [PRW 2.0] (part3) moved type specific conversions to prompb and writev2 codecs. Signed-off-by: bwplotka * Added test for rwProtoMsgFlagParser; fixed TODO comment. Signed-off-by: bwplotka * Renamed DecodeV2WriteRequestStr to DecodeWriteV2Request (with tests). Signed-off-by: bwplotka * Addressed comments on remote_storage example, updated it for 2.0 Signed-off-by: bwplotka * Fixed `--enable-feature=metadata-wal-records` docs and error when using PRW 2.0 without it. Signed-off-by: bwplotka * Addressed Callum comments on custom*.go Signed-off-by: bwplotka * Added TODO to genproto. Signed-off-by: bwplotka * Addressed Callum comments in remote pkg. Signed-off-by: bwplotka * Added metadata validation to write handler test; fixed ToMetadata. Signed-off-by: bwplotka * Addressed rest of Callum comments. Signed-off-by: bwplotka * Fixed writev2.FromMetadataType (was wrongly using prompb). Signed-off-by: bwplotka * fix a few import whitespaces Signed-off-by: Callum Styan * add a default case with an error to the example RW receiver Signed-off-by: Callum Styan * more minor import whitespace chagnes Signed-off-by: Callum Styan * Apply suggestions from code review Signed-off-by: Bartlomiej Plotka * Update storage/remote/queue_manager_test.go Signed-off-by: Bartlomiej Plotka --------- Signed-off-by: Nicolás Pazos Signed-off-by: Callum Styan Signed-off-by: bwplotka Signed-off-by: Paschalis Tsilias Signed-off-by: Julian Wiedmann Signed-off-by: Bryan Boreham Signed-off-by: Erik Sommer Signed-off-by: Linas Medziunas Signed-off-by: Arianna Vespri Signed-off-by: machine424 Signed-off-by: Daniel Nicholls Signed-off-by: Daniel Kerbel Signed-off-by: dependabot[bot] Signed-off-by: Jan Fajerski Signed-off-by: Kevin Mingtarja Signed-off-by: Paschalis Tsilias Signed-off-by: Marc Tuduri Signed-off-by: Paschalis Tsilias Signed-off-by: Giedrius Statkevičius Signed-off-by: Augustin Husson Signed-off-by: Jeanette Tan Signed-off-by: Bartlomiej Plotka Signed-off-by: Kumar Kalpadiptya Roy Signed-off-by: Marco Pracucci Signed-off-by: tyltr Signed-off-by: Ted Robertson 10043369+tredondo@users.noreply.github.com Signed-off-by: Ivan Babrou Signed-off-by: Arve Knudsen Signed-off-by: Israel Blancas Signed-off-by: Ziqi Zhao Signed-off-by: Björn Rabenstein Signed-off-by: Goutham Signed-off-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> Signed-off-by: Chris Marchbanks Signed-off-by: Ben Ye Signed-off-by: Oleg Zaytsev Signed-off-by: SuperQ Signed-off-by: Ben Kochie Signed-off-by: Matthieu MOREL Signed-off-by: Paulin Todev Signed-off-by: Filip Petkovski Signed-off-by: beorn7 Signed-off-by: Augustin Husson Signed-off-by: Yury Moladau Signed-off-by: Danny Kopping Signed-off-by: Leegin <114397475+Leegin-darknight@users.noreply.github.com> Signed-off-by: Mikhail Fesenko Signed-off-by: Jesus Vazquez Signed-off-by: Alan Protasio Signed-off-by: Alex Greenbank Co-authored-by: Nicolás Pazos <32206519+npazosmendez@users.noreply.github.com> Co-authored-by: Callum Styan Co-authored-by: Nicolás Pazos Co-authored-by: alexgreenbank Co-authored-by: Marco Pracucci Co-authored-by: Paschalis Tsilias Co-authored-by: Julian Wiedmann Co-authored-by: Bryan Boreham Co-authored-by: Erik Sommer Co-authored-by: Linas Medziunas Co-authored-by: Arianna Vespri Co-authored-by: machine424 Co-authored-by: daniel-resdiary <109083091+daniel-resdiary@users.noreply.github.com> Co-authored-by: Daniel Kerbel Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jan Fajerski Co-authored-by: Kevin Mingtarja Co-authored-by: Paschalis Tsilias Co-authored-by: Marc Tudurí Co-authored-by: Paschalis Tsilias Co-authored-by: Giedrius Statkevičius Co-authored-by: Augustin Husson Co-authored-by: Björn Rabenstein Co-authored-by: zenador Co-authored-by: gotjosh Co-authored-by: Ben Kochie Co-authored-by: Kumar Kalpadiptya Roy Co-authored-by: tyltr Co-authored-by: Ted Robertson <10043369+tredondo@users.noreply.github.com> Co-authored-by: Julien Pivotto Co-authored-by: Matthias Loibl Co-authored-by: Ivan Babrou Co-authored-by: Arve Knudsen Co-authored-by: Israel Blancas Co-authored-by: Ziqi Zhao Co-authored-by: George Krajcsovits Co-authored-by: Björn Rabenstein Co-authored-by: Goutham Co-authored-by: Rewanth Tammana <22347290+rewanthtammana@users.noreply.github.com> Co-authored-by: Chris Marchbanks Co-authored-by: Ben Ye Co-authored-by: Oleg Zaytsev Co-authored-by: Matthieu MOREL Co-authored-by: Paulin Todev Co-authored-by: Filip Petkovski Co-authored-by: Yury Molodov Co-authored-by: Danny Kopping Co-authored-by: Leegin <114397475+Leegin-darknight@users.noreply.github.com> Co-authored-by: Guillermo Sanchez Gavier Co-authored-by: Mikhail Fesenko Co-authored-by: Alan Protasio --- cmd/prometheus/main.go | 47 +- cmd/prometheus/main_test.go | 64 + config/config.go | 53 +- config/config_test.go | 22 +- config/testdata/conf.good.yml | 1 + .../testdata/remote_write_wrong_msg.bad.yml | 3 + docs/command-line/prometheus.md | 1 + docs/configuration/configuration.md | 23 +- docs/feature_flags.md | 10 + .../example_write_adapter/README.md | 14 +- .../example_write_adapter/server.go | 109 +- documentation/examples/remote_storage/go.mod | 30 +- documentation/examples/remote_storage/go.sum | 129 +- prompb/codec.go | 201 + prompb/custom.go | 8 - prompb/io/prometheus/write/v2/codec.go | 213 ++ prompb/io/prometheus/write/v2/custom.go | 165 + prompb/io/prometheus/write/v2/custom_test.go | 97 + prompb/io/prometheus/write/v2/symbols.go | 83 + prompb/io/prometheus/write/v2/symbols_test.go | 60 + prompb/io/prometheus/write/v2/types.pb.go | 3241 +++++++++++++++++ prompb/io/prometheus/write/v2/types.proto | 260 ++ prompb/io/prometheus/write/v2/types_test.go | 97 + prompb/rwcommon/codec_test.go | 282 ++ scrape/manager.go | 8 +- scrape/scrape.go | 2 +- scripts/genproto.sh | 6 +- storage/remote/client.go | 57 +- storage/remote/codec.go | 219 +- storage/remote/codec_test.go | 308 +- storage/remote/metadata_watcher.go | 4 +- storage/remote/metadata_watcher_test.go | 2 +- .../prometheus/normalize_name.go | 2 - .../otlptranslator/prometheus/unit_to_ucum.go | 1 - storage/remote/queue_manager.go | 468 ++- storage/remote/queue_manager_test.go | 1019 ++++-- storage/remote/read_handler_test.go | 2 +- storage/remote/read_test.go | 8 +- storage/remote/storage.go | 4 +- storage/remote/storage_test.go | 10 +- storage/remote/write.go | 10 +- storage/remote/write_handler.go | 323 +- storage/remote/write_handler_test.go | 529 ++- storage/remote/write_test.go | 142 +- tsdb/agent/db_test.go | 6 +- tsdb/wlog/watcher.go | 17 +- tsdb/wlog/watcher_test.go | 18 +- web/api/v1/api.go | 3 +- web/api/v1/api_test.go | 2 +- web/api/v1/errors_test.go | 1 + web/web.go | 3 + 51 files changed, 7282 insertions(+), 1105 deletions(-) create mode 100644 config/testdata/remote_write_wrong_msg.bad.yml create mode 100644 prompb/codec.go create mode 100644 prompb/io/prometheus/write/v2/codec.go create mode 100644 prompb/io/prometheus/write/v2/custom.go create mode 100644 prompb/io/prometheus/write/v2/custom_test.go create mode 100644 prompb/io/prometheus/write/v2/symbols.go create mode 100644 prompb/io/prometheus/write/v2/symbols_test.go create mode 100644 prompb/io/prometheus/write/v2/types.pb.go create mode 100644 prompb/io/prometheus/write/v2/types.proto create mode 100644 prompb/io/prometheus/write/v2/types_test.go create mode 100644 prompb/rwcommon/codec_test.go diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go index 7544f276a6..1d844ddba6 100644 --- a/cmd/prometheus/main.go +++ b/cmd/prometheus/main.go @@ -194,6 +194,9 @@ func (c *flagConfig) setFeatureListOptions(logger log.Logger) error { case "extra-scrape-metrics": c.scrape.ExtraMetrics = true level.Info(logger).Log("msg", "Experimental additional scrape metrics enabled") + case "metadata-wal-records": + c.scrape.AppendMetadata = true + level.Info(logger).Log("msg", "Experimental metadata records in WAL enabled, required for remote write 2.0") case "new-service-discovery-manager": c.enableNewSDManager = true level.Info(logger).Log("msg", "Experimental service discovery manager") @@ -322,9 +325,15 @@ func main() { a.Flag("web.enable-admin-api", "Enable API endpoints for admin control actions."). Default("false").BoolVar(&cfg.web.EnableAdminAPI) + // TODO(bwplotka): Consider allowing those remote receive flags to be changed in config. + // See https://github.com/prometheus/prometheus/issues/14410 a.Flag("web.enable-remote-write-receiver", "Enable API endpoint accepting remote write requests."). Default("false").BoolVar(&cfg.web.EnableRemoteWriteReceiver) + supportedRemoteWriteProtoMsgs := config.RemoteWriteProtoMsgs{config.RemoteWriteProtoMsgV1, config.RemoteWriteProtoMsgV2} + a.Flag("web.remote-write-receiver.accepted-protobuf-messages", fmt.Sprintf("List of the remote write protobuf messages to accept when receiving the remote writes. Supported values: %v", supportedRemoteWriteProtoMsgs.String())). + Default(supportedRemoteWriteProtoMsgs.Strings()...).SetValue(rwProtoMsgFlagValue(&cfg.web.AcceptRemoteWriteProtoMsgs)) + a.Flag("web.console.templates", "Path to the console template directory, available at /consoles."). Default("consoles").StringVar(&cfg.web.ConsoleTemplatesPath) @@ -646,7 +655,7 @@ func main() { var ( localStorage = &readyStorage{stats: tsdb.NewDBStats()} scraper = &readyScrapeManager{} - remoteStorage = remote.NewStorage(log.With(logger, "component", "remote"), prometheus.DefaultRegisterer, localStorage.StartTime, localStoragePath, time.Duration(cfg.RemoteFlushDeadline), scraper) + remoteStorage = remote.NewStorage(log.With(logger, "component", "remote"), prometheus.DefaultRegisterer, localStorage.StartTime, localStoragePath, time.Duration(cfg.RemoteFlushDeadline), scraper, cfg.scrape.AppendMetadata) fanoutStorage = storage.NewFanout(logger, localStorage, remoteStorage) ) @@ -1767,3 +1776,39 @@ type discoveryManager interface { Run() error SyncCh() <-chan map[string][]*targetgroup.Group } + +// rwProtoMsgFlagParser is a custom parser for config.RemoteWriteProtoMsg enum. +type rwProtoMsgFlagParser struct { + msgs *[]config.RemoteWriteProtoMsg +} + +func rwProtoMsgFlagValue(msgs *[]config.RemoteWriteProtoMsg) kingpin.Value { + return &rwProtoMsgFlagParser{msgs: msgs} +} + +// IsCumulative is used by kingpin to tell if it's an array or not. +func (p *rwProtoMsgFlagParser) IsCumulative() bool { + return true +} + +func (p *rwProtoMsgFlagParser) String() string { + ss := make([]string, 0, len(*p.msgs)) + for _, t := range *p.msgs { + ss = append(ss, string(t)) + } + return strings.Join(ss, ",") +} + +func (p *rwProtoMsgFlagParser) Set(opt string) error { + t := config.RemoteWriteProtoMsg(opt) + if err := t.Validate(); err != nil { + return err + } + for _, prev := range *p.msgs { + if prev == t { + return fmt.Errorf("duplicated %v flag value, got %v already", t, *p.msgs) + } + } + *p.msgs = append(*p.msgs, t) + return nil +} diff --git a/cmd/prometheus/main_test.go b/cmd/prometheus/main_test.go index 89c171bb5b..c827812e60 100644 --- a/cmd/prometheus/main_test.go +++ b/cmd/prometheus/main_test.go @@ -30,11 +30,13 @@ import ( "testing" "time" + "github.com/alecthomas/kingpin/v2" "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" + "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/notifier" "github.com/prometheus/prometheus/rules" @@ -499,3 +501,65 @@ func TestDocumentation(t *testing.T) { require.Equal(t, string(expectedContent), generatedContent, "Generated content does not match documentation. Hint: run `make cli-documentation`.") } + +func TestRwProtoMsgFlagParser(t *testing.T) { + defaultOpts := config.RemoteWriteProtoMsgs{ + config.RemoteWriteProtoMsgV1, config.RemoteWriteProtoMsgV2, + } + + for _, tcase := range []struct { + args []string + expected []config.RemoteWriteProtoMsg + expectedErr error + }{ + { + args: nil, + expected: defaultOpts, + }, + { + args: []string{"--test-proto-msgs", "test"}, + expectedErr: errors.New("unknown remote write protobuf message test, supported: prometheus.WriteRequest, io.prometheus.write.v2.Request"), + }, + { + args: []string{"--test-proto-msgs", "io.prometheus.write.v2.Request"}, + expected: config.RemoteWriteProtoMsgs{config.RemoteWriteProtoMsgV2}, + }, + { + args: []string{ + "--test-proto-msgs", "io.prometheus.write.v2.Request", + "--test-proto-msgs", "io.prometheus.write.v2.Request", + }, + expectedErr: errors.New("duplicated io.prometheus.write.v2.Request flag value, got [io.prometheus.write.v2.Request] already"), + }, + { + args: []string{ + "--test-proto-msgs", "io.prometheus.write.v2.Request", + "--test-proto-msgs", "prometheus.WriteRequest", + }, + expected: config.RemoteWriteProtoMsgs{config.RemoteWriteProtoMsgV2, config.RemoteWriteProtoMsgV1}, + }, + { + args: []string{ + "--test-proto-msgs", "io.prometheus.write.v2.Request", + "--test-proto-msgs", "prometheus.WriteRequest", + "--test-proto-msgs", "io.prometheus.write.v2.Request", + }, + expectedErr: errors.New("duplicated io.prometheus.write.v2.Request flag value, got [io.prometheus.write.v2.Request prometheus.WriteRequest] already"), + }, + } { + t.Run(strings.Join(tcase.args, ","), func(t *testing.T) { + a := kingpin.New("test", "") + var opt []config.RemoteWriteProtoMsg + a.Flag("test-proto-msgs", "").Default(defaultOpts.Strings()...).SetValue(rwProtoMsgFlagValue(&opt)) + + _, err := a.Parse(tcase.args) + if tcase.expectedErr != nil { + require.Error(t, err) + require.Equal(t, tcase.expectedErr, err) + } else { + require.NoError(t, err) + require.Equal(t, tcase.expected, opt) + } + }) + } +} diff --git a/config/config.go b/config/config.go index 9defa10d48..c924e30989 100644 --- a/config/config.go +++ b/config/config.go @@ -180,6 +180,7 @@ var ( // DefaultRemoteWriteConfig is the default remote write configuration. DefaultRemoteWriteConfig = RemoteWriteConfig{ RemoteTimeout: model.Duration(30 * time.Second), + ProtobufMessage: RemoteWriteProtoMsgV1, QueueConfig: DefaultQueueConfig, MetadataConfig: DefaultMetadataConfig, HTTPClientConfig: config.DefaultHTTPClientConfig, @@ -279,7 +280,7 @@ func (c *Config) GetScrapeConfigs() ([]*ScrapeConfig, error) { jobNames := map[string]string{} for i, scfg := range c.ScrapeConfigs { - // We do these checks for library users that would not call Validate in + // We do these checks for library users that would not call validate in // Unmarshal. if err := scfg.Validate(c.GlobalConfig); err != nil { return nil, err @@ -1055,6 +1056,49 @@ func CheckTargetAddress(address model.LabelValue) error { return nil } +// RemoteWriteProtoMsg represents the known protobuf message for the remote write +// 1.0 and 2.0 specs. +type RemoteWriteProtoMsg string + +// Validate returns error if the given reference for the protobuf message is not supported. +func (s RemoteWriteProtoMsg) Validate() error { + switch s { + case RemoteWriteProtoMsgV1, RemoteWriteProtoMsgV2: + return nil + default: + return fmt.Errorf("unknown remote write protobuf message %v, supported: %v", s, RemoteWriteProtoMsgs{RemoteWriteProtoMsgV1, RemoteWriteProtoMsgV2}.String()) + } +} + +type RemoteWriteProtoMsgs []RemoteWriteProtoMsg + +func (m RemoteWriteProtoMsgs) Strings() []string { + ret := make([]string, 0, len(m)) + for _, typ := range m { + ret = append(ret, string(typ)) + } + return ret +} + +func (m RemoteWriteProtoMsgs) String() string { + return strings.Join(m.Strings(), ", ") +} + +var ( + // RemoteWriteProtoMsgV1 represents the deprecated `prometheus.WriteRequest` protobuf + // message introduced in the https://prometheus.io/docs/specs/remote_write_spec/. + // + // NOTE: This string is used for both HTTP header values and config value, so don't change + // this reference. + RemoteWriteProtoMsgV1 RemoteWriteProtoMsg = "prometheus.WriteRequest" + // RemoteWriteProtoMsgV2 represents the `io.prometheus.write.v2.Request` protobuf + // message introduced in https://prometheus.io/docs/specs/remote_write_spec_2_0/ + // + // NOTE: This string is used for both HTTP header values and config value, so don't change + // this reference. + RemoteWriteProtoMsgV2 RemoteWriteProtoMsg = "io.prometheus.write.v2.Request" +) + // RemoteWriteConfig is the configuration for writing to remote storage. type RemoteWriteConfig struct { URL *config.URL `yaml:"url"` @@ -1064,6 +1108,9 @@ type RemoteWriteConfig struct { Name string `yaml:"name,omitempty"` SendExemplars bool `yaml:"send_exemplars,omitempty"` SendNativeHistograms bool `yaml:"send_native_histograms,omitempty"` + // ProtobufMessage specifies the protobuf message to use against the remote + // receiver as specified in https://prometheus.io/docs/specs/remote_write_spec_2_0/ + ProtobufMessage RemoteWriteProtoMsg `yaml:"protobuf_message,omitempty"` // We cannot do proper Go type embedding below as the parser will then parse // values arbitrarily into the overflow maps of further-down types. @@ -1098,6 +1145,10 @@ func (c *RemoteWriteConfig) UnmarshalYAML(unmarshal func(interface{}) error) err return err } + if err := c.ProtobufMessage.Validate(); err != nil { + return fmt.Errorf("invalid protobuf_message value: %w", err) + } + // The UnmarshalYAML method of HTTPClientConfig is not being called because it's not a pointer. // We cannot make it a pointer as the parser panics for inlined pointer structs. // Thus we just do its validation here. diff --git a/config/config_test.go b/config/config_test.go index d84059b48f..3c4907a46c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -108,9 +108,10 @@ var expectedConf = &Config{ RemoteWriteConfigs: []*RemoteWriteConfig{ { - URL: mustParseURL("http://remote1/push"), - RemoteTimeout: model.Duration(30 * time.Second), - Name: "drop_expensive", + URL: mustParseURL("http://remote1/push"), + ProtobufMessage: RemoteWriteProtoMsgV1, + RemoteTimeout: model.Duration(30 * time.Second), + Name: "drop_expensive", WriteRelabelConfigs: []*relabel.Config{ { SourceLabels: model.LabelNames{"__name__"}, @@ -137,11 +138,12 @@ var expectedConf = &Config{ }, }, { - URL: mustParseURL("http://remote2/push"), - RemoteTimeout: model.Duration(30 * time.Second), - QueueConfig: DefaultQueueConfig, - MetadataConfig: DefaultMetadataConfig, - Name: "rw_tls", + URL: mustParseURL("http://remote2/push"), + ProtobufMessage: RemoteWriteProtoMsgV2, + RemoteTimeout: model.Duration(30 * time.Second), + QueueConfig: DefaultQueueConfig, + MetadataConfig: DefaultMetadataConfig, + Name: "rw_tls", HTTPClientConfig: config.HTTPClientConfig{ TLSConfig: config.TLSConfig{ CertFile: filepath.FromSlash("testdata/valid_cert_file"), @@ -1800,6 +1802,10 @@ var expectedErrors = []struct { filename: "remote_write_authorization_header.bad.yml", errMsg: `authorization header must be changed via the basic_auth, authorization, oauth2, sigv4, or azuread parameter`, }, + { + filename: "remote_write_wrong_msg.bad.yml", + errMsg: `invalid protobuf_message value: unknown remote write protobuf message io.prometheus.writet.v2.Request, supported: prometheus.WriteRequest, io.prometheus.write.v2.Request`, + }, { filename: "remote_write_url_missing.bad.yml", errMsg: `url for remote_write is empty`, diff --git a/config/testdata/conf.good.yml b/config/testdata/conf.good.yml index 184e6363ce..0e0aa2bd5d 100644 --- a/config/testdata/conf.good.yml +++ b/config/testdata/conf.good.yml @@ -37,6 +37,7 @@ remote_write: key_file: valid_key_file - url: http://remote2/push + protobuf_message: io.prometheus.write.v2.Request name: rw_tls tls_config: cert_file: valid_cert_file diff --git a/config/testdata/remote_write_wrong_msg.bad.yml b/config/testdata/remote_write_wrong_msg.bad.yml new file mode 100644 index 0000000000..0918309540 --- /dev/null +++ b/config/testdata/remote_write_wrong_msg.bad.yml @@ -0,0 +1,3 @@ +remote_write: + - url: localhost:9090 + protobuf_message: io.prometheus.writet.v2.Request # typo in 'write" diff --git a/docs/command-line/prometheus.md b/docs/command-line/prometheus.md index 1fc032d09b..2232602430 100644 --- a/docs/command-line/prometheus.md +++ b/docs/command-line/prometheus.md @@ -26,6 +26,7 @@ The Prometheus monitoring server | --web.enable-lifecycle | Enable shutdown and reload via HTTP request. | `false` | | --web.enable-admin-api | Enable API endpoints for admin control actions. | `false` | | --web.enable-remote-write-receiver | Enable API endpoint accepting remote write requests. | `false` | +| --web.remote-write-receiver.accepted-protobuf-messages | List of the remote write protobuf messages to accept when receiving the remote writes. Supported values: prometheus.WriteRequest, io.prometheus.write.v2.Request | `prometheus.WriteRequest` | | --web.console.templates | Path to the console template directory, available at /consoles. | `consoles` | | --web.console.libraries | Path to the console library directory. | `console_libraries` | | --web.page-title | Document title of Prometheus instance. | `Prometheus Time Series Collection and Processing Server` | diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index c03ed49715..35976871b9 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -3575,6 +3575,17 @@ this functionality. # The URL of the endpoint to send samples to. url: +# protobuf message to use when writing to the remote write endpoint. +# +# * The `prometheus.WriteRequest` represents the message introduced in Remote Write 1.0, which +# will be deprecated eventually. +# * The `io.prometheus.write.v2.Request` was introduced in Remote Write 2.0 and replaces the former, +# by improving efficiency and sending metadata, created timestamp and native histograms by default. +# +# Before changing this value, consult with your remote storage provider (or test) what message it supports. +# Read more on https://prometheus.io/docs/specs/remote_write_spec_2_0/#io-prometheus-write-v2-request +[ protobuf_message: | default = prometheus.WriteRequest ] + # Timeout for requests to the remote write endpoint. [ remote_timeout: | default = 30s ] @@ -3596,6 +3607,7 @@ write_relabel_configs: [ send_exemplars: | default = false ] # Enables sending of native histograms, also known as sparse histograms, over remote write. +# For the `io.prometheus.write.v2.Request` message, this option is noop (always true). [ send_native_histograms: | default = false ] # Sets the `Authorization` header on every remote write request with the @@ -3609,7 +3621,7 @@ basic_auth: # Optional `Authorization` header configuration. authorization: # Sets the authentication type. - [ type: | default: Bearer ] + [ type: | default = Bearer ] # Sets the credentials. It is mutually exclusive with # `credentials_file`. [ credentials: ] @@ -3673,7 +3685,7 @@ tls_config: # contain port numbers. [ no_proxy: ] # Use proxy URL indicated by environment variables (HTTP_PROXY, https_proxy, HTTPs_PROXY, https_proxy, and no_proxy) -[ proxy_from_environment: | default: false ] +[ proxy_from_environment: | default = false ] # Specifies headers to send to proxies during CONNECT requests. [ proxy_connect_header: [ : [, ...] ] ] @@ -3682,7 +3694,7 @@ tls_config: [ follow_redirects: | default = true ] # Whether to enable HTTP2. -[ enable_http2: | default: true ] +[ enable_http2: | default = true ] # Configures the queue used to write to remote storage. queue_config: @@ -3712,7 +3724,10 @@ queue_config: # which means that all samples are sent. [ sample_age_limit: | default = 0s ] -# Configures the sending of series metadata to remote storage. +# Configures the sending of series metadata to remote storage +# if the `prometheus.WriteRequest` message was chosen. When +# `io.prometheus.write.v2.Request` is used, metadata is always sent. +# # Metadata configuration is subject to change at any point # or be removed in future releases. metadata_config: diff --git a/docs/feature_flags.md b/docs/feature_flags.md index a5dc69a718..24d70647fd 100644 --- a/docs/feature_flags.md +++ b/docs/feature_flags.md @@ -224,3 +224,13 @@ When the `concurrent-rule-eval` feature flag is enabled, rules without any depen This has the potential to improve rule group evaluation latency and resource utilization at the expense of adding more concurrent query load. The number of concurrent rule evaluations can be configured with `--rules.max-concurrent-rule-evals`, which is set to `4` by default. + +## Metadata WAL Records + +`--enable-feature=metadata-wal-records` + +When enabled, Prometheus will store metadata in-memory and keep track of +metadata changes as WAL records on a per-series basis. + +This must be used if +you are also using remote write 2.0 as it will only gather metadata from the WAL. diff --git a/documentation/examples/remote_storage/example_write_adapter/README.md b/documentation/examples/remote_storage/example_write_adapter/README.md index 9748c448db..739cf3be36 100644 --- a/documentation/examples/remote_storage/example_write_adapter/README.md +++ b/documentation/examples/remote_storage/example_write_adapter/README.md @@ -7,6 +7,7 @@ To use it: ``` go build + ./example_write_adapter ``` @@ -15,10 +16,19 @@ go build ```yaml remote_write: - url: "http://localhost:1234/receive" + protobuf_message: "io.prometheus.write.v2.Request" ``` -Then start Prometheus: +or for deprecated Remote Write 1.0 message: + +```yaml +remote_write: + - url: "http://localhost:1234/receive" + protobuf_message: "prometheus.WriteRequest" +``` + +Then start Prometheus (in separate terminal): ``` -./prometheus +./prometheus --enable-feature=metadata-wal-records ``` diff --git a/documentation/examples/remote_storage/example_write_adapter/server.go b/documentation/examples/remote_storage/example_write_adapter/server.go index 48c0a9571f..727a3056d3 100644 --- a/documentation/examples/remote_storage/example_write_adapter/server.go +++ b/documentation/examples/remote_storage/example_write_adapter/server.go @@ -18,44 +18,103 @@ import ( "log" "net/http" - "github.com/prometheus/common/model" - + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/prompb" + writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2" "github.com/prometheus/prometheus/storage/remote" ) func main() { http.HandleFunc("/receive", func(w http.ResponseWriter, r *http.Request) { - req, err := remote.DecodeWriteRequest(r.Body) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + enc := r.Header.Get("Content-Encoding") + if enc == "" { + http.Error(w, "missing Content-Encoding header", http.StatusUnsupportedMediaType) + return + } + if enc != "snappy" { + http.Error(w, "unknown encoding, only snappy supported", http.StatusUnsupportedMediaType) return } - for _, ts := range req.Timeseries { - m := make(model.Metric, len(ts.Labels)) - for _, l := range ts.Labels { - m[model.LabelName(l.Name)] = model.LabelValue(l.Value) - } - fmt.Println(m) + contentType := r.Header.Get("Content-Type") + if contentType == "" { + http.Error(w, "missing Content-Type header", http.StatusUnsupportedMediaType) + } - for _, s := range ts.Samples { - fmt.Printf("\tSample: %f %d\n", s.Value, s.Timestamp) - } + defer func() { _ = r.Body.Close() }() - for _, e := range ts.Exemplars { - m := make(model.Metric, len(e.Labels)) - for _, l := range e.Labels { - m[model.LabelName(l.Name)] = model.LabelValue(l.Value) - } - fmt.Printf("\tExemplar: %+v %f %d\n", m, e.Value, e.Timestamp) + // Very simplistic content parsing, see + // storage/remote/write_handler.go#WriteHandler.ServeHTTP for production example. + switch contentType { + case "application/x-protobuf", "application/x-protobuf;proto=prometheus.WriteRequest": + req, err := remote.DecodeWriteRequest(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return } - - for _, hp := range ts.Histograms { - h := remote.HistogramProtoToHistogram(hp) - fmt.Printf("\tHistogram: %s\n", h.String()) + printV1(req) + case "application/x-protobuf;proto=io.prometheus.write.v2.Request": + req, err := remote.DecodeWriteV2Request(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return } + printV2(req) + default: + msg := fmt.Sprintf("Unknown remote write content type: %s", contentType) + fmt.Println(msg) + http.Error(w, msg, http.StatusBadRequest) } }) - log.Fatal(http.ListenAndServe(":1234", nil)) } + +func printV1(req *prompb.WriteRequest) { + b := labels.NewScratchBuilder(0) + for _, ts := range req.Timeseries { + fmt.Println(ts.ToLabels(&b, nil)) + + for _, s := range ts.Samples { + fmt.Printf("\tSample: %f %d\n", s.Value, s.Timestamp) + } + for _, ep := range ts.Exemplars { + e := ep.ToExemplar(&b, nil) + fmt.Printf("\tExemplar: %+v %f %d\n", e.Labels, e.Value, ep.Timestamp) + } + for _, hp := range ts.Histograms { + if hp.IsFloatHistogram() { + h := hp.ToFloatHistogram() + fmt.Printf("\tHistogram: %s\n", h.String()) + continue + } + h := hp.ToIntHistogram() + fmt.Printf("\tHistogram: %s\n", h.String()) + } + } +} + +func printV2(req *writev2.Request) { + b := labels.NewScratchBuilder(0) + for _, ts := range req.Timeseries { + l := ts.ToLabels(&b, req.Symbols) + m := ts.ToMetadata(req.Symbols) + fmt.Println(l, m) + + for _, s := range ts.Samples { + fmt.Printf("\tSample: %f %d\n", s.Value, s.Timestamp) + } + for _, ep := range ts.Exemplars { + e := ep.ToExemplar(&b, req.Symbols) + fmt.Printf("\tExemplar: %+v %f %d\n", e.Labels, e.Value, ep.Timestamp) + } + for _, hp := range ts.Histograms { + if hp.IsFloatHistogram() { + h := hp.ToFloatHistogram() + fmt.Printf("\tHistogram: %s\n", h.String()) + continue + } + h := hp.ToIntHistogram() + fmt.Printf("\tHistogram: %s\n", h.String()) + } + } +} diff --git a/documentation/examples/remote_storage/go.mod b/documentation/examples/remote_storage/go.mod index d4f19749dc..2491bbe2db 100644 --- a/documentation/examples/remote_storage/go.mod +++ b/documentation/examples/remote_storage/go.mod @@ -17,10 +17,10 @@ require ( require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect - github.com/aws/aws-sdk-go v1.51.25 // indirect + github.com/aws/aws-sdk-go v1.53.16 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -31,8 +31,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect - github.com/hashicorp/go-version v1.6.0 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -49,13 +48,12 @@ require ( github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect - go.opentelemetry.io/collector/featuregate v1.5.0 // indirect - go.opentelemetry.io/collector/pdata v1.5.0 // indirect - go.opentelemetry.io/collector/semconv v0.98.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 // indirect - go.opentelemetry.io/otel v1.25.0 // indirect - go.opentelemetry.io/otel/metric v1.25.0 // indirect - go.opentelemetry.io/otel/trace v1.25.0 // indirect + go.opentelemetry.io/collector/pdata v1.8.0 // indirect + go.opentelemetry.io/collector/semconv v0.101.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect + go.opentelemetry.io/otel v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.24.0 // indirect @@ -64,8 +62,8 @@ require ( golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect - google.golang.org/grpc v1.63.2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/grpc v1.64.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -82,4 +80,10 @@ exclude ( cloud.google.com/go v0.34.0 cloud.google.com/go v0.65.0 cloud.google.com/go v0.82.0 + + // Fixing ambiguous import: found package google.golang.org/genproto/googleapis/api/annotations in multiple modules. + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 ) + +// TODO(bwplotka): Move to main branch commit or perhaps released version. +replace github.com/prometheus/prometheus => github.com/prometheus/prometheus v0.53.1-0.20240704074759-c137febfcf8c diff --git a/documentation/examples/remote_storage/go.sum b/documentation/examples/remote_storage/go.sum index ec04347111..9898d75d70 100644 --- a/documentation/examples/remote_storage/go.sum +++ b/documentation/examples/remote_storage/go.sum @@ -2,10 +2,10 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqb github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 h1:FDif4R1+UUR+00q6wquyX90K7A8dN+R5E8GEadoP7sU= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2/go.mod h1:aiYBYui4BJ/BJCAIKs92XiPyQfTaBWqvHujDwKb6CBU= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.6.0 h1:ui3YNbxfW7J3tTFIZMH6LIGRjCngp+J+nIFlnizfNTE= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.6.0/go.mod h1:gZmgV+qBqygoznvqo2J9oKZAFziqhLZ2xE/WVUmzkHA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0 h1:sUFnFjzDUie80h24I7mrKtwCKgLY9L8h5Tp2x9+TWqk= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0/go.mod h1:52JbnQTp15qg5mRkMBHwp0j0ZFwHJ42Sx3zVV5RE9p0= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0/go.mod h1:QyiQdW4f4/BIfB8ZutZ2s+28RAgfa/pT+zS++ZHyM1I= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0 h1:bXwSugBiSbgtz7rOtbfGf+woewp4f06orW9OP5BjHLA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0/go.mod h1:Y/HgrePTmGy9HjdSGTqZNa+apUpTVIEVKXJyARP2lrk= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= @@ -26,8 +26,8 @@ github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8V github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.51.25 h1:DjTT8mtmsachhV6yrXR8+yhnG6120dazr720nopRsls= -github.com/aws/aws-sdk-go v1.51.25/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go v1.53.16 h1:8oZjKQO/ml1WLUZw5hvF7pvYjPf8o9f57Wldoy/q9Qc= +github.com/aws/aws-sdk-go v1.53.16/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -37,8 +37,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa h1:jQCWAUqqlij9Pgj2i/PB79y4KOPYVyFYdROxgaCwdTQ= -github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM= +github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc= +github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -46,14 +46,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= -github.com/digitalocean/godo v1.113.0 h1:CLtCxlP4wDAjKIQ+Hshht/UNbgAp8/J/XBH1ZtDCF9Y= -github.com/digitalocean/godo v1.113.0/go.mod h1:Z2mTP848Vi3IXXl5YbPekUgr4j4tOePomA+OE1Ag98w= +github.com/digitalocean/godo v1.117.0 h1:WVlTe09melDYTd7VCVyvHcNWbgB+uI1O115+5LOtdSw= +github.com/digitalocean/godo v1.117.0/go.mod h1:Vk0vpCot2HOAJwc5WE8wljZGtJ3ZtWIc8MQ8rF38sdo= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/docker/docker v26.0.1+incompatible h1:t39Hm6lpXuXtgkF0dm1t9a5HkbUfdGy6XbWexmGr+hA= -github.com/docker/docker v26.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.1.3+incompatible h1:lLCzRbrVZrljpVNobJu1J2FHk8V0s4BawoZippkc+xo= +github.com/docker/docker v26.1.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -68,8 +68,8 @@ github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -95,8 +95,8 @@ github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdX github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= -github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA= -github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= +github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g= +github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= @@ -135,40 +135,38 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gophercloud/gophercloud v1.11.0 h1:ls0O747DIq1D8SUHc7r2vI8BFbMLeLFuENaAIfEx7OM= -github.com/gophercloud/gophercloud v1.11.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= +github.com/gophercloud/gophercloud v1.12.0 h1:Jrz16vPAL93l80q16fp8NplrTCp93y7rZh2P3Q4Yq7g= +github.com/gophercloud/gophercloud v1.12.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd h1:PpuIBO5P3e9hpqBD0O/HjhShYuM6XE0i/lbE6J94kww= -github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= -github.com/hashicorp/consul/api v1.28.2 h1:mXfkRHrpHN4YY3RqL09nXU1eHKLNiuAN4kHvDQ16k/8= -github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= +github.com/hashicorp/consul/api v1.29.1 h1:UEwOjYJrd3lG1x5w7HxDRMGiAUPrb3f103EoeKuuEcc= +github.com/hashicorp/consul/api v1.29.1/go.mod h1:lumfRkY/coLuqMICkI7Fh3ylMG31mQSRZyef2c5YvJI= github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A= github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= -github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= -github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/nomad/api v0.0.0-20240418183417-ea5f2f6748c7 h1:pjE59CS2C9Bg+Xby0ROrnZSSBWtKwx3Sf9gqsrvIFSA= -github.com/hashicorp/nomad/api v0.0.0-20240418183417-ea5f2f6748c7/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE= +github.com/hashicorp/nomad/api v0.0.0-20240604134157-e73d8bb1140d h1:KHq+mAzWSkumj4PDoXc5VZbycPGcmYu8tohgVLQ6SIc= +github.com/hashicorp/nomad/api v0.0.0-20240604134157-e73d8bb1140d/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= -github.com/hetznercloud/hcloud-go/v2 v2.7.2 h1:UlE7n1GQZacCfyjv9tDVUN7HZfOXErPIfM/M039u9A0= -github.com/hetznercloud/hcloud-go/v2 v2.7.2/go.mod h1:49tIV+pXRJTUC7fbFZ03s45LKqSQdOPP5y91eOnJo/k= +github.com/hetznercloud/hcloud-go/v2 v2.9.0 h1:s0N6R7Zoi2DPfMtUF5o9VeUBzTtHVY6MIkHOQnfu/AY= +github.com/hetznercloud/hcloud-go/v2 v2.9.0/go.mod h1:qtW/TuU7Bs16ibXl/ktJarWqU2LwHr7eGlwoilHxtgg= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/influxdata/influxdb v1.11.5 h1:+em5VOl6lhAZubXj5o6SobCwvrRs3XDlBx/MUI4schI= @@ -208,14 +206,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/linode/linodego v1.32.0 h1:OmZzB3iON6uu84VtLFf64uKmAQqJJarvmsVguroioPI= -github.com/linode/linodego v1.32.0/go.mod h1:y8GDP9uLVH4jTB9qyrgw79qfKdYJmNCGUOJmfuiOcmI= +github.com/linode/linodego v1.35.0 h1:rIhUeCHBLEDlkoRnOTwzSGzljQ3ksXwLxacmXnrV+Do= +github.com/linode/linodego v1.35.0/go.mod h1:JxuhOEAMfSxun6RU5/MgTKH2GGTmFrhKRj3wL1NFin0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= @@ -243,8 +241,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0= -github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY= +github.com/ovh/go-ovh v1.5.1 h1:P8O+7H+NQuFK9P/j4sFW5C0fvSS2DnHYGPwdVCp45wI= +github.com/ovh/go-ovh v1.5.1/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -279,12 +277,12 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/prometheus/prometheus v0.52.1 h1:BrQ29YG+mzdGh8DgHPirHbeMGNqtL+INe0rqg7ttBJ4= -github.com/prometheus/prometheus v0.52.1/go.mod h1:3z74cVsmVH0iXOR5QBjB7Pa6A0KJeEAK5A6UsmAFb1g= +github.com/prometheus/prometheus v0.53.1-0.20240704074759-c137febfcf8c h1:6GEA48LnonkYZhQ654v7QTIP5uBTbCEVm49oIhif5lc= +github.com/prometheus/prometheus v0.53.1-0.20240704074759-c137febfcf8c/go.mod h1:FcNs5wa7M9yV8IlxlB/05s5oy9vULUIlu/tZsviRIT8= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.26 h1:F+GIVtGqCFxPxO46ujf8cEOP574MBoRm3gNbPXECbxs= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.26/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.27 h1:yGAraK1uUjlhSXgNMIy8o/J4LFNcy7yeipBqt9N9mVg= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.27/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -306,20 +304,18 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8 github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/collector/featuregate v1.5.0 h1:uK8qnYQKz1TMkK+FDTFsywg/EybW/gbnOUaPNUkRznM= -go.opentelemetry.io/collector/featuregate v1.5.0/go.mod h1:w7nUODKxEi3FLf1HslCiE6YWtMtOOrMnSwsDam8Mg9w= -go.opentelemetry.io/collector/pdata v1.5.0 h1:1fKTmUpr0xCOhP/B0VEvtz7bYPQ45luQ8XFyA07j8LE= -go.opentelemetry.io/collector/pdata v1.5.0/go.mod h1:TYj8aKRWZyT/KuKQXKyqSEvK/GV+slFaDMEI+Ke64Yw= -go.opentelemetry.io/collector/semconv v0.98.0 h1:zO4L4TmlxXoYu8UgPeYElGY19BW7wPjM+quL5CzoOoY= -go.opentelemetry.io/collector/semconv v0.98.0/go.mod h1:8ElcRZ8Cdw5JnvhTOQOdYizkJaQ10Z2fS+R6djOnj6A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 h1:cEPbyTSEHlQR89XVlyo78gqluF8Y3oMeBkXGWzQsfXY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8= -go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= -go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= -go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= -go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= -go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= -go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= +go.opentelemetry.io/collector/pdata v1.8.0 h1:d/QQgZxB4Y+d3mqLVh2ozvzujUhloD3P/fk7X+In764= +go.opentelemetry.io/collector/pdata v1.8.0/go.mod h1:/W7clu0wFC4WSRp94Ucn6Vm36Wkrt+tmtlDb1aiNZCY= +go.opentelemetry.io/collector/semconv v0.101.0 h1:tOe9iTe9dDCnvz/bqgfNRr4w80kXG8505tQJ5h5v08Q= +go.opentelemetry.io/collector/semconv v0.101.0/go.mod h1:8ElcRZ8Cdw5JnvhTOQOdYizkJaQ10Z2fS+R6djOnj6A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -336,8 +332,8 @@ golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRj golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -397,21 +393,20 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU= -google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/prompb/codec.go b/prompb/codec.go new file mode 100644 index 0000000000..ad30cd5e7b --- /dev/null +++ b/prompb/codec.go @@ -0,0 +1,201 @@ +// Copyright 2024 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prompb + +import ( + "strings" + + "github.com/prometheus/common/model" + + "github.com/prometheus/prometheus/model/exemplar" + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" +) + +// NOTE(bwplotka): This file's code is tested in /prompb/rwcommon. + +// ToLabels return model labels.Labels from timeseries' remote labels. +func (m TimeSeries) ToLabels(b *labels.ScratchBuilder, _ []string) labels.Labels { + return labelProtosToLabels(b, m.GetLabels()) +} + +// ToLabels return model labels.Labels from timeseries' remote labels. +func (m ChunkedSeries) ToLabels(b *labels.ScratchBuilder, _ []string) labels.Labels { + return labelProtosToLabels(b, m.GetLabels()) +} + +func labelProtosToLabels(b *labels.ScratchBuilder, labelPairs []Label) labels.Labels { + b.Reset() + for _, l := range labelPairs { + b.Add(l.Name, l.Value) + } + b.Sort() + return b.Labels() +} + +// FromLabels transforms labels into prompb labels. The buffer slice +// will be used to avoid allocations if it is big enough to store the labels. +func FromLabels(lbls labels.Labels, buf []Label) []Label { + result := buf[:0] + lbls.Range(func(l labels.Label) { + result = append(result, Label{ + Name: l.Name, + Value: l.Value, + }) + }) + return result +} + +// FromMetadataType transforms a Prometheus metricType into prompb metricType. Since the former is a string we need to transform it to an enum. +func FromMetadataType(t model.MetricType) MetricMetadata_MetricType { + mt := strings.ToUpper(string(t)) + v, ok := MetricMetadata_MetricType_value[mt] + if !ok { + return MetricMetadata_UNKNOWN + } + return MetricMetadata_MetricType(v) +} + +// IsFloatHistogram returns true if the histogram is float. +func (h Histogram) IsFloatHistogram() bool { + _, ok := h.GetCount().(*Histogram_CountFloat) + return ok +} + +// ToIntHistogram returns integer Prometheus histogram from the remote implementation +// of integer histogram. If it's a float histogram, the method returns nil. +func (h Histogram) ToIntHistogram() *histogram.Histogram { + if h.IsFloatHistogram() { + return nil + } + return &histogram.Histogram{ + CounterResetHint: histogram.CounterResetHint(h.ResetHint), + Schema: h.Schema, + ZeroThreshold: h.ZeroThreshold, + ZeroCount: h.GetZeroCountInt(), + Count: h.GetCountInt(), + Sum: h.Sum, + PositiveSpans: spansProtoToSpans(h.GetPositiveSpans()), + PositiveBuckets: h.GetPositiveDeltas(), + NegativeSpans: spansProtoToSpans(h.GetNegativeSpans()), + NegativeBuckets: h.GetNegativeDeltas(), + } +} + +// ToFloatHistogram returns float Prometheus histogram from the remote implementation +// of float histogram. If the underlying implementation is an integer histogram, a +// conversion is performed. +func (h Histogram) ToFloatHistogram() *histogram.FloatHistogram { + if h.IsFloatHistogram() { + return &histogram.FloatHistogram{ + CounterResetHint: histogram.CounterResetHint(h.ResetHint), + Schema: h.Schema, + ZeroThreshold: h.ZeroThreshold, + ZeroCount: h.GetZeroCountFloat(), + Count: h.GetCountFloat(), + Sum: h.Sum, + PositiveSpans: spansProtoToSpans(h.GetPositiveSpans()), + PositiveBuckets: h.GetPositiveCounts(), + NegativeSpans: spansProtoToSpans(h.GetNegativeSpans()), + NegativeBuckets: h.GetNegativeCounts(), + } + } + // Conversion from integer histogram. + return &histogram.FloatHistogram{ + CounterResetHint: histogram.CounterResetHint(h.ResetHint), + Schema: h.Schema, + ZeroThreshold: h.ZeroThreshold, + ZeroCount: float64(h.GetZeroCountInt()), + Count: float64(h.GetCountInt()), + Sum: h.Sum, + PositiveSpans: spansProtoToSpans(h.GetPositiveSpans()), + PositiveBuckets: deltasToCounts(h.GetPositiveDeltas()), + NegativeSpans: spansProtoToSpans(h.GetNegativeSpans()), + NegativeBuckets: deltasToCounts(h.GetNegativeDeltas()), + } +} + +func spansProtoToSpans(s []BucketSpan) []histogram.Span { + spans := make([]histogram.Span, len(s)) + for i := 0; i < len(s); i++ { + spans[i] = histogram.Span{Offset: s[i].Offset, Length: s[i].Length} + } + + return spans +} + +func deltasToCounts(deltas []int64) []float64 { + counts := make([]float64, len(deltas)) + var cur float64 + for i, d := range deltas { + cur += float64(d) + counts[i] = cur + } + return counts +} + +// FromIntHistogram returns remote Histogram from the integer Histogram. +func FromIntHistogram(timestamp int64, h *histogram.Histogram) Histogram { + return Histogram{ + Count: &Histogram_CountInt{CountInt: h.Count}, + Sum: h.Sum, + Schema: h.Schema, + ZeroThreshold: h.ZeroThreshold, + ZeroCount: &Histogram_ZeroCountInt{ZeroCountInt: h.ZeroCount}, + NegativeSpans: spansToSpansProto(h.NegativeSpans), + NegativeDeltas: h.NegativeBuckets, + PositiveSpans: spansToSpansProto(h.PositiveSpans), + PositiveDeltas: h.PositiveBuckets, + ResetHint: Histogram_ResetHint(h.CounterResetHint), + Timestamp: timestamp, + } +} + +// FromFloatHistogram returns remote Histogram from the float Histogram. +func FromFloatHistogram(timestamp int64, fh *histogram.FloatHistogram) Histogram { + return Histogram{ + Count: &Histogram_CountFloat{CountFloat: fh.Count}, + Sum: fh.Sum, + Schema: fh.Schema, + ZeroThreshold: fh.ZeroThreshold, + ZeroCount: &Histogram_ZeroCountFloat{ZeroCountFloat: fh.ZeroCount}, + NegativeSpans: spansToSpansProto(fh.NegativeSpans), + NegativeCounts: fh.NegativeBuckets, + PositiveSpans: spansToSpansProto(fh.PositiveSpans), + PositiveCounts: fh.PositiveBuckets, + ResetHint: Histogram_ResetHint(fh.CounterResetHint), + Timestamp: timestamp, + } +} + +func spansToSpansProto(s []histogram.Span) []BucketSpan { + spans := make([]BucketSpan, len(s)) + for i := 0; i < len(s); i++ { + spans[i] = BucketSpan{Offset: s[i].Offset, Length: s[i].Length} + } + + return spans +} + +// ToExemplar converts remote exemplar to model exemplar. +func (m Exemplar) ToExemplar(b *labels.ScratchBuilder, _ []string) exemplar.Exemplar { + timestamp := m.Timestamp + + return exemplar.Exemplar{ + Labels: labelProtosToLabels(b, m.GetLabels()), + Value: m.Value, + Ts: timestamp, + HasTs: timestamp != 0, + } +} diff --git a/prompb/custom.go b/prompb/custom.go index 13d6e0f0cd..f73ddd446b 100644 --- a/prompb/custom.go +++ b/prompb/custom.go @@ -17,14 +17,6 @@ import ( "sync" ) -func (m Sample) T() int64 { return m.Timestamp } -func (m Sample) V() float64 { return m.Value } - -func (h Histogram) IsFloatHistogram() bool { - _, ok := h.GetCount().(*Histogram_CountFloat) - return ok -} - func (r *ChunkedReadResponse) PooledMarshal(p *sync.Pool) ([]byte, error) { size := r.Size() data, ok := p.Get().(*[]byte) diff --git a/prompb/io/prometheus/write/v2/codec.go b/prompb/io/prometheus/write/v2/codec.go new file mode 100644 index 0000000000..2939941a88 --- /dev/null +++ b/prompb/io/prometheus/write/v2/codec.go @@ -0,0 +1,213 @@ +// Copyright 2024 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package writev2 + +import ( + "github.com/prometheus/common/model" + + "github.com/prometheus/prometheus/model/exemplar" + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/metadata" +) + +// NOTE(bwplotka): This file's code is tested in /prompb/rwcommon. + +// ToLabels return model labels.Labels from timeseries' remote labels. +func (m TimeSeries) ToLabels(b *labels.ScratchBuilder, symbols []string) labels.Labels { + return desymbolizeLabels(b, m.GetLabelsRefs(), symbols) +} + +// ToMetadata return model metadata from timeseries' remote metadata. +func (m TimeSeries) ToMetadata(symbols []string) metadata.Metadata { + typ := model.MetricTypeUnknown + switch m.Metadata.Type { + case Metadata_METRIC_TYPE_COUNTER: + typ = model.MetricTypeCounter + case Metadata_METRIC_TYPE_GAUGE: + typ = model.MetricTypeGauge + case Metadata_METRIC_TYPE_HISTOGRAM: + typ = model.MetricTypeHistogram + case Metadata_METRIC_TYPE_GAUGEHISTOGRAM: + typ = model.MetricTypeGaugeHistogram + case Metadata_METRIC_TYPE_SUMMARY: + typ = model.MetricTypeSummary + case Metadata_METRIC_TYPE_INFO: + typ = model.MetricTypeInfo + case Metadata_METRIC_TYPE_STATESET: + typ = model.MetricTypeStateset + } + return metadata.Metadata{ + Type: typ, + Unit: symbols[m.Metadata.UnitRef], + Help: symbols[m.Metadata.HelpRef], + } +} + +// FromMetadataType transforms a Prometheus metricType into writev2 metricType. +// Since the former is a string we need to transform it to an enum. +func FromMetadataType(t model.MetricType) Metadata_MetricType { + switch t { + case model.MetricTypeCounter: + return Metadata_METRIC_TYPE_COUNTER + case model.MetricTypeGauge: + return Metadata_METRIC_TYPE_GAUGE + case model.MetricTypeHistogram: + return Metadata_METRIC_TYPE_HISTOGRAM + case model.MetricTypeGaugeHistogram: + return Metadata_METRIC_TYPE_GAUGEHISTOGRAM + case model.MetricTypeSummary: + return Metadata_METRIC_TYPE_SUMMARY + case model.MetricTypeInfo: + return Metadata_METRIC_TYPE_INFO + case model.MetricTypeStateset: + return Metadata_METRIC_TYPE_STATESET + default: + return Metadata_METRIC_TYPE_UNSPECIFIED + } +} + +// IsFloatHistogram returns true if the histogram is float. +func (h Histogram) IsFloatHistogram() bool { + _, ok := h.GetCount().(*Histogram_CountFloat) + return ok +} + +// ToIntHistogram returns integer Prometheus histogram from the remote implementation +// of integer histogram. If it's a float histogram, the method returns nil. +// TODO(bwplotka): Add support for incoming NHCB. +func (h Histogram) ToIntHistogram() *histogram.Histogram { + if h.IsFloatHistogram() { + return nil + } + return &histogram.Histogram{ + CounterResetHint: histogram.CounterResetHint(h.ResetHint), + Schema: h.Schema, + ZeroThreshold: h.ZeroThreshold, + ZeroCount: h.GetZeroCountInt(), + Count: h.GetCountInt(), + Sum: h.Sum, + PositiveSpans: spansProtoToSpans(h.GetPositiveSpans()), + PositiveBuckets: h.GetPositiveDeltas(), + NegativeSpans: spansProtoToSpans(h.GetNegativeSpans()), + NegativeBuckets: h.GetNegativeDeltas(), + } +} + +// ToFloatHistogram returns float Prometheus histogram from the remote implementation +// of float histogram. If the underlying implementation is an integer histogram, a +// conversion is performed. +// TODO(bwplotka): Add support for incoming NHCB. +func (h Histogram) ToFloatHistogram() *histogram.FloatHistogram { + if h.IsFloatHistogram() { + return &histogram.FloatHistogram{ + CounterResetHint: histogram.CounterResetHint(h.ResetHint), + Schema: h.Schema, + ZeroThreshold: h.ZeroThreshold, + ZeroCount: h.GetZeroCountFloat(), + Count: h.GetCountFloat(), + Sum: h.Sum, + PositiveSpans: spansProtoToSpans(h.GetPositiveSpans()), + PositiveBuckets: h.GetPositiveCounts(), + NegativeSpans: spansProtoToSpans(h.GetNegativeSpans()), + NegativeBuckets: h.GetNegativeCounts(), + } + } + // Conversion from integer histogram. + return &histogram.FloatHistogram{ + CounterResetHint: histogram.CounterResetHint(h.ResetHint), + Schema: h.Schema, + ZeroThreshold: h.ZeroThreshold, + ZeroCount: float64(h.GetZeroCountInt()), + Count: float64(h.GetCountInt()), + Sum: h.Sum, + PositiveSpans: spansProtoToSpans(h.GetPositiveSpans()), + PositiveBuckets: deltasToCounts(h.GetPositiveDeltas()), + NegativeSpans: spansProtoToSpans(h.GetNegativeSpans()), + NegativeBuckets: deltasToCounts(h.GetNegativeDeltas()), + } +} + +func spansProtoToSpans(s []BucketSpan) []histogram.Span { + spans := make([]histogram.Span, len(s)) + for i := 0; i < len(s); i++ { + spans[i] = histogram.Span{Offset: s[i].Offset, Length: s[i].Length} + } + + return spans +} + +func deltasToCounts(deltas []int64) []float64 { + counts := make([]float64, len(deltas)) + var cur float64 + for i, d := range deltas { + cur += float64(d) + counts[i] = cur + } + return counts +} + +// FromIntHistogram returns remote Histogram from the integer Histogram. +func FromIntHistogram(timestamp int64, h *histogram.Histogram) Histogram { + return Histogram{ + Count: &Histogram_CountInt{CountInt: h.Count}, + Sum: h.Sum, + Schema: h.Schema, + ZeroThreshold: h.ZeroThreshold, + ZeroCount: &Histogram_ZeroCountInt{ZeroCountInt: h.ZeroCount}, + NegativeSpans: spansToSpansProto(h.NegativeSpans), + NegativeDeltas: h.NegativeBuckets, + PositiveSpans: spansToSpansProto(h.PositiveSpans), + PositiveDeltas: h.PositiveBuckets, + ResetHint: Histogram_ResetHint(h.CounterResetHint), + Timestamp: timestamp, + } +} + +// FromFloatHistogram returns remote Histogram from the float Histogram. +func FromFloatHistogram(timestamp int64, fh *histogram.FloatHistogram) Histogram { + return Histogram{ + Count: &Histogram_CountFloat{CountFloat: fh.Count}, + Sum: fh.Sum, + Schema: fh.Schema, + ZeroThreshold: fh.ZeroThreshold, + ZeroCount: &Histogram_ZeroCountFloat{ZeroCountFloat: fh.ZeroCount}, + NegativeSpans: spansToSpansProto(fh.NegativeSpans), + NegativeCounts: fh.NegativeBuckets, + PositiveSpans: spansToSpansProto(fh.PositiveSpans), + PositiveCounts: fh.PositiveBuckets, + ResetHint: Histogram_ResetHint(fh.CounterResetHint), + Timestamp: timestamp, + } +} + +func spansToSpansProto(s []histogram.Span) []BucketSpan { + spans := make([]BucketSpan, len(s)) + for i := 0; i < len(s); i++ { + spans[i] = BucketSpan{Offset: s[i].Offset, Length: s[i].Length} + } + + return spans +} + +func (m Exemplar) ToExemplar(b *labels.ScratchBuilder, symbols []string) exemplar.Exemplar { + timestamp := m.Timestamp + + return exemplar.Exemplar{ + Labels: desymbolizeLabels(b, m.LabelsRefs, symbols), + Value: m.Value, + Ts: timestamp, + HasTs: timestamp != 0, + } +} diff --git a/prompb/io/prometheus/write/v2/custom.go b/prompb/io/prometheus/write/v2/custom.go new file mode 100644 index 0000000000..3aa778eb60 --- /dev/null +++ b/prompb/io/prometheus/write/v2/custom.go @@ -0,0 +1,165 @@ +// Copyright 2024 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package writev2 + +import ( + "slices" +) + +func (m Sample) T() int64 { return m.Timestamp } +func (m Sample) V() float64 { return m.Value } + +func (m *Request) OptimizedMarshal(dst []byte) ([]byte, error) { + siz := m.Size() + if cap(dst) < siz { + dst = make([]byte, siz) + } + n, err := m.OptimizedMarshalToSizedBuffer(dst[:siz]) + if err != nil { + return nil, err + } + return dst[:n], nil +} + +// OptimizedMarshalToSizedBuffer is mostly a copy of the generated MarshalToSizedBuffer, +// but calls OptimizedMarshalToSizedBuffer on the timeseries. +func (m *Request) OptimizedMarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if len(m.Timeseries) > 0 { + for iNdEx := len(m.Timeseries) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Timeseries[iNdEx].OptimizedMarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x2a + } + } + if len(m.Symbols) > 0 { + for iNdEx := len(m.Symbols) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.Symbols[iNdEx]) + copy(dAtA[i:], m.Symbols[iNdEx]) + i = encodeVarintTypes(dAtA, i, uint64(len(m.Symbols[iNdEx]))) + i-- + dAtA[i] = 0x22 + } + } + return len(dAtA) - i, nil +} + +// OptimizedMarshalToSizedBuffer is mostly a copy of the generated MarshalToSizedBuffer, +// but marshals m.LabelsRefs in place without extra allocations. +func (m *TimeSeries) OptimizedMarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.CreatedTimestamp != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.CreatedTimestamp)) + i-- + dAtA[i] = 0x30 + } + { + size, err := m.Metadata.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x2a + if len(m.Histograms) > 0 { + for iNdEx := len(m.Histograms) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Histograms[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.Exemplars) > 0 { + for iNdEx := len(m.Exemplars) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Exemplars[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + } + if len(m.Samples) > 0 { + for iNdEx := len(m.Samples) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Samples[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + + if len(m.LabelsRefs) > 0 { + // This is the trick: encode the varints in reverse order to make it easier + // to do it in place. Then reverse the whole thing. + var j10 int + start := i + for _, num := range m.LabelsRefs { + for num >= 1<<7 { + dAtA[i-1] = uint8(uint64(num)&0x7f | 0x80) + num >>= 7 + i-- + j10++ + } + dAtA[i-1] = uint8(num) + i-- + j10++ + } + slices.Reverse(dAtA[i:start]) + // --- end of trick + + i = encodeVarintTypes(dAtA, i, uint64(j10)) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} diff --git a/prompb/io/prometheus/write/v2/custom_test.go b/prompb/io/prometheus/write/v2/custom_test.go new file mode 100644 index 0000000000..139cbfb225 --- /dev/null +++ b/prompb/io/prometheus/write/v2/custom_test.go @@ -0,0 +1,97 @@ +// Copyright 2023 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package writev2 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOptimizedMarshal(t *testing.T) { + for _, tt := range []struct { + name string + m *Request + }{ + { + name: "empty", + m: &Request{}, + }, + { + name: "simple", + m: &Request{ + Timeseries: []TimeSeries{ + { + LabelsRefs: []uint32{ + 0, 1, + 2, 3, + 4, 5, + 6, 7, + 8, 9, + 10, 11, + 12, 13, + 14, 15, + }, + + Samples: []Sample{{Value: 1, Timestamp: 0}}, + Exemplars: []Exemplar{{LabelsRefs: []uint32{0, 1}, Value: 1, Timestamp: 0}}, + Histograms: nil, + }, + { + LabelsRefs: []uint32{ + 0, 1, + 2, 3, + 4, 5, + 6, 7, + 8, 9, + 10, 11, + 12, 13, + 14, 15, + }, + Samples: []Sample{{Value: 2, Timestamp: 1}}, + Exemplars: []Exemplar{{LabelsRefs: []uint32{0, 1}, Value: 2, Timestamp: 1}}, + Histograms: nil, + }, + }, + Symbols: []string{ + "a", "b", + "c", "d", + "e", "f", + "g", "h", + "i", "j", + "k", "l", + "m", "n", + "o", "p", + }, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + // Keep the slice allocated to mimic what std Marshal + // would give to sized Marshal. + got := make([]byte, 0) + + // Should be the same as the standard marshal. + expected, err := tt.m.Marshal() + require.NoError(t, err) + got, err = tt.m.OptimizedMarshal(got) + require.NoError(t, err) + require.Equal(t, expected, got) + + // Unmarshal should work too. + m := &Request{} + require.NoError(t, m.Unmarshal(got)) + require.Equal(t, tt.m, m) + }) + } +} diff --git a/prompb/io/prometheus/write/v2/symbols.go b/prompb/io/prometheus/write/v2/symbols.go new file mode 100644 index 0000000000..f316a976f2 --- /dev/null +++ b/prompb/io/prometheus/write/v2/symbols.go @@ -0,0 +1,83 @@ +// Copyright 2024 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package writev2 + +import "github.com/prometheus/prometheus/model/labels" + +// SymbolsTable implements table for easy symbol use. +type SymbolsTable struct { + strings []string + symbolsMap map[string]uint32 +} + +// NewSymbolTable returns a symbol table. +func NewSymbolTable() SymbolsTable { + return SymbolsTable{ + // Empty string is required as a first element. + symbolsMap: map[string]uint32{"": 0}, + strings: []string{""}, + } +} + +// Symbolize adds (if not added before) a string to the symbols table, +// while returning its reference number. +func (t *SymbolsTable) Symbolize(str string) uint32 { + if ref, ok := t.symbolsMap[str]; ok { + return ref + } + ref := uint32(len(t.strings)) + t.strings = append(t.strings, str) + t.symbolsMap[str] = ref + return ref +} + +// SymbolizeLabels symbolize Prometheus labels. +func (t *SymbolsTable) SymbolizeLabels(lbls labels.Labels, buf []uint32) []uint32 { + result := buf[:0] + lbls.Range(func(l labels.Label) { + off := t.Symbolize(l.Name) + result = append(result, off) + off = t.Symbolize(l.Value) + result = append(result, off) + }) + return result +} + +// Symbols returns computes symbols table to put in e.g. Request.Symbols. +// As per spec, order does not matter. +func (t *SymbolsTable) Symbols() []string { + return t.strings +} + +// Reset clears symbols table. +func (t *SymbolsTable) Reset() { + // NOTE: Make sure to keep empty symbol. + t.strings = t.strings[:1] + for k := range t.symbolsMap { + if k == "" { + continue + } + delete(t.symbolsMap, k) + } +} + +// desymbolizeLabels decodes label references, with given symbols to labels. +func desymbolizeLabels(b *labels.ScratchBuilder, labelRefs []uint32, symbols []string) labels.Labels { + b.Reset() + for i := 0; i < len(labelRefs); i += 2 { + b.Add(symbols[labelRefs[i]], symbols[labelRefs[i+1]]) + } + b.Sort() + return b.Labels() +} diff --git a/prompb/io/prometheus/write/v2/symbols_test.go b/prompb/io/prometheus/write/v2/symbols_test.go new file mode 100644 index 0000000000..3d852e88f1 --- /dev/null +++ b/prompb/io/prometheus/write/v2/symbols_test.go @@ -0,0 +1,60 @@ +// Copyright 2024 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package writev2 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/prometheus/prometheus/model/labels" +) + +func TestSymbolsTable(t *testing.T) { + s := NewSymbolTable() + require.Equal(t, []string{""}, s.Symbols(), "required empty reference does not exist") + require.Equal(t, uint32(0), s.Symbolize("")) + require.Equal(t, []string{""}, s.Symbols()) + + require.Equal(t, uint32(1), s.Symbolize("abc")) + require.Equal(t, []string{"", "abc"}, s.Symbols()) + + require.Equal(t, uint32(2), s.Symbolize("__name__")) + require.Equal(t, []string{"", "abc", "__name__"}, s.Symbols()) + + require.Equal(t, uint32(3), s.Symbolize("foo")) + require.Equal(t, []string{"", "abc", "__name__", "foo"}, s.Symbols()) + + s.Reset() + require.Equal(t, []string{""}, s.Symbols(), "required empty reference does not exist") + require.Equal(t, uint32(0), s.Symbolize("")) + + require.Equal(t, uint32(1), s.Symbolize("__name__")) + require.Equal(t, []string{"", "__name__"}, s.Symbols()) + + require.Equal(t, uint32(2), s.Symbolize("abc")) + require.Equal(t, []string{"", "__name__", "abc"}, s.Symbols()) + + ls := labels.FromStrings("__name__", "qwer", "zxcv", "1234") + encoded := s.SymbolizeLabels(ls, nil) + require.Equal(t, []uint32{1, 3, 4, 5}, encoded) + b := labels.NewScratchBuilder(len(encoded)) + decoded := desymbolizeLabels(&b, encoded, s.Symbols()) + require.Equal(t, ls, decoded) + + // Different buf. + ls = labels.FromStrings("__name__", "qwer", "zxcv2222", "1234") + encoded = s.SymbolizeLabels(ls, []uint32{1, 3, 4, 5}) + require.Equal(t, []uint32{1, 3, 6, 5}, encoded) +} diff --git a/prompb/io/prometheus/write/v2/types.pb.go b/prompb/io/prometheus/write/v2/types.pb.go new file mode 100644 index 0000000000..d6ea8398f7 --- /dev/null +++ b/prompb/io/prometheus/write/v2/types.pb.go @@ -0,0 +1,3241 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: io/prometheus/write/v2/types.proto + +package writev2 + +import ( + encoding_binary "encoding/binary" + fmt "fmt" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type Metadata_MetricType int32 + +const ( + Metadata_METRIC_TYPE_UNSPECIFIED Metadata_MetricType = 0 + Metadata_METRIC_TYPE_COUNTER Metadata_MetricType = 1 + Metadata_METRIC_TYPE_GAUGE Metadata_MetricType = 2 + Metadata_METRIC_TYPE_HISTOGRAM Metadata_MetricType = 3 + Metadata_METRIC_TYPE_GAUGEHISTOGRAM Metadata_MetricType = 4 + Metadata_METRIC_TYPE_SUMMARY Metadata_MetricType = 5 + Metadata_METRIC_TYPE_INFO Metadata_MetricType = 6 + Metadata_METRIC_TYPE_STATESET Metadata_MetricType = 7 +) + +var Metadata_MetricType_name = map[int32]string{ + 0: "METRIC_TYPE_UNSPECIFIED", + 1: "METRIC_TYPE_COUNTER", + 2: "METRIC_TYPE_GAUGE", + 3: "METRIC_TYPE_HISTOGRAM", + 4: "METRIC_TYPE_GAUGEHISTOGRAM", + 5: "METRIC_TYPE_SUMMARY", + 6: "METRIC_TYPE_INFO", + 7: "METRIC_TYPE_STATESET", +} + +var Metadata_MetricType_value = map[string]int32{ + "METRIC_TYPE_UNSPECIFIED": 0, + "METRIC_TYPE_COUNTER": 1, + "METRIC_TYPE_GAUGE": 2, + "METRIC_TYPE_HISTOGRAM": 3, + "METRIC_TYPE_GAUGEHISTOGRAM": 4, + "METRIC_TYPE_SUMMARY": 5, + "METRIC_TYPE_INFO": 6, + "METRIC_TYPE_STATESET": 7, +} + +func (x Metadata_MetricType) String() string { + return proto.EnumName(Metadata_MetricType_name, int32(x)) +} + +func (Metadata_MetricType) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_f139519efd9fa8d7, []int{4, 0} +} + +type Histogram_ResetHint int32 + +const ( + Histogram_RESET_HINT_UNSPECIFIED Histogram_ResetHint = 0 + Histogram_RESET_HINT_YES Histogram_ResetHint = 1 + Histogram_RESET_HINT_NO Histogram_ResetHint = 2 + Histogram_RESET_HINT_GAUGE Histogram_ResetHint = 3 +) + +var Histogram_ResetHint_name = map[int32]string{ + 0: "RESET_HINT_UNSPECIFIED", + 1: "RESET_HINT_YES", + 2: "RESET_HINT_NO", + 3: "RESET_HINT_GAUGE", +} + +var Histogram_ResetHint_value = map[string]int32{ + "RESET_HINT_UNSPECIFIED": 0, + "RESET_HINT_YES": 1, + "RESET_HINT_NO": 2, + "RESET_HINT_GAUGE": 3, +} + +func (x Histogram_ResetHint) String() string { + return proto.EnumName(Histogram_ResetHint_name, int32(x)) +} + +func (Histogram_ResetHint) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_f139519efd9fa8d7, []int{5, 0} +} + +// Request represents a request to write the given timeseries to a remote destination. +// This message was introduced in the Remote Write 2.0 specification: +// https://prometheus.io/docs/concepts/remote_write_spec_2_0/ +// +// The canonical Content-Type request header value for this message is +// "application/x-protobuf;proto=io.prometheus.write.v2.Request" +// +// NOTE: gogoproto options might change in future for this file, they +// are not part of the spec proto (they only modify the generated Go code, not +// the serialized message). See: https://github.com/prometheus/prometheus/issues/11908 +type Request struct { + // symbols contains a de-duplicated array of string elements used for various + // items in a Request message, like labels and metadata items. For the sender's convenience + // around empty values for optional fields like unit_ref, symbols array MUST start with + // empty string. + // + // To decode each of the symbolized strings, referenced, by "ref(s)" suffix, you + // need to lookup the actual string by index from symbols array. The order of + // strings is up to the sender. The receiver should not assume any particular encoding. + Symbols []string `protobuf:"bytes,4,rep,name=symbols,proto3" json:"symbols,omitempty"` + // timeseries represents an array of distinct series with 0 or more samples. + Timeseries []TimeSeries `protobuf:"bytes,5,rep,name=timeseries,proto3" json:"timeseries"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Request) Reset() { *m = Request{} } +func (m *Request) String() string { return proto.CompactTextString(m) } +func (*Request) ProtoMessage() {} +func (*Request) Descriptor() ([]byte, []int) { + return fileDescriptor_f139519efd9fa8d7, []int{0} +} +func (m *Request) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Request.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Request) XXX_Merge(src proto.Message) { + xxx_messageInfo_Request.Merge(m, src) +} +func (m *Request) XXX_Size() int { + return m.Size() +} +func (m *Request) XXX_DiscardUnknown() { + xxx_messageInfo_Request.DiscardUnknown(m) +} + +var xxx_messageInfo_Request proto.InternalMessageInfo + +func (m *Request) GetSymbols() []string { + if m != nil { + return m.Symbols + } + return nil +} + +func (m *Request) GetTimeseries() []TimeSeries { + if m != nil { + return m.Timeseries + } + return nil +} + +// TimeSeries represents a single series. +type TimeSeries struct { + // labels_refs is a list of label name-value pair references, encoded + // as indices to the Request.symbols array. This list's length is always + // a multiple of two, and the underlying labels should be sorted lexicographically. + // + // Note that there might be multiple TimeSeries objects in the same + // Requests with the same labels e.g. for different exemplars, metadata + // or created timestamp. + LabelsRefs []uint32 `protobuf:"varint,1,rep,packed,name=labels_refs,json=labelsRefs,proto3" json:"labels_refs,omitempty"` + // Timeseries messages can either specify samples or (native) histogram samples + // (histogram field), but not both. For a typical sender (real-time metric + // streaming), in healthy cases, there will be only one sample or histogram. + // + // Samples and histograms are sorted by timestamp (older first). + Samples []Sample `protobuf:"bytes,2,rep,name=samples,proto3" json:"samples"` + Histograms []Histogram `protobuf:"bytes,3,rep,name=histograms,proto3" json:"histograms"` + // exemplars represents an optional set of exemplars attached to this series' samples. + Exemplars []Exemplar `protobuf:"bytes,4,rep,name=exemplars,proto3" json:"exemplars"` + // metadata represents the metadata associated with the given series' samples. + Metadata Metadata `protobuf:"bytes,5,opt,name=metadata,proto3" json:"metadata"` + // created_timestamp represents an optional created timestamp associated with + // this series' samples in ms format, typically for counter or histogram type + // metrics. Created timestamp represents the time when the counter started + // counting (sometimes referred to as start timestamp), which can increase + // the accuracy of query results. + // + // Note that some receivers might require this and in return fail to + // ingest such samples within the Request. + // + // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go + // for conversion from/to time.Time to Prometheus timestamp. + // + // Note that the "optional" keyword is omitted due to + // https://cloud.google.com/apis/design/design_patterns.md#optional_primitive_fields + // Zero value means value not set. If you need to use exactly zero value for + // the timestamp, use 1 millisecond before or after. + CreatedTimestamp int64 `protobuf:"varint,6,opt,name=created_timestamp,json=createdTimestamp,proto3" json:"created_timestamp,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *TimeSeries) Reset() { *m = TimeSeries{} } +func (m *TimeSeries) String() string { return proto.CompactTextString(m) } +func (*TimeSeries) ProtoMessage() {} +func (*TimeSeries) Descriptor() ([]byte, []int) { + return fileDescriptor_f139519efd9fa8d7, []int{1} +} +func (m *TimeSeries) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *TimeSeries) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_TimeSeries.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *TimeSeries) XXX_Merge(src proto.Message) { + xxx_messageInfo_TimeSeries.Merge(m, src) +} +func (m *TimeSeries) XXX_Size() int { + return m.Size() +} +func (m *TimeSeries) XXX_DiscardUnknown() { + xxx_messageInfo_TimeSeries.DiscardUnknown(m) +} + +var xxx_messageInfo_TimeSeries proto.InternalMessageInfo + +func (m *TimeSeries) GetLabelsRefs() []uint32 { + if m != nil { + return m.LabelsRefs + } + return nil +} + +func (m *TimeSeries) GetSamples() []Sample { + if m != nil { + return m.Samples + } + return nil +} + +func (m *TimeSeries) GetHistograms() []Histogram { + if m != nil { + return m.Histograms + } + return nil +} + +func (m *TimeSeries) GetExemplars() []Exemplar { + if m != nil { + return m.Exemplars + } + return nil +} + +func (m *TimeSeries) GetMetadata() Metadata { + if m != nil { + return m.Metadata + } + return Metadata{} +} + +func (m *TimeSeries) GetCreatedTimestamp() int64 { + if m != nil { + return m.CreatedTimestamp + } + return 0 +} + +// Exemplar is an additional information attached to some series' samples. +// It is typically used to attach an example trace or request ID associated with +// the metric changes. +type Exemplar struct { + // labels_refs is an optional list of label name-value pair references, encoded + // as indices to the Request.symbols array. This list's len is always + // a multiple of 2, and the underlying labels should be sorted lexicographically. + // If the exemplar references a trace it should use the `trace_id` label name, as a best practice. + LabelsRefs []uint32 `protobuf:"varint,1,rep,packed,name=labels_refs,json=labelsRefs,proto3" json:"labels_refs,omitempty"` + // value represents an exact example value. This can be useful when the exemplar + // is attached to a histogram, which only gives an estimated value through buckets. + Value float64 `protobuf:"fixed64,2,opt,name=value,proto3" json:"value,omitempty"` + // timestamp represents an optional timestamp of the sample in ms. + // + // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go + // for conversion from/to time.Time to Prometheus timestamp. + // + // Note that the "optional" keyword is omitted due to + // https://cloud.google.com/apis/design/design_patterns.md#optional_primitive_fields + // Zero value means value not set. If you need to use exactly zero value for + // the timestamp, use 1 millisecond before or after. + Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Exemplar) Reset() { *m = Exemplar{} } +func (m *Exemplar) String() string { return proto.CompactTextString(m) } +func (*Exemplar) ProtoMessage() {} +func (*Exemplar) Descriptor() ([]byte, []int) { + return fileDescriptor_f139519efd9fa8d7, []int{2} +} +func (m *Exemplar) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Exemplar) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Exemplar.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Exemplar) XXX_Merge(src proto.Message) { + xxx_messageInfo_Exemplar.Merge(m, src) +} +func (m *Exemplar) XXX_Size() int { + return m.Size() +} +func (m *Exemplar) XXX_DiscardUnknown() { + xxx_messageInfo_Exemplar.DiscardUnknown(m) +} + +var xxx_messageInfo_Exemplar proto.InternalMessageInfo + +func (m *Exemplar) GetLabelsRefs() []uint32 { + if m != nil { + return m.LabelsRefs + } + return nil +} + +func (m *Exemplar) GetValue() float64 { + if m != nil { + return m.Value + } + return 0 +} + +func (m *Exemplar) GetTimestamp() int64 { + if m != nil { + return m.Timestamp + } + return 0 +} + +// Sample represents series sample. +type Sample struct { + // value of the sample. + Value float64 `protobuf:"fixed64,1,opt,name=value,proto3" json:"value,omitempty"` + // timestamp represents timestamp of the sample in ms. + // + // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go + // for conversion from/to time.Time to Prometheus timestamp. + Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Sample) Reset() { *m = Sample{} } +func (m *Sample) String() string { return proto.CompactTextString(m) } +func (*Sample) ProtoMessage() {} +func (*Sample) Descriptor() ([]byte, []int) { + return fileDescriptor_f139519efd9fa8d7, []int{3} +} +func (m *Sample) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Sample) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Sample.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Sample) XXX_Merge(src proto.Message) { + xxx_messageInfo_Sample.Merge(m, src) +} +func (m *Sample) XXX_Size() int { + return m.Size() +} +func (m *Sample) XXX_DiscardUnknown() { + xxx_messageInfo_Sample.DiscardUnknown(m) +} + +var xxx_messageInfo_Sample proto.InternalMessageInfo + +func (m *Sample) GetValue() float64 { + if m != nil { + return m.Value + } + return 0 +} + +func (m *Sample) GetTimestamp() int64 { + if m != nil { + return m.Timestamp + } + return 0 +} + +// Metadata represents the metadata associated with the given series' samples. +type Metadata struct { + Type Metadata_MetricType `protobuf:"varint,1,opt,name=type,proto3,enum=io.prometheus.write.v2.Metadata_MetricType" json:"type,omitempty"` + // help_ref is a reference to the Request.symbols array representing help + // text for the metric. Help is optional, reference should point to an empty string in + // such a case. + HelpRef uint32 `protobuf:"varint,3,opt,name=help_ref,json=helpRef,proto3" json:"help_ref,omitempty"` + // unit_ref is a reference to the Request.symbols array representing a unit + // for the metric. Unit is optional, reference should point to an empty string in + // such a case. + UnitRef uint32 `protobuf:"varint,4,opt,name=unit_ref,json=unitRef,proto3" json:"unit_ref,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Metadata) Reset() { *m = Metadata{} } +func (m *Metadata) String() string { return proto.CompactTextString(m) } +func (*Metadata) ProtoMessage() {} +func (*Metadata) Descriptor() ([]byte, []int) { + return fileDescriptor_f139519efd9fa8d7, []int{4} +} +func (m *Metadata) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Metadata) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Metadata.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Metadata) XXX_Merge(src proto.Message) { + xxx_messageInfo_Metadata.Merge(m, src) +} +func (m *Metadata) XXX_Size() int { + return m.Size() +} +func (m *Metadata) XXX_DiscardUnknown() { + xxx_messageInfo_Metadata.DiscardUnknown(m) +} + +var xxx_messageInfo_Metadata proto.InternalMessageInfo + +func (m *Metadata) GetType() Metadata_MetricType { + if m != nil { + return m.Type + } + return Metadata_METRIC_TYPE_UNSPECIFIED +} + +func (m *Metadata) GetHelpRef() uint32 { + if m != nil { + return m.HelpRef + } + return 0 +} + +func (m *Metadata) GetUnitRef() uint32 { + if m != nil { + return m.UnitRef + } + return 0 +} + +// A native histogram, also known as a sparse histogram. +// Original design doc: +// https://docs.google.com/document/d/1cLNv3aufPZb3fNfaJgdaRBZsInZKKIHo9E6HinJVbpM/edit +// The appendix of this design doc also explains the concept of float +// histograms. This Histogram message can represent both, the usual +// integer histogram as well as a float histogram. +type Histogram struct { + // Types that are valid to be assigned to Count: + // + // *Histogram_CountInt + // *Histogram_CountFloat + Count isHistogram_Count `protobuf_oneof:"count"` + Sum float64 `protobuf:"fixed64,3,opt,name=sum,proto3" json:"sum,omitempty"` + // The schema defines the bucket schema. Currently, valid numbers + // are -53 and numbers in range of -4 <= n <= 8. More valid numbers might be + // added in future for new bucketing layouts. + // + // The schema equal to -53 means custom buckets. See + // custom_values field description for more details. + // + // Values between -4 and 8 represent base-2 bucket schema, where 1 + // is a bucket boundary in each case, and then each power of two is + // divided into 2^n (n is schema value) logarithmic buckets. Or in other words, + // each bucket boundary is the previous boundary times 2^(2^-n). + Schema int32 `protobuf:"zigzag32,4,opt,name=schema,proto3" json:"schema,omitempty"` + ZeroThreshold float64 `protobuf:"fixed64,5,opt,name=zero_threshold,json=zeroThreshold,proto3" json:"zero_threshold,omitempty"` + // Types that are valid to be assigned to ZeroCount: + // + // *Histogram_ZeroCountInt + // *Histogram_ZeroCountFloat + ZeroCount isHistogram_ZeroCount `protobuf_oneof:"zero_count"` + // Negative Buckets. + NegativeSpans []BucketSpan `protobuf:"bytes,8,rep,name=negative_spans,json=negativeSpans,proto3" json:"negative_spans"` + // Use either "negative_deltas" or "negative_counts", the former for + // regular histograms with integer counts, the latter for + // float histograms. + NegativeDeltas []int64 `protobuf:"zigzag64,9,rep,packed,name=negative_deltas,json=negativeDeltas,proto3" json:"negative_deltas,omitempty"` + NegativeCounts []float64 `protobuf:"fixed64,10,rep,packed,name=negative_counts,json=negativeCounts,proto3" json:"negative_counts,omitempty"` + // Positive Buckets. + // + // In case of custom buckets (-53 schema value) the positive buckets are interpreted as follows: + // * The span offset+length points to an the index of the custom_values array + // or +Inf if pointing to the len of the array. + // * The counts and deltas have the same meaning as for exponential histograms. + PositiveSpans []BucketSpan `protobuf:"bytes,11,rep,name=positive_spans,json=positiveSpans,proto3" json:"positive_spans"` + // Use either "positive_deltas" or "positive_counts", the former for + // regular histograms with integer counts, the latter for + // float histograms. + PositiveDeltas []int64 `protobuf:"zigzag64,12,rep,packed,name=positive_deltas,json=positiveDeltas,proto3" json:"positive_deltas,omitempty"` + PositiveCounts []float64 `protobuf:"fixed64,13,rep,packed,name=positive_counts,json=positiveCounts,proto3" json:"positive_counts,omitempty"` + ResetHint Histogram_ResetHint `protobuf:"varint,14,opt,name=reset_hint,json=resetHint,proto3,enum=io.prometheus.write.v2.Histogram_ResetHint" json:"reset_hint,omitempty"` + // timestamp represents timestamp of the sample in ms. + // + // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go + // for conversion from/to time.Time to Prometheus timestamp. + Timestamp int64 `protobuf:"varint,15,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // custom_values is an additional field used by non-exponential bucketing layouts. + // + // For custom buckets (-53 schema value) custom_values specify monotonically + // increasing upper inclusive boundaries for the bucket counts with arbitrary + // widths for this histogram. In other words, custom_values represents custom, + // explicit bucketing that could have been converted from the classic histograms. + // + // Those bounds are then referenced by spans in positive_spans with corresponding positive + // counts of deltas (refer to positive_spans for more details). This way we can + // have encode sparse histograms with custom bucketing (many buckets are often + // not used). + // + // Note that for custom bounds, even negative observations are placed in the positive + // counts to simplify the implementation and avoid ambiguity of where to place + // an underflow bucket, e.g. (-2, 1]. Therefore negative buckets and + // the zero bucket are unused, if the schema indicates custom bucketing. + // + // For each upper boundary the previous boundary represent the lower exclusive + // boundary for that bucket. The first element is the upper inclusive boundary + // for the first bucket, which implicitly has a lower inclusive bound of -Inf. + // This is similar to "le" label semantics on classic histograms. You may add a + // bucket with an upper bound of 0 to make sure that you really have no negative + // observations, but in practice, native histogram rendering will show both with + // or without first upper boundary 0 and no negative counts as the same case. + // + // The last element is not only the upper inclusive bound of the last regular + // bucket, but implicitly the lower exclusive bound of the +Inf bucket. + CustomValues []float64 `protobuf:"fixed64,16,rep,packed,name=custom_values,json=customValues,proto3" json:"custom_values,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Histogram) Reset() { *m = Histogram{} } +func (m *Histogram) String() string { return proto.CompactTextString(m) } +func (*Histogram) ProtoMessage() {} +func (*Histogram) Descriptor() ([]byte, []int) { + return fileDescriptor_f139519efd9fa8d7, []int{5} +} +func (m *Histogram) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Histogram) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Histogram.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Histogram) XXX_Merge(src proto.Message) { + xxx_messageInfo_Histogram.Merge(m, src) +} +func (m *Histogram) XXX_Size() int { + return m.Size() +} +func (m *Histogram) XXX_DiscardUnknown() { + xxx_messageInfo_Histogram.DiscardUnknown(m) +} + +var xxx_messageInfo_Histogram proto.InternalMessageInfo + +type isHistogram_Count interface { + isHistogram_Count() + MarshalTo([]byte) (int, error) + Size() int +} +type isHistogram_ZeroCount interface { + isHistogram_ZeroCount() + MarshalTo([]byte) (int, error) + Size() int +} + +type Histogram_CountInt struct { + CountInt uint64 `protobuf:"varint,1,opt,name=count_int,json=countInt,proto3,oneof" json:"count_int,omitempty"` +} +type Histogram_CountFloat struct { + CountFloat float64 `protobuf:"fixed64,2,opt,name=count_float,json=countFloat,proto3,oneof" json:"count_float,omitempty"` +} +type Histogram_ZeroCountInt struct { + ZeroCountInt uint64 `protobuf:"varint,6,opt,name=zero_count_int,json=zeroCountInt,proto3,oneof" json:"zero_count_int,omitempty"` +} +type Histogram_ZeroCountFloat struct { + ZeroCountFloat float64 `protobuf:"fixed64,7,opt,name=zero_count_float,json=zeroCountFloat,proto3,oneof" json:"zero_count_float,omitempty"` +} + +func (*Histogram_CountInt) isHistogram_Count() {} +func (*Histogram_CountFloat) isHistogram_Count() {} +func (*Histogram_ZeroCountInt) isHistogram_ZeroCount() {} +func (*Histogram_ZeroCountFloat) isHistogram_ZeroCount() {} + +func (m *Histogram) GetCount() isHistogram_Count { + if m != nil { + return m.Count + } + return nil +} +func (m *Histogram) GetZeroCount() isHistogram_ZeroCount { + if m != nil { + return m.ZeroCount + } + return nil +} + +func (m *Histogram) GetCountInt() uint64 { + if x, ok := m.GetCount().(*Histogram_CountInt); ok { + return x.CountInt + } + return 0 +} + +func (m *Histogram) GetCountFloat() float64 { + if x, ok := m.GetCount().(*Histogram_CountFloat); ok { + return x.CountFloat + } + return 0 +} + +func (m *Histogram) GetSum() float64 { + if m != nil { + return m.Sum + } + return 0 +} + +func (m *Histogram) GetSchema() int32 { + if m != nil { + return m.Schema + } + return 0 +} + +func (m *Histogram) GetZeroThreshold() float64 { + if m != nil { + return m.ZeroThreshold + } + return 0 +} + +func (m *Histogram) GetZeroCountInt() uint64 { + if x, ok := m.GetZeroCount().(*Histogram_ZeroCountInt); ok { + return x.ZeroCountInt + } + return 0 +} + +func (m *Histogram) GetZeroCountFloat() float64 { + if x, ok := m.GetZeroCount().(*Histogram_ZeroCountFloat); ok { + return x.ZeroCountFloat + } + return 0 +} + +func (m *Histogram) GetNegativeSpans() []BucketSpan { + if m != nil { + return m.NegativeSpans + } + return nil +} + +func (m *Histogram) GetNegativeDeltas() []int64 { + if m != nil { + return m.NegativeDeltas + } + return nil +} + +func (m *Histogram) GetNegativeCounts() []float64 { + if m != nil { + return m.NegativeCounts + } + return nil +} + +func (m *Histogram) GetPositiveSpans() []BucketSpan { + if m != nil { + return m.PositiveSpans + } + return nil +} + +func (m *Histogram) GetPositiveDeltas() []int64 { + if m != nil { + return m.PositiveDeltas + } + return nil +} + +func (m *Histogram) GetPositiveCounts() []float64 { + if m != nil { + return m.PositiveCounts + } + return nil +} + +func (m *Histogram) GetResetHint() Histogram_ResetHint { + if m != nil { + return m.ResetHint + } + return Histogram_RESET_HINT_UNSPECIFIED +} + +func (m *Histogram) GetTimestamp() int64 { + if m != nil { + return m.Timestamp + } + return 0 +} + +func (m *Histogram) GetCustomValues() []float64 { + if m != nil { + return m.CustomValues + } + return nil +} + +// XXX_OneofWrappers is for the internal use of the proto package. +func (*Histogram) XXX_OneofWrappers() []interface{} { + return []interface{}{ + (*Histogram_CountInt)(nil), + (*Histogram_CountFloat)(nil), + (*Histogram_ZeroCountInt)(nil), + (*Histogram_ZeroCountFloat)(nil), + } +} + +// A BucketSpan defines a number of consecutive buckets with their +// offset. Logically, it would be more straightforward to include the +// bucket counts in the Span. However, the protobuf representation is +// more compact in the way the data is structured here (with all the +// buckets in a single array separate from the Spans). +type BucketSpan struct { + Offset int32 `protobuf:"zigzag32,1,opt,name=offset,proto3" json:"offset,omitempty"` + Length uint32 `protobuf:"varint,2,opt,name=length,proto3" json:"length,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *BucketSpan) Reset() { *m = BucketSpan{} } +func (m *BucketSpan) String() string { return proto.CompactTextString(m) } +func (*BucketSpan) ProtoMessage() {} +func (*BucketSpan) Descriptor() ([]byte, []int) { + return fileDescriptor_f139519efd9fa8d7, []int{6} +} +func (m *BucketSpan) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *BucketSpan) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_BucketSpan.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *BucketSpan) XXX_Merge(src proto.Message) { + xxx_messageInfo_BucketSpan.Merge(m, src) +} +func (m *BucketSpan) XXX_Size() int { + return m.Size() +} +func (m *BucketSpan) XXX_DiscardUnknown() { + xxx_messageInfo_BucketSpan.DiscardUnknown(m) +} + +var xxx_messageInfo_BucketSpan proto.InternalMessageInfo + +func (m *BucketSpan) GetOffset() int32 { + if m != nil { + return m.Offset + } + return 0 +} + +func (m *BucketSpan) GetLength() uint32 { + if m != nil { + return m.Length + } + return 0 +} + +func init() { + proto.RegisterEnum("io.prometheus.write.v2.Metadata_MetricType", Metadata_MetricType_name, Metadata_MetricType_value) + proto.RegisterEnum("io.prometheus.write.v2.Histogram_ResetHint", Histogram_ResetHint_name, Histogram_ResetHint_value) + proto.RegisterType((*Request)(nil), "io.prometheus.write.v2.Request") + proto.RegisterType((*TimeSeries)(nil), "io.prometheus.write.v2.TimeSeries") + proto.RegisterType((*Exemplar)(nil), "io.prometheus.write.v2.Exemplar") + proto.RegisterType((*Sample)(nil), "io.prometheus.write.v2.Sample") + proto.RegisterType((*Metadata)(nil), "io.prometheus.write.v2.Metadata") + proto.RegisterType((*Histogram)(nil), "io.prometheus.write.v2.Histogram") + proto.RegisterType((*BucketSpan)(nil), "io.prometheus.write.v2.BucketSpan") +} + +func init() { + proto.RegisterFile("io/prometheus/write/v2/types.proto", fileDescriptor_f139519efd9fa8d7) +} + +var fileDescriptor_f139519efd9fa8d7 = []byte{ + // 926 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x55, 0x5d, 0x6f, 0xe3, 0x44, + 0x14, 0xed, 0xc4, 0x69, 0x3e, 0x6e, 0x9a, 0xac, 0x33, 0xb4, 0x5d, 0x6f, 0x81, 0x6c, 0xd6, 0x08, + 0x88, 0x58, 0x29, 0x91, 0xc2, 0xeb, 0x0a, 0xd4, 0xb4, 0x6e, 0x93, 0x95, 0x92, 0xac, 0x26, 0x2e, + 0x52, 0x79, 0xb1, 0xdc, 0x64, 0x92, 0x58, 0xd8, 0xb1, 0xf1, 0x4c, 0x02, 0xe5, 0xf7, 0xf1, 0xb0, + 0x8f, 0xfc, 0x01, 0x10, 0xf4, 0x9d, 0xff, 0x80, 0x66, 0xfc, 0xd9, 0x42, 0xbb, 0xe2, 0x6d, 0xe6, + 0xdc, 0x73, 0xee, 0x3d, 0xb9, 0xbe, 0x77, 0x02, 0xba, 0xe3, 0xf7, 0x82, 0xd0, 0xf7, 0x28, 0x5f, + 0xd3, 0x2d, 0xeb, 0xfd, 0x14, 0x3a, 0x9c, 0xf6, 0x76, 0xfd, 0x1e, 0xbf, 0x0d, 0x28, 0xeb, 0x06, + 0xa1, 0xcf, 0x7d, 0x7c, 0xec, 0xf8, 0xdd, 0x8c, 0xd3, 0x95, 0x9c, 0xee, 0xae, 0x7f, 0x72, 0xb8, + 0xf2, 0x57, 0xbe, 0xa4, 0xf4, 0xc4, 0x29, 0x62, 0xeb, 0x0c, 0xca, 0x84, 0xfe, 0xb8, 0xa5, 0x8c, + 0x63, 0x0d, 0xca, 0xec, 0xd6, 0xbb, 0xf1, 0x5d, 0xa6, 0x15, 0xdb, 0x4a, 0xa7, 0x4a, 0x92, 0x2b, + 0x1e, 0x02, 0x70, 0xc7, 0xa3, 0x8c, 0x86, 0x0e, 0x65, 0xda, 0x7e, 0x5b, 0xe9, 0xd4, 0xfa, 0x7a, + 0xf7, 0xbf, 0xeb, 0x74, 0x4d, 0xc7, 0xa3, 0x33, 0xc9, 0x1c, 0x14, 0xdf, 0xff, 0xf1, 0x72, 0x8f, + 0xe4, 0xb4, 0x6f, 0x8b, 0x15, 0xa4, 0x16, 0xf5, 0xbf, 0x0b, 0x00, 0x19, 0x0d, 0xbf, 0x84, 0x9a, + 0x6b, 0xdf, 0x50, 0x97, 0x59, 0x21, 0x5d, 0x32, 0x0d, 0xb5, 0x95, 0x4e, 0x9d, 0x40, 0x04, 0x11, + 0xba, 0x64, 0xf8, 0x1b, 0x28, 0x33, 0xdb, 0x0b, 0x5c, 0xca, 0xb4, 0x82, 0x2c, 0xde, 0x7a, 0xac, + 0xf8, 0x4c, 0xd2, 0xe2, 0xc2, 0x89, 0x08, 0x5f, 0x02, 0xac, 0x1d, 0xc6, 0xfd, 0x55, 0x68, 0x7b, + 0x4c, 0x53, 0x64, 0x8a, 0x57, 0x8f, 0xa5, 0x18, 0x26, 0xcc, 0xc4, 0x7e, 0x26, 0xc5, 0xe7, 0x50, + 0xa5, 0x3f, 0x53, 0x2f, 0x70, 0xed, 0x30, 0x6a, 0x52, 0xad, 0xdf, 0x7e, 0x2c, 0x8f, 0x11, 0x13, + 0xe3, 0x34, 0x99, 0x10, 0x0f, 0xa0, 0xe2, 0x51, 0x6e, 0x2f, 0x6c, 0x6e, 0x6b, 0xfb, 0x6d, 0xf4, + 0x54, 0x92, 0x71, 0xcc, 0x8b, 0x93, 0xa4, 0x3a, 0xfc, 0x1a, 0x9a, 0xf3, 0x90, 0xda, 0x9c, 0x2e, + 0x2c, 0xd9, 0x5e, 0x6e, 0x7b, 0x81, 0x56, 0x6a, 0xa3, 0x8e, 0x42, 0xd4, 0x38, 0x60, 0x26, 0xb8, + 0x6e, 0x41, 0x25, 0x71, 0xf3, 0xe1, 0x66, 0x1f, 0xc2, 0xfe, 0xce, 0x76, 0xb7, 0x54, 0x2b, 0xb4, + 0x51, 0x07, 0x91, 0xe8, 0x82, 0x3f, 0x81, 0x6a, 0x56, 0x47, 0x91, 0x75, 0x32, 0x40, 0x7f, 0x03, + 0xa5, 0xa8, 0xf3, 0x99, 0x1a, 0x3d, 0xaa, 0x2e, 0x3c, 0x54, 0xff, 0x55, 0x80, 0x4a, 0xf2, 0x43, + 0xf1, 0xb7, 0x50, 0x14, 0xd3, 0x2c, 0xf5, 0x8d, 0xfe, 0xeb, 0x0f, 0x35, 0x46, 0x1c, 0x42, 0x67, + 0x6e, 0xde, 0x06, 0x94, 0x48, 0x21, 0x7e, 0x01, 0x95, 0x35, 0x75, 0x03, 0xf1, 0xf3, 0xa4, 0xd1, + 0x3a, 0x29, 0x8b, 0x3b, 0xa1, 0x4b, 0x11, 0xda, 0x6e, 0x1c, 0x2e, 0x43, 0xc5, 0x28, 0x24, 0xee, + 0x84, 0x2e, 0xf5, 0xdf, 0x11, 0x40, 0x96, 0x0a, 0x7f, 0x0c, 0xcf, 0xc7, 0x86, 0x49, 0x46, 0x67, + 0x96, 0x79, 0xfd, 0xce, 0xb0, 0xae, 0x26, 0xb3, 0x77, 0xc6, 0xd9, 0xe8, 0x62, 0x64, 0x9c, 0xab, + 0x7b, 0xf8, 0x39, 0x7c, 0x94, 0x0f, 0x9e, 0x4d, 0xaf, 0x26, 0xa6, 0x41, 0x54, 0x84, 0x8f, 0xa0, + 0x99, 0x0f, 0x5c, 0x9e, 0x5e, 0x5d, 0x1a, 0x6a, 0x01, 0xbf, 0x80, 0xa3, 0x3c, 0x3c, 0x1c, 0xcd, + 0xcc, 0xe9, 0x25, 0x39, 0x1d, 0xab, 0x0a, 0x6e, 0xc1, 0xc9, 0xbf, 0x14, 0x59, 0xbc, 0xf8, 0xb0, + 0xd4, 0xec, 0x6a, 0x3c, 0x3e, 0x25, 0xd7, 0xea, 0x3e, 0x3e, 0x04, 0x35, 0x1f, 0x18, 0x4d, 0x2e, + 0xa6, 0x6a, 0x09, 0x6b, 0x70, 0x78, 0x8f, 0x6e, 0x9e, 0x9a, 0xc6, 0xcc, 0x30, 0xd5, 0xb2, 0xfe, + 0x6b, 0x09, 0xaa, 0xe9, 0x64, 0xe3, 0x4f, 0xa1, 0x3a, 0xf7, 0xb7, 0x1b, 0x6e, 0x39, 0x1b, 0x2e, + 0x3b, 0x5d, 0x1c, 0xee, 0x91, 0x8a, 0x84, 0x46, 0x1b, 0x8e, 0x5f, 0x41, 0x2d, 0x0a, 0x2f, 0x5d, + 0xdf, 0xe6, 0xd1, 0x20, 0x0c, 0xf7, 0x08, 0x48, 0xf0, 0x42, 0x60, 0x58, 0x05, 0x85, 0x6d, 0x3d, + 0xd9, 0x60, 0x44, 0xc4, 0x11, 0x1f, 0x43, 0x89, 0xcd, 0xd7, 0xd4, 0xb3, 0x65, 0x6b, 0x9b, 0x24, + 0xbe, 0xe1, 0xcf, 0xa1, 0xf1, 0x0b, 0x0d, 0x7d, 0x8b, 0xaf, 0x43, 0xca, 0xd6, 0xbe, 0xbb, 0x90, + 0x33, 0x8f, 0x48, 0x5d, 0xa0, 0x66, 0x02, 0xe2, 0x2f, 0x62, 0x5a, 0xe6, 0xab, 0x24, 0x7d, 0x21, + 0x72, 0x20, 0xf0, 0xb3, 0xc4, 0xdb, 0x57, 0xa0, 0xe6, 0x78, 0x91, 0xc1, 0xb2, 0x34, 0x88, 0x48, + 0x23, 0x65, 0x46, 0x26, 0xa7, 0xd0, 0xd8, 0xd0, 0x95, 0xcd, 0x9d, 0x1d, 0xb5, 0x58, 0x60, 0x6f, + 0x98, 0x56, 0x79, 0xfa, 0xed, 0x1a, 0x6c, 0xe7, 0x3f, 0x50, 0x3e, 0x0b, 0xec, 0x4d, 0xbc, 0x70, + 0xf5, 0x44, 0x2f, 0x30, 0x86, 0xbf, 0x84, 0x67, 0x69, 0xc2, 0x05, 0x75, 0xb9, 0xcd, 0xb4, 0x6a, + 0x5b, 0xe9, 0x60, 0x92, 0xd6, 0x39, 0x97, 0xe8, 0x3d, 0xa2, 0x74, 0xca, 0x34, 0x68, 0x2b, 0x1d, + 0x94, 0x11, 0xa5, 0x4d, 0x26, 0x2c, 0x06, 0x3e, 0x73, 0x72, 0x16, 0x6b, 0xff, 0xd7, 0x62, 0xa2, + 0x4f, 0x2d, 0xa6, 0x09, 0x63, 0x8b, 0x07, 0x91, 0xc5, 0x04, 0xce, 0x2c, 0xa6, 0xc4, 0xd8, 0x62, + 0x3d, 0xb2, 0x98, 0xc0, 0xb1, 0xc5, 0xb7, 0x00, 0x21, 0x65, 0x94, 0x5b, 0x6b, 0xf1, 0x55, 0x1a, + 0x4f, 0xef, 0x65, 0x3a, 0x63, 0x5d, 0x22, 0x34, 0x43, 0x67, 0xc3, 0x49, 0x35, 0x4c, 0x8e, 0xf7, + 0x1f, 0x82, 0x67, 0x0f, 0x1e, 0x02, 0xfc, 0x19, 0xd4, 0xe7, 0x5b, 0xc6, 0x7d, 0xcf, 0x92, 0xcf, + 0x06, 0xd3, 0x54, 0x69, 0xe8, 0x20, 0x02, 0xbf, 0x93, 0x98, 0xbe, 0x80, 0x6a, 0x9a, 0x1a, 0x9f, + 0xc0, 0x31, 0x11, 0x13, 0x6e, 0x0d, 0x47, 0x13, 0xf3, 0xc1, 0x9a, 0x62, 0x68, 0xe4, 0x62, 0xd7, + 0xc6, 0x4c, 0x45, 0xb8, 0x09, 0xf5, 0x1c, 0x36, 0x99, 0xaa, 0x05, 0xb1, 0x49, 0x39, 0x28, 0xda, + 0x59, 0x65, 0x50, 0x86, 0x7d, 0xd9, 0x94, 0xc1, 0x01, 0x40, 0x36, 0x6f, 0xfa, 0x1b, 0x80, 0xec, + 0x03, 0x88, 0x91, 0xf7, 0x97, 0x4b, 0x46, 0xa3, 0x1d, 0x6a, 0x92, 0xf8, 0x26, 0x70, 0x97, 0x6e, + 0x56, 0x7c, 0x2d, 0x57, 0xa7, 0x4e, 0xe2, 0xdb, 0xe0, 0xe8, 0xfd, 0x5d, 0x0b, 0xfd, 0x76, 0xd7, + 0x42, 0x7f, 0xde, 0xb5, 0xd0, 0xf7, 0x65, 0xd9, 0xb4, 0x5d, 0xff, 0xa6, 0x24, 0xff, 0x8a, 0xbf, + 0xfe, 0x27, 0x00, 0x00, 0xff, 0xff, 0x3e, 0xfc, 0x93, 0x1c, 0xde, 0x07, 0x00, 0x00, +} + +func (m *Request) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Request) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Request) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if len(m.Timeseries) > 0 { + for iNdEx := len(m.Timeseries) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Timeseries[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x2a + } + } + if len(m.Symbols) > 0 { + for iNdEx := len(m.Symbols) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.Symbols[iNdEx]) + copy(dAtA[i:], m.Symbols[iNdEx]) + i = encodeVarintTypes(dAtA, i, uint64(len(m.Symbols[iNdEx]))) + i-- + dAtA[i] = 0x22 + } + } + return len(dAtA) - i, nil +} + +func (m *TimeSeries) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TimeSeries) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *TimeSeries) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.CreatedTimestamp != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.CreatedTimestamp)) + i-- + dAtA[i] = 0x30 + } + { + size, err := m.Metadata.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x2a + if len(m.Exemplars) > 0 { + for iNdEx := len(m.Exemplars) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Exemplars[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + } + if len(m.Histograms) > 0 { + for iNdEx := len(m.Histograms) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Histograms[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.Samples) > 0 { + for iNdEx := len(m.Samples) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Samples[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + if len(m.LabelsRefs) > 0 { + dAtA3 := make([]byte, len(m.LabelsRefs)*10) + var j2 int + for _, num := range m.LabelsRefs { + for num >= 1<<7 { + dAtA3[j2] = uint8(uint64(num)&0x7f | 0x80) + num >>= 7 + j2++ + } + dAtA3[j2] = uint8(num) + j2++ + } + i -= j2 + copy(dAtA[i:], dAtA3[:j2]) + i = encodeVarintTypes(dAtA, i, uint64(j2)) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *Exemplar) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Exemplar) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Exemplar) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.Timestamp != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.Timestamp)) + i-- + dAtA[i] = 0x18 + } + if m.Value != 0 { + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.Value)))) + i-- + dAtA[i] = 0x11 + } + if len(m.LabelsRefs) > 0 { + dAtA5 := make([]byte, len(m.LabelsRefs)*10) + var j4 int + for _, num := range m.LabelsRefs { + for num >= 1<<7 { + dAtA5[j4] = uint8(uint64(num)&0x7f | 0x80) + num >>= 7 + j4++ + } + dAtA5[j4] = uint8(num) + j4++ + } + i -= j4 + copy(dAtA[i:], dAtA5[:j4]) + i = encodeVarintTypes(dAtA, i, uint64(j4)) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *Sample) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Sample) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Sample) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.Timestamp != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.Timestamp)) + i-- + dAtA[i] = 0x10 + } + if m.Value != 0 { + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.Value)))) + i-- + dAtA[i] = 0x9 + } + return len(dAtA) - i, nil +} + +func (m *Metadata) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Metadata) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Metadata) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.UnitRef != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.UnitRef)) + i-- + dAtA[i] = 0x20 + } + if m.HelpRef != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.HelpRef)) + i-- + dAtA[i] = 0x18 + } + if m.Type != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.Type)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *Histogram) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Histogram) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Histogram) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if len(m.CustomValues) > 0 { + for iNdEx := len(m.CustomValues) - 1; iNdEx >= 0; iNdEx-- { + f6 := math.Float64bits(float64(m.CustomValues[iNdEx])) + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(f6)) + } + i = encodeVarintTypes(dAtA, i, uint64(len(m.CustomValues)*8)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x82 + } + if m.Timestamp != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.Timestamp)) + i-- + dAtA[i] = 0x78 + } + if m.ResetHint != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.ResetHint)) + i-- + dAtA[i] = 0x70 + } + if len(m.PositiveCounts) > 0 { + for iNdEx := len(m.PositiveCounts) - 1; iNdEx >= 0; iNdEx-- { + f7 := math.Float64bits(float64(m.PositiveCounts[iNdEx])) + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(f7)) + } + i = encodeVarintTypes(dAtA, i, uint64(len(m.PositiveCounts)*8)) + i-- + dAtA[i] = 0x6a + } + if len(m.PositiveDeltas) > 0 { + var j8 int + dAtA10 := make([]byte, len(m.PositiveDeltas)*10) + for _, num := range m.PositiveDeltas { + x9 := (uint64(num) << 1) ^ uint64((num >> 63)) + for x9 >= 1<<7 { + dAtA10[j8] = uint8(uint64(x9)&0x7f | 0x80) + j8++ + x9 >>= 7 + } + dAtA10[j8] = uint8(x9) + j8++ + } + i -= j8 + copy(dAtA[i:], dAtA10[:j8]) + i = encodeVarintTypes(dAtA, i, uint64(j8)) + i-- + dAtA[i] = 0x62 + } + if len(m.PositiveSpans) > 0 { + for iNdEx := len(m.PositiveSpans) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.PositiveSpans[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x5a + } + } + if len(m.NegativeCounts) > 0 { + for iNdEx := len(m.NegativeCounts) - 1; iNdEx >= 0; iNdEx-- { + f11 := math.Float64bits(float64(m.NegativeCounts[iNdEx])) + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(f11)) + } + i = encodeVarintTypes(dAtA, i, uint64(len(m.NegativeCounts)*8)) + i-- + dAtA[i] = 0x52 + } + if len(m.NegativeDeltas) > 0 { + var j12 int + dAtA14 := make([]byte, len(m.NegativeDeltas)*10) + for _, num := range m.NegativeDeltas { + x13 := (uint64(num) << 1) ^ uint64((num >> 63)) + for x13 >= 1<<7 { + dAtA14[j12] = uint8(uint64(x13)&0x7f | 0x80) + j12++ + x13 >>= 7 + } + dAtA14[j12] = uint8(x13) + j12++ + } + i -= j12 + copy(dAtA[i:], dAtA14[:j12]) + i = encodeVarintTypes(dAtA, i, uint64(j12)) + i-- + dAtA[i] = 0x4a + } + if len(m.NegativeSpans) > 0 { + for iNdEx := len(m.NegativeSpans) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.NegativeSpans[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTypes(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x42 + } + } + if m.ZeroCount != nil { + { + size := m.ZeroCount.Size() + i -= size + if _, err := m.ZeroCount.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + } + } + if m.ZeroThreshold != 0 { + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.ZeroThreshold)))) + i-- + dAtA[i] = 0x29 + } + if m.Schema != 0 { + i = encodeVarintTypes(dAtA, i, uint64((uint32(m.Schema)<<1)^uint32((m.Schema>>31)))) + i-- + dAtA[i] = 0x20 + } + if m.Sum != 0 { + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.Sum)))) + i-- + dAtA[i] = 0x19 + } + if m.Count != nil { + { + size := m.Count.Size() + i -= size + if _, err := m.Count.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + } + } + return len(dAtA) - i, nil +} + +func (m *Histogram_CountInt) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Histogram_CountInt) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + i = encodeVarintTypes(dAtA, i, uint64(m.CountInt)) + i-- + dAtA[i] = 0x8 + return len(dAtA) - i, nil +} +func (m *Histogram_CountFloat) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Histogram_CountFloat) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.CountFloat)))) + i-- + dAtA[i] = 0x11 + return len(dAtA) - i, nil +} +func (m *Histogram_ZeroCountInt) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Histogram_ZeroCountInt) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + i = encodeVarintTypes(dAtA, i, uint64(m.ZeroCountInt)) + i-- + dAtA[i] = 0x30 + return len(dAtA) - i, nil +} +func (m *Histogram_ZeroCountFloat) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Histogram_ZeroCountFloat) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.ZeroCountFloat)))) + i-- + dAtA[i] = 0x39 + return len(dAtA) - i, nil +} +func (m *BucketSpan) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *BucketSpan) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *BucketSpan) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.Length != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.Length)) + i-- + dAtA[i] = 0x10 + } + if m.Offset != 0 { + i = encodeVarintTypes(dAtA, i, uint64((uint32(m.Offset)<<1)^uint32((m.Offset>>31)))) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func encodeVarintTypes(dAtA []byte, offset int, v uint64) int { + offset -= sovTypes(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *Request) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Symbols) > 0 { + for _, s := range m.Symbols { + l = len(s) + n += 1 + l + sovTypes(uint64(l)) + } + } + if len(m.Timeseries) > 0 { + for _, e := range m.Timeseries { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *TimeSeries) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.LabelsRefs) > 0 { + l = 0 + for _, e := range m.LabelsRefs { + l += sovTypes(uint64(e)) + } + n += 1 + sovTypes(uint64(l)) + l + } + if len(m.Samples) > 0 { + for _, e := range m.Samples { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } + if len(m.Histograms) > 0 { + for _, e := range m.Histograms { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } + if len(m.Exemplars) > 0 { + for _, e := range m.Exemplars { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } + l = m.Metadata.Size() + n += 1 + l + sovTypes(uint64(l)) + if m.CreatedTimestamp != 0 { + n += 1 + sovTypes(uint64(m.CreatedTimestamp)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *Exemplar) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.LabelsRefs) > 0 { + l = 0 + for _, e := range m.LabelsRefs { + l += sovTypes(uint64(e)) + } + n += 1 + sovTypes(uint64(l)) + l + } + if m.Value != 0 { + n += 9 + } + if m.Timestamp != 0 { + n += 1 + sovTypes(uint64(m.Timestamp)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *Sample) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Value != 0 { + n += 9 + } + if m.Timestamp != 0 { + n += 1 + sovTypes(uint64(m.Timestamp)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *Metadata) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Type != 0 { + n += 1 + sovTypes(uint64(m.Type)) + } + if m.HelpRef != 0 { + n += 1 + sovTypes(uint64(m.HelpRef)) + } + if m.UnitRef != 0 { + n += 1 + sovTypes(uint64(m.UnitRef)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *Histogram) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Count != nil { + n += m.Count.Size() + } + if m.Sum != 0 { + n += 9 + } + if m.Schema != 0 { + n += 1 + sozTypes(uint64(m.Schema)) + } + if m.ZeroThreshold != 0 { + n += 9 + } + if m.ZeroCount != nil { + n += m.ZeroCount.Size() + } + if len(m.NegativeSpans) > 0 { + for _, e := range m.NegativeSpans { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } + if len(m.NegativeDeltas) > 0 { + l = 0 + for _, e := range m.NegativeDeltas { + l += sozTypes(uint64(e)) + } + n += 1 + sovTypes(uint64(l)) + l + } + if len(m.NegativeCounts) > 0 { + n += 1 + sovTypes(uint64(len(m.NegativeCounts)*8)) + len(m.NegativeCounts)*8 + } + if len(m.PositiveSpans) > 0 { + for _, e := range m.PositiveSpans { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } + if len(m.PositiveDeltas) > 0 { + l = 0 + for _, e := range m.PositiveDeltas { + l += sozTypes(uint64(e)) + } + n += 1 + sovTypes(uint64(l)) + l + } + if len(m.PositiveCounts) > 0 { + n += 1 + sovTypes(uint64(len(m.PositiveCounts)*8)) + len(m.PositiveCounts)*8 + } + if m.ResetHint != 0 { + n += 1 + sovTypes(uint64(m.ResetHint)) + } + if m.Timestamp != 0 { + n += 1 + sovTypes(uint64(m.Timestamp)) + } + if len(m.CustomValues) > 0 { + n += 2 + sovTypes(uint64(len(m.CustomValues)*8)) + len(m.CustomValues)*8 + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *Histogram_CountInt) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + n += 1 + sovTypes(uint64(m.CountInt)) + return n +} +func (m *Histogram_CountFloat) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + n += 9 + return n +} +func (m *Histogram_ZeroCountInt) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + n += 1 + sovTypes(uint64(m.ZeroCountInt)) + return n +} +func (m *Histogram_ZeroCountFloat) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + n += 9 + return n +} +func (m *BucketSpan) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Offset != 0 { + n += 1 + sozTypes(uint64(m.Offset)) + } + if m.Length != 0 { + n += 1 + sovTypes(uint64(m.Length)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func sovTypes(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozTypes(x uint64) (n int) { + return sovTypes(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *Request) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Request: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Request: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Symbols", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Symbols = append(m.Symbols, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Timeseries", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Timeseries = append(m.Timeseries, TimeSeries{}) + if err := m.Timeseries[len(m.Timeseries)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *TimeSeries) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: TimeSeries: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: TimeSeries: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType == 0 { + var v uint32 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.LabelsRefs = append(m.LabelsRefs, v) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + var count int + for _, integer := range dAtA[iNdEx:postIndex] { + if integer < 128 { + count++ + } + } + elementCount = count + if elementCount != 0 && len(m.LabelsRefs) == 0 { + m.LabelsRefs = make([]uint32, 0, elementCount) + } + for iNdEx < postIndex { + var v uint32 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.LabelsRefs = append(m.LabelsRefs, v) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field LabelsRefs", wireType) + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Samples", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Samples = append(m.Samples, Sample{}) + if err := m.Samples[len(m.Samples)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Histograms", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Histograms = append(m.Histograms, Histogram{}) + if err := m.Histograms[len(m.Histograms)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Exemplars", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Exemplars = append(m.Exemplars, Exemplar{}) + if err := m.Exemplars[len(m.Exemplars)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Metadata", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Metadata.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CreatedTimestamp", wireType) + } + m.CreatedTimestamp = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.CreatedTimestamp |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Exemplar) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Exemplar: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Exemplar: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType == 0 { + var v uint32 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.LabelsRefs = append(m.LabelsRefs, v) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + var count int + for _, integer := range dAtA[iNdEx:postIndex] { + if integer < 128 { + count++ + } + } + elementCount = count + if elementCount != 0 && len(m.LabelsRefs) == 0 { + m.LabelsRefs = make([]uint32, 0, elementCount) + } + for iNdEx < postIndex { + var v uint32 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.LabelsRefs = append(m.LabelsRefs, v) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field LabelsRefs", wireType) + } + case 2: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) + } + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + m.Value = float64(math.Float64frombits(v)) + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) + } + m.Timestamp = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Timestamp |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Sample) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Sample: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Sample: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) + } + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + m.Value = float64(math.Float64frombits(v)) + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) + } + m.Timestamp = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Timestamp |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Metadata) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Metadata: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Metadata: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Type", wireType) + } + m.Type = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Type |= Metadata_MetricType(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field HelpRef", wireType) + } + m.HelpRef = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.HelpRef |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field UnitRef", wireType) + } + m.UnitRef = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.UnitRef |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Histogram) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Histogram: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Histogram: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CountInt", wireType) + } + var v uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Count = &Histogram_CountInt{v} + case 2: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field CountFloat", wireType) + } + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + m.Count = &Histogram_CountFloat{float64(math.Float64frombits(v))} + case 3: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field Sum", wireType) + } + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + m.Sum = float64(math.Float64frombits(v)) + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Schema", wireType) + } + var v int32 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int32(b&0x7F) << shift + if b < 0x80 { + break + } + } + v = int32((uint32(v) >> 1) ^ uint32(((v&1)<<31)>>31)) + m.Schema = v + case 5: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field ZeroThreshold", wireType) + } + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + m.ZeroThreshold = float64(math.Float64frombits(v)) + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ZeroCountInt", wireType) + } + var v uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.ZeroCount = &Histogram_ZeroCountInt{v} + case 7: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field ZeroCountFloat", wireType) + } + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + m.ZeroCount = &Histogram_ZeroCountFloat{float64(math.Float64frombits(v))} + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field NegativeSpans", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.NegativeSpans = append(m.NegativeSpans, BucketSpan{}) + if err := m.NegativeSpans[len(m.NegativeSpans)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 9: + if wireType == 0 { + var v uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + v = (v >> 1) ^ uint64((int64(v&1)<<63)>>63) + m.NegativeDeltas = append(m.NegativeDeltas, int64(v)) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + var count int + for _, integer := range dAtA[iNdEx:postIndex] { + if integer < 128 { + count++ + } + } + elementCount = count + if elementCount != 0 && len(m.NegativeDeltas) == 0 { + m.NegativeDeltas = make([]int64, 0, elementCount) + } + for iNdEx < postIndex { + var v uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + v = (v >> 1) ^ uint64((int64(v&1)<<63)>>63) + m.NegativeDeltas = append(m.NegativeDeltas, int64(v)) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field NegativeDeltas", wireType) + } + case 10: + if wireType == 1 { + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + v2 := float64(math.Float64frombits(v)) + m.NegativeCounts = append(m.NegativeCounts, v2) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + elementCount = packedLen / 8 + if elementCount != 0 && len(m.NegativeCounts) == 0 { + m.NegativeCounts = make([]float64, 0, elementCount) + } + for iNdEx < postIndex { + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + v2 := float64(math.Float64frombits(v)) + m.NegativeCounts = append(m.NegativeCounts, v2) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field NegativeCounts", wireType) + } + case 11: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PositiveSpans", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PositiveSpans = append(m.PositiveSpans, BucketSpan{}) + if err := m.PositiveSpans[len(m.PositiveSpans)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 12: + if wireType == 0 { + var v uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + v = (v >> 1) ^ uint64((int64(v&1)<<63)>>63) + m.PositiveDeltas = append(m.PositiveDeltas, int64(v)) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + var count int + for _, integer := range dAtA[iNdEx:postIndex] { + if integer < 128 { + count++ + } + } + elementCount = count + if elementCount != 0 && len(m.PositiveDeltas) == 0 { + m.PositiveDeltas = make([]int64, 0, elementCount) + } + for iNdEx < postIndex { + var v uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + v = (v >> 1) ^ uint64((int64(v&1)<<63)>>63) + m.PositiveDeltas = append(m.PositiveDeltas, int64(v)) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field PositiveDeltas", wireType) + } + case 13: + if wireType == 1 { + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + v2 := float64(math.Float64frombits(v)) + m.PositiveCounts = append(m.PositiveCounts, v2) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + elementCount = packedLen / 8 + if elementCount != 0 && len(m.PositiveCounts) == 0 { + m.PositiveCounts = make([]float64, 0, elementCount) + } + for iNdEx < postIndex { + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + v2 := float64(math.Float64frombits(v)) + m.PositiveCounts = append(m.PositiveCounts, v2) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field PositiveCounts", wireType) + } + case 14: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ResetHint", wireType) + } + m.ResetHint = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ResetHint |= Histogram_ResetHint(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 15: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) + } + m.Timestamp = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Timestamp |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 16: + if wireType == 1 { + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + v2 := float64(math.Float64frombits(v)) + m.CustomValues = append(m.CustomValues, v2) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + elementCount = packedLen / 8 + if elementCount != 0 && len(m.CustomValues) == 0 { + m.CustomValues = make([]float64, 0, elementCount) + } + for iNdEx < postIndex { + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + v2 := float64(math.Float64frombits(v)) + m.CustomValues = append(m.CustomValues, v2) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field CustomValues", wireType) + } + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *BucketSpan) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: BucketSpan: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: BucketSpan: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Offset", wireType) + } + var v int32 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int32(b&0x7F) << shift + if b < 0x80 { + break + } + } + v = int32((uint32(v) >> 1) ^ uint32(((v&1)<<31)>>31)) + m.Offset = v + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Length", wireType) + } + m.Length = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Length |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipTypes(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTypes + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTypes + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTypes + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthTypes + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupTypes + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthTypes + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthTypes = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowTypes = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupTypes = fmt.Errorf("proto: unexpected end of group") +) diff --git a/prompb/io/prometheus/write/v2/types.proto b/prompb/io/prometheus/write/v2/types.proto new file mode 100644 index 0000000000..0cc7b8bc4a --- /dev/null +++ b/prompb/io/prometheus/write/v2/types.proto @@ -0,0 +1,260 @@ +// Copyright 2024 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// NOTE: This file is also available on https://buf.build/prometheus/prometheus/docs/main:io.prometheus.write.v2 + +syntax = "proto3"; +package io.prometheus.write.v2; + +option go_package = "writev2"; + +import "gogoproto/gogo.proto"; + +// Request represents a request to write the given timeseries to a remote destination. +// This message was introduced in the Remote Write 2.0 specification: +// https://prometheus.io/docs/concepts/remote_write_spec_2_0/ +// +// The canonical Content-Type request header value for this message is +// "application/x-protobuf;proto=io.prometheus.write.v2.Request" +// +// NOTE: gogoproto options might change in future for this file, they +// are not part of the spec proto (they only modify the generated Go code, not +// the serialized message). See: https://github.com/prometheus/prometheus/issues/11908 +message Request { + // Since Request supersedes 1.0 spec's prometheus.WriteRequest, we reserve the top-down message + // for the deterministic interop between those two, see types_test.go for details. + // Generally it's not needed, because Receivers must use the Content-Type header, but we want to + // be sympathetic to adopters with mistaken implementations and have deterministic error (empty + // message if you use the wrong proto schema). + reserved 1 to 3; + + // symbols contains a de-duplicated array of string elements used for various + // items in a Request message, like labels and metadata items. For the sender's convenience + // around empty values for optional fields like unit_ref, symbols array MUST start with + // empty string. + // + // To decode each of the symbolized strings, referenced, by "ref(s)" suffix, you + // need to lookup the actual string by index from symbols array. The order of + // strings is up to the sender. The receiver should not assume any particular encoding. + repeated string symbols = 4; + // timeseries represents an array of distinct series with 0 or more samples. + repeated TimeSeries timeseries = 5 [(gogoproto.nullable) = false]; +} + +// TimeSeries represents a single series. +message TimeSeries { + // labels_refs is a list of label name-value pair references, encoded + // as indices to the Request.symbols array. This list's length is always + // a multiple of two, and the underlying labels should be sorted lexicographically. + // + // Note that there might be multiple TimeSeries objects in the same + // Requests with the same labels e.g. for different exemplars, metadata + // or created timestamp. + repeated uint32 labels_refs = 1; + + // Timeseries messages can either specify samples or (native) histogram samples + // (histogram field), but not both. For a typical sender (real-time metric + // streaming), in healthy cases, there will be only one sample or histogram. + // + // Samples and histograms are sorted by timestamp (older first). + repeated Sample samples = 2 [(gogoproto.nullable) = false]; + repeated Histogram histograms = 3 [(gogoproto.nullable) = false]; + + // exemplars represents an optional set of exemplars attached to this series' samples. + repeated Exemplar exemplars = 4 [(gogoproto.nullable) = false]; + + // metadata represents the metadata associated with the given series' samples. + Metadata metadata = 5 [(gogoproto.nullable) = false]; + + // created_timestamp represents an optional created timestamp associated with + // this series' samples in ms format, typically for counter or histogram type + // metrics. Created timestamp represents the time when the counter started + // counting (sometimes referred to as start timestamp), which can increase + // the accuracy of query results. + // + // Note that some receivers might require this and in return fail to + // ingest such samples within the Request. + // + // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go + // for conversion from/to time.Time to Prometheus timestamp. + // + // Note that the "optional" keyword is omitted due to + // https://cloud.google.com/apis/design/design_patterns.md#optional_primitive_fields + // Zero value means value not set. If you need to use exactly zero value for + // the timestamp, use 1 millisecond before or after. + int64 created_timestamp = 6; +} + +// Exemplar is an additional information attached to some series' samples. +// It is typically used to attach an example trace or request ID associated with +// the metric changes. +message Exemplar { + // labels_refs is an optional list of label name-value pair references, encoded + // as indices to the Request.symbols array. This list's len is always + // a multiple of 2, and the underlying labels should be sorted lexicographically. + // If the exemplar references a trace it should use the `trace_id` label name, as a best practice. + repeated uint32 labels_refs = 1; + // value represents an exact example value. This can be useful when the exemplar + // is attached to a histogram, which only gives an estimated value through buckets. + double value = 2; + // timestamp represents an optional timestamp of the sample in ms. + // + // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go + // for conversion from/to time.Time to Prometheus timestamp. + // + // Note that the "optional" keyword is omitted due to + // https://cloud.google.com/apis/design/design_patterns.md#optional_primitive_fields + // Zero value means value not set. If you need to use exactly zero value for + // the timestamp, use 1 millisecond before or after. + int64 timestamp = 3; +} + +// Sample represents series sample. +message Sample { + // value of the sample. + double value = 1; + // timestamp represents timestamp of the sample in ms. + // + // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go + // for conversion from/to time.Time to Prometheus timestamp. + int64 timestamp = 2; +} + +// Metadata represents the metadata associated with the given series' samples. +message Metadata { + enum MetricType { + METRIC_TYPE_UNSPECIFIED = 0; + METRIC_TYPE_COUNTER = 1; + METRIC_TYPE_GAUGE = 2; + METRIC_TYPE_HISTOGRAM = 3; + METRIC_TYPE_GAUGEHISTOGRAM = 4; + METRIC_TYPE_SUMMARY = 5; + METRIC_TYPE_INFO = 6; + METRIC_TYPE_STATESET = 7; + } + MetricType type = 1; + // help_ref is a reference to the Request.symbols array representing help + // text for the metric. Help is optional, reference should point to an empty string in + // such a case. + uint32 help_ref = 3; + // unit_ref is a reference to the Request.symbols array representing a unit + // for the metric. Unit is optional, reference should point to an empty string in + // such a case. + uint32 unit_ref = 4; +} + +// A native histogram, also known as a sparse histogram. +// Original design doc: +// https://docs.google.com/document/d/1cLNv3aufPZb3fNfaJgdaRBZsInZKKIHo9E6HinJVbpM/edit +// The appendix of this design doc also explains the concept of float +// histograms. This Histogram message can represent both, the usual +// integer histogram as well as a float histogram. +message Histogram { + enum ResetHint { + RESET_HINT_UNSPECIFIED = 0; // Need to test for a counter reset explicitly. + RESET_HINT_YES = 1; // This is the 1st histogram after a counter reset. + RESET_HINT_NO = 2; // There was no counter reset between this and the previous Histogram. + RESET_HINT_GAUGE = 3; // This is a gauge histogram where counter resets don't happen. + } + + oneof count { // Count of observations in the histogram. + uint64 count_int = 1; + double count_float = 2; + } + double sum = 3; // Sum of observations in the histogram. + + // The schema defines the bucket schema. Currently, valid numbers + // are -53 and numbers in range of -4 <= n <= 8. More valid numbers might be + // added in future for new bucketing layouts. + // + // The schema equal to -53 means custom buckets. See + // custom_values field description for more details. + // + // Values between -4 and 8 represent base-2 bucket schema, where 1 + // is a bucket boundary in each case, and then each power of two is + // divided into 2^n (n is schema value) logarithmic buckets. Or in other words, + // each bucket boundary is the previous boundary times 2^(2^-n). + sint32 schema = 4; + double zero_threshold = 5; // Breadth of the zero bucket. + oneof zero_count { // Count in zero bucket. + uint64 zero_count_int = 6; + double zero_count_float = 7; + } + + // Negative Buckets. + repeated BucketSpan negative_spans = 8 [(gogoproto.nullable) = false]; + // Use either "negative_deltas" or "negative_counts", the former for + // regular histograms with integer counts, the latter for + // float histograms. + repeated sint64 negative_deltas = 9; // Count delta of each bucket compared to previous one (or to zero for 1st bucket). + repeated double negative_counts = 10; // Absolute count of each bucket. + + // Positive Buckets. + // + // In case of custom buckets (-53 schema value) the positive buckets are interpreted as follows: + // * The span offset+length points to an the index of the custom_values array + // or +Inf if pointing to the len of the array. + // * The counts and deltas have the same meaning as for exponential histograms. + repeated BucketSpan positive_spans = 11 [(gogoproto.nullable) = false]; + // Use either "positive_deltas" or "positive_counts", the former for + // regular histograms with integer counts, the latter for + // float histograms. + repeated sint64 positive_deltas = 12; // Count delta of each bucket compared to previous one (or to zero for 1st bucket). + repeated double positive_counts = 13; // Absolute count of each bucket. + + ResetHint reset_hint = 14; + // timestamp represents timestamp of the sample in ms. + // + // For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go + // for conversion from/to time.Time to Prometheus timestamp. + int64 timestamp = 15; + + // custom_values is an additional field used by non-exponential bucketing layouts. + // + // For custom buckets (-53 schema value) custom_values specify monotonically + // increasing upper inclusive boundaries for the bucket counts with arbitrary + // widths for this histogram. In other words, custom_values represents custom, + // explicit bucketing that could have been converted from the classic histograms. + // + // Those bounds are then referenced by spans in positive_spans with corresponding positive + // counts of deltas (refer to positive_spans for more details). This way we can + // have encode sparse histograms with custom bucketing (many buckets are often + // not used). + // + // Note that for custom bounds, even negative observations are placed in the positive + // counts to simplify the implementation and avoid ambiguity of where to place + // an underflow bucket, e.g. (-2, 1]. Therefore negative buckets and + // the zero bucket are unused, if the schema indicates custom bucketing. + // + // For each upper boundary the previous boundary represent the lower exclusive + // boundary for that bucket. The first element is the upper inclusive boundary + // for the first bucket, which implicitly has a lower inclusive bound of -Inf. + // This is similar to "le" label semantics on classic histograms. You may add a + // bucket with an upper bound of 0 to make sure that you really have no negative + // observations, but in practice, native histogram rendering will show both with + // or without first upper boundary 0 and no negative counts as the same case. + // + // The last element is not only the upper inclusive bound of the last regular + // bucket, but implicitly the lower exclusive bound of the +Inf bucket. + repeated double custom_values = 16; +} + +// A BucketSpan defines a number of consecutive buckets with their +// offset. Logically, it would be more straightforward to include the +// bucket counts in the Span. However, the protobuf representation is +// more compact in the way the data is structured here (with all the +// buckets in a single array separate from the Spans). +message BucketSpan { + sint32 offset = 1; // Gap to previous span, or starting point for 1st span (which can be negative). + uint32 length = 2; // Length of consecutive buckets. +} diff --git a/prompb/io/prometheus/write/v2/types_test.go b/prompb/io/prometheus/write/v2/types_test.go new file mode 100644 index 0000000000..5b7622fc2f --- /dev/null +++ b/prompb/io/prometheus/write/v2/types_test.go @@ -0,0 +1,97 @@ +// Copyright 2024 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package writev2 + +import ( + "testing" + "time" + + "github.com/gogo/protobuf/proto" + "github.com/stretchr/testify/require" + + "github.com/prometheus/prometheus/prompb" +) + +func TestInteropV2UnmarshalWithV1_DeterministicEmpty(t *testing.T) { + expectedV1Empty := &prompb.WriteRequest{} + for _, tc := range []struct{ incoming *Request }{ + { + incoming: &Request{}, // Technically wrong, should be at least empty string in symbol. + }, + { + incoming: &Request{ + Symbols: []string{""}, + }, // NOTE: Without reserved fields, failed with "corrupted" ghost TimeSeries element. + }, + { + incoming: &Request{ + Symbols: []string{"", "__name__", "metric1"}, + Timeseries: []TimeSeries{ + {LabelsRefs: []uint32{1, 2}}, + {Samples: []Sample{{Value: 21.4, Timestamp: time.Now().UnixMilli()}}}, + }, // NOTE: Without reserved fields, proto: illegal wireType 7 + }, + }, + } { + t.Run("", func(t *testing.T) { + in, err := proto.Marshal(tc.incoming) + require.NoError(t, err) + + // Test accidental unmarshal of v2 payload with v1 proto. + out := &prompb.WriteRequest{} + require.NoError(t, proto.Unmarshal(in, out)) + + // Drop unknowns, we expect them when incoming payload had some fields. + // This field & method will be likely gone after gogo removal. + out.XXX_unrecognized = nil // NOTE: out.XXX_DiscardUnknown() does not work with nullables. + + require.Equal(t, expectedV1Empty, out) + }) + } +} + +func TestInteropV1UnmarshalWithV2_DeterministicEmpty(t *testing.T) { + expectedV2Empty := &Request{} + for _, tc := range []struct{ incoming *prompb.WriteRequest }{ + { + incoming: &prompb.WriteRequest{}, + }, + { + incoming: &prompb.WriteRequest{ + Timeseries: []prompb.TimeSeries{ + { + Labels: []prompb.Label{{Name: "__name__", Value: "metric1"}}, + Samples: []prompb.Sample{{Value: 21.4, Timestamp: time.Now().UnixMilli()}}, + }, + }, + }, + // NOTE: Without reserved fields, results in corrupted v2.Request.Symbols. + }, + } { + t.Run("", func(t *testing.T) { + in, err := proto.Marshal(tc.incoming) + require.NoError(t, err) + + // Test accidental unmarshal of v1 payload with v2 proto. + out := &Request{} + require.NoError(t, proto.Unmarshal(in, out)) + + // Drop unknowns, we expect them when incoming payload had some fields. + // This field & method will be likely gone after gogo removal. + out.XXX_unrecognized = nil // NOTE: out.XXX_DiscardUnknown() does not work with nullables. + + require.Equal(t, expectedV2Empty, out) + }) + } +} diff --git a/prompb/rwcommon/codec_test.go b/prompb/rwcommon/codec_test.go new file mode 100644 index 0000000000..08e9e62d22 --- /dev/null +++ b/prompb/rwcommon/codec_test.go @@ -0,0 +1,282 @@ +// Copyright 2024 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rwcommon + +import ( + "testing" + + "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" + + "github.com/prometheus/prometheus/model/histogram" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/metadata" + "github.com/prometheus/prometheus/prompb" + writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2" +) + +func TestToLabels(t *testing.T) { + expected := labels.FromStrings("__name__", "metric1", "foo", "bar") + + t.Run("v1", func(t *testing.T) { + ts := prompb.TimeSeries{Labels: []prompb.Label{{Name: "__name__", Value: "metric1"}, {Name: "foo", Value: "bar"}}} + b := labels.NewScratchBuilder(2) + require.Equal(t, expected, ts.ToLabels(&b, nil)) + require.Equal(t, ts.Labels, prompb.FromLabels(expected, nil)) + require.Equal(t, ts.Labels, prompb.FromLabels(expected, ts.Labels)) + }) + t.Run("v2", func(t *testing.T) { + v2Symbols := []string{"", "__name__", "metric1", "foo", "bar"} + ts := writev2.TimeSeries{LabelsRefs: []uint32{1, 2, 3, 4}} + b := labels.NewScratchBuilder(2) + require.Equal(t, expected, ts.ToLabels(&b, v2Symbols)) + // No need for FromLabels in our prod code as we use symbol table to do so. + }) +} + +func TestFromMetadataType(t *testing.T) { + for _, tc := range []struct { + desc string + input model.MetricType + expectedV1 prompb.MetricMetadata_MetricType + expectedV2 writev2.Metadata_MetricType + }{ + { + desc: "with a single-word metric", + input: model.MetricTypeCounter, + expectedV1: prompb.MetricMetadata_COUNTER, + expectedV2: writev2.Metadata_METRIC_TYPE_COUNTER, + }, + { + desc: "with a two-word metric", + input: model.MetricTypeStateset, + expectedV1: prompb.MetricMetadata_STATESET, + expectedV2: writev2.Metadata_METRIC_TYPE_STATESET, + }, + { + desc: "with an unknown metric", + input: "not-known", + expectedV1: prompb.MetricMetadata_UNKNOWN, + expectedV2: writev2.Metadata_METRIC_TYPE_UNSPECIFIED, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + t.Run("v1", func(t *testing.T) { + require.Equal(t, tc.expectedV1, prompb.FromMetadataType(tc.input)) + }) + t.Run("v2", func(t *testing.T) { + require.Equal(t, tc.expectedV2, writev2.FromMetadataType(tc.input)) + }) + }) + } +} + +func TestToMetadata(t *testing.T) { + sym := writev2.NewSymbolTable() + + for _, tc := range []struct { + input writev2.Metadata + expected metadata.Metadata + }{ + { + input: writev2.Metadata{}, + expected: metadata.Metadata{ + Type: model.MetricTypeUnknown, + }, + }, + { + input: writev2.Metadata{ + Type: 12414, // Unknown. + }, + expected: metadata.Metadata{ + Type: model.MetricTypeUnknown, + }, + }, + { + input: writev2.Metadata{ + Type: writev2.Metadata_METRIC_TYPE_COUNTER, + HelpRef: sym.Symbolize("help1"), + UnitRef: sym.Symbolize("unit1"), + }, + expected: metadata.Metadata{ + Type: model.MetricTypeCounter, + Help: "help1", + Unit: "unit1", + }, + }, + { + input: writev2.Metadata{ + Type: writev2.Metadata_METRIC_TYPE_STATESET, + HelpRef: sym.Symbolize("help2"), + }, + expected: metadata.Metadata{ + Type: model.MetricTypeStateset, + Help: "help2", + }, + }, + } { + t.Run("", func(t *testing.T) { + ts := writev2.TimeSeries{Metadata: tc.input} + require.Equal(t, tc.expected, ts.ToMetadata(sym.Symbols())) + }) + } +} + +func TestToHistogram_Empty(t *testing.T) { + t.Run("v1", func(t *testing.T) { + require.NotNilf(t, prompb.Histogram{}.ToIntHistogram(), "") + require.NotNilf(t, prompb.Histogram{}.ToFloatHistogram(), "") + }) + t.Run("v2", func(t *testing.T) { + require.NotNilf(t, writev2.Histogram{}.ToIntHistogram(), "") + require.NotNilf(t, writev2.Histogram{}.ToFloatHistogram(), "") + }) +} + +func testIntHistogram() histogram.Histogram { + return histogram.Histogram{ + CounterResetHint: histogram.GaugeType, + Schema: 0, + Count: 19, + Sum: 2.7, + ZeroThreshold: 1e-128, + PositiveSpans: []histogram.Span{ + {Offset: 0, Length: 4}, + {Offset: 0, Length: 0}, + {Offset: 0, Length: 3}, + }, + PositiveBuckets: []int64{1, 2, -2, 1, -1, 0, 0}, + NegativeSpans: []histogram.Span{ + {Offset: 0, Length: 5}, + {Offset: 1, Length: 0}, + {Offset: 0, Length: 1}, + }, + NegativeBuckets: []int64{1, 2, -2, 1, -1, 0}, + } +} + +func testFloatHistogram() histogram.FloatHistogram { + return histogram.FloatHistogram{ + CounterResetHint: histogram.GaugeType, + Schema: 0, + Count: 19, + Sum: 2.7, + ZeroThreshold: 1e-128, + PositiveSpans: []histogram.Span{ + {Offset: 0, Length: 4}, + {Offset: 0, Length: 0}, + {Offset: 0, Length: 3}, + }, + PositiveBuckets: []float64{1, 3, 1, 2, 1, 1, 1}, + NegativeSpans: []histogram.Span{ + {Offset: 0, Length: 5}, + {Offset: 1, Length: 0}, + {Offset: 0, Length: 1}, + }, + NegativeBuckets: []float64{1, 3, 1, 2, 1, 1}, + } +} + +func TestFromIntToFloatOrIntHistogram(t *testing.T) { + testIntHist := testIntHistogram() + testFloatHist := testFloatHistogram() + + t.Run("v1", func(t *testing.T) { + h := prompb.FromIntHistogram(123, testIntHist.Copy()) + require.False(t, h.IsFloatHistogram()) + require.Equal(t, int64(123), h.Timestamp) + require.Equal(t, testIntHist, *h.ToIntHistogram()) + require.Equal(t, testFloatHist, *h.ToFloatHistogram()) + }) + t.Run("v2", func(t *testing.T) { + h := writev2.FromIntHistogram(123, testIntHist.Copy()) + require.False(t, h.IsFloatHistogram()) + require.Equal(t, int64(123), h.Timestamp) + require.Equal(t, testIntHist, *h.ToIntHistogram()) + require.Equal(t, testFloatHist, *h.ToFloatHistogram()) + }) +} + +func TestFromFloatToFloatHistogram(t *testing.T) { + testFloatHist := testFloatHistogram() + + t.Run("v1", func(t *testing.T) { + h := prompb.FromFloatHistogram(123, testFloatHist.Copy()) + require.True(t, h.IsFloatHistogram()) + require.Equal(t, int64(123), h.Timestamp) + require.Nil(t, h.ToIntHistogram()) + require.Equal(t, testFloatHist, *h.ToFloatHistogram()) + }) + t.Run("v2", func(t *testing.T) { + h := writev2.FromFloatHistogram(123, testFloatHist.Copy()) + require.True(t, h.IsFloatHistogram()) + require.Equal(t, int64(123), h.Timestamp) + require.Nil(t, h.ToIntHistogram()) + require.Equal(t, testFloatHist, *h.ToFloatHistogram()) + }) +} + +func TestFromIntOrFloatHistogram_ResetHint(t *testing.T) { + for _, tc := range []struct { + input histogram.CounterResetHint + expectedV1 prompb.Histogram_ResetHint + expectedV2 writev2.Histogram_ResetHint + }{ + { + input: histogram.UnknownCounterReset, + expectedV1: prompb.Histogram_UNKNOWN, + expectedV2: writev2.Histogram_RESET_HINT_UNSPECIFIED, + }, + { + input: histogram.CounterReset, + expectedV1: prompb.Histogram_YES, + expectedV2: writev2.Histogram_RESET_HINT_YES, + }, + { + input: histogram.NotCounterReset, + expectedV1: prompb.Histogram_NO, + expectedV2: writev2.Histogram_RESET_HINT_NO, + }, + { + input: histogram.GaugeType, + expectedV1: prompb.Histogram_GAUGE, + expectedV2: writev2.Histogram_RESET_HINT_GAUGE, + }, + } { + t.Run("", func(t *testing.T) { + t.Run("v1", func(t *testing.T) { + h := testIntHistogram() + h.CounterResetHint = tc.input + got := prompb.FromIntHistogram(1337, &h) + require.Equal(t, tc.expectedV1, got.GetResetHint()) + + fh := testFloatHistogram() + fh.CounterResetHint = tc.input + got2 := prompb.FromFloatHistogram(1337, &fh) + require.Equal(t, tc.expectedV1, got2.GetResetHint()) + }) + t.Run("v2", func(t *testing.T) { + h := testIntHistogram() + h.CounterResetHint = tc.input + got := writev2.FromIntHistogram(1337, &h) + require.Equal(t, tc.expectedV2, got.GetResetHint()) + + fh := testFloatHistogram() + fh.CounterResetHint = tc.input + got2 := writev2.FromFloatHistogram(1337, &fh) + require.Equal(t, tc.expectedV2, got2.GetResetHint()) + }) + }) + } +} diff --git a/scrape/manager.go b/scrape/manager.go index cb92db5a8c..156e949f83 100644 --- a/scrape/manager.go +++ b/scrape/manager.go @@ -73,9 +73,11 @@ type Options struct { // Option used by downstream scraper users like OpenTelemetry Collector // to help lookup metric metadata. Should be false for Prometheus. PassMetadataInContext bool - // Option to enable the experimental in-memory metadata storage and append - // metadata to the WAL. - EnableMetadataStorage bool + // Option to enable appending of scraped Metadata to the TSDB/other appenders. Individual appenders + // can decide what to do with metadata, but for practical purposes this flag exists so that metadata + // can be written to the WAL and thus read for remote write. + // TODO: implement some form of metadata storage + AppendMetadata bool // Option to increase the interval used by scrape manager to throttle target groups updates. DiscoveryReloadInterval model.Duration // Option to enable the ingestion of the created timestamp as a synthetic zero sample. diff --git a/scrape/scrape.go b/scrape/scrape.go index a0b681444c..17e9913e8c 100644 --- a/scrape/scrape.go +++ b/scrape/scrape.go @@ -181,7 +181,7 @@ func newScrapePool(cfg *config.ScrapeConfig, app storage.Appendable, offsetSeed options.EnableNativeHistogramsIngestion, options.EnableCreatedTimestampZeroIngestion, options.ExtraMetrics, - options.EnableMetadataStorage, + options.AppendMetadata, opts.target, options.PassMetadataInContext, metrics, diff --git a/scripts/genproto.sh b/scripts/genproto.sh index dee51d4aad..4ee337dfa9 100755 --- a/scripts/genproto.sh +++ b/scripts/genproto.sh @@ -10,8 +10,9 @@ if ! [[ "$0" =~ "scripts/genproto.sh" ]]; then exit 255 fi +# TODO(bwplotka): Move to buf, this is not OSS agnostic, likely won't work locally. if ! [[ $(protoc --version) =~ "3.15.8" ]]; then - echo "could not find protoc 3.15.8, is it installed + in PATH?" + echo "could not find protoc 3.15.8, is it installed + in PATH? Consider commenting out this check for local flow" exit 255 fi @@ -40,6 +41,9 @@ for dir in ${DIRS}; do -I="${PROM_PATH}" \ -I="${GRPC_GATEWAY_ROOT}/third_party/googleapis" \ ./*.proto + protoc --gogofast_out=plugins=grpc:. -I=. \ + -I="${GOGOPROTO_PATH}" \ + ./io/prometheus/write/v2/*.proto protoc --gogofast_out=Mgoogle/protobuf/timestamp.proto=github.com/gogo/protobuf/types,paths=source_relative:. -I=. \ -I="${GOGOPROTO_PATH}" \ ./io/prometheus/client/*.proto diff --git a/storage/remote/client.go b/storage/remote/client.go index e8791b643a..eff44c6060 100644 --- a/storage/remote/client.go +++ b/storage/remote/client.go @@ -35,13 +35,40 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" + "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/prompb" "github.com/prometheus/prometheus/storage/remote/azuread" ) const maxErrMsgLen = 1024 -var UserAgent = fmt.Sprintf("Prometheus/%s", version.Version) +const ( + RemoteWriteVersionHeader = "X-Prometheus-Remote-Write-Version" + RemoteWriteVersion1HeaderValue = "0.1.0" + RemoteWriteVersion20HeaderValue = "2.0.0" + appProtoContentType = "application/x-protobuf" +) + +// Compression represents the encoding. Currently remote storage supports only +// one, but we experiment with more, thus leaving the compression scaffolding +// for now. +// NOTE(bwplotka): Keeping it public, as a non-stable help for importers to use. +type Compression string + +const ( + // SnappyBlockCompression represents https://github.com/google/snappy/blob/2c94e11145f0b7b184b831577c93e5a41c4c0346/format_description.txt + SnappyBlockCompression Compression = "snappy" +) + +var ( + // UserAgent represents Prometheus version to use for user agent header. + UserAgent = fmt.Sprintf("Prometheus/%s", version.Version) + + remoteWriteContentTypeHeaders = map[config.RemoteWriteProtoMsg]string{ + config.RemoteWriteProtoMsgV1: appProtoContentType, // Also application/x-protobuf;proto=prometheus.WriteRequest but simplified for compatibility with 1.x spec. + config.RemoteWriteProtoMsgV2: appProtoContentType + ";proto=io.prometheus.write.v2.Request", + } +) var ( remoteReadQueriesTotal = prometheus.NewCounterVec( @@ -93,6 +120,9 @@ type Client struct { readQueries prometheus.Gauge readQueriesTotal *prometheus.CounterVec readQueriesDuration prometheus.Observer + + writeProtoMsg config.RemoteWriteProtoMsg + writeCompression Compression // Not exposed by ClientConfig for now. } // ClientConfig configures a client. @@ -104,6 +134,7 @@ type ClientConfig struct { AzureADConfig *azuread.AzureADConfig Headers map[string]string RetryOnRateLimit bool + WriteProtoMsg config.RemoteWriteProtoMsg } // ReadClient uses the SAMPLES method of remote read to read series samples from remote server. @@ -162,14 +193,20 @@ func NewWriteClient(name string, conf *ClientConfig) (WriteClient, error) { } } - httpClient.Transport = otelhttp.NewTransport(t) + writeProtoMsg := config.RemoteWriteProtoMsgV1 + if conf.WriteProtoMsg != "" { + writeProtoMsg = conf.WriteProtoMsg + } + httpClient.Transport = otelhttp.NewTransport(t) return &Client{ remoteName: name, urlString: conf.URL.String(), Client: httpClient, retryOnRateLimit: conf.RetryOnRateLimit, timeout: time.Duration(conf.Timeout), + writeProtoMsg: writeProtoMsg, + writeCompression: SnappyBlockCompression, }, nil } @@ -206,10 +243,16 @@ func (c *Client) Store(ctx context.Context, req []byte, attempt int) error { return err } - httpReq.Header.Add("Content-Encoding", "snappy") - httpReq.Header.Set("Content-Type", "application/x-protobuf") + httpReq.Header.Add("Content-Encoding", string(c.writeCompression)) + httpReq.Header.Set("Content-Type", remoteWriteContentTypeHeaders[c.writeProtoMsg]) httpReq.Header.Set("User-Agent", UserAgent) - httpReq.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0") + if c.writeProtoMsg == config.RemoteWriteProtoMsgV1 { + // Compatibility mode for 1.0. + httpReq.Header.Set(RemoteWriteVersionHeader, RemoteWriteVersion1HeaderValue) + } else { + httpReq.Header.Set(RemoteWriteVersionHeader, RemoteWriteVersion20HeaderValue) + } + if attempt > 0 { httpReq.Header.Set("Retry-Attempt", strconv.Itoa(attempt)) } @@ -265,12 +308,12 @@ func retryAfterDuration(t string) model.Duration { } // Name uniquely identifies the client. -func (c Client) Name() string { +func (c *Client) Name() string { return c.remoteName } // Endpoint is the remote read or write endpoint. -func (c Client) Endpoint() string { +func (c *Client) Endpoint() string { return c.urlString } diff --git a/storage/remote/codec.go b/storage/remote/codec.go index 8c569ff038..c9220ca42d 100644 --- a/storage/remote/codec.go +++ b/storage/remote/codec.go @@ -22,7 +22,6 @@ import ( "net/http" "slices" "sort" - "strings" "sync" "github.com/gogo/protobuf/proto" @@ -30,10 +29,10 @@ import ( "github.com/prometheus/common/model" "go.opentelemetry.io/collector/pdata/pmetric/pmetricotlp" - "github.com/prometheus/prometheus/model/exemplar" "github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/prompb" + writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2" "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/tsdb/chunkenc" "github.com/prometheus/prometheus/tsdb/chunks" @@ -153,10 +152,10 @@ func ToQueryResult(ss storage.SeriesSet, sampleLimit int) (*prompb.QueryResult, }) case chunkenc.ValHistogram: ts, h := iter.AtHistogram(nil) - histograms = append(histograms, HistogramToHistogramProto(ts, h)) + histograms = append(histograms, prompb.FromIntHistogram(ts, h)) case chunkenc.ValFloatHistogram: ts, fh := iter.AtFloatHistogram(nil) - histograms = append(histograms, FloatHistogramToHistogramProto(ts, fh)) + histograms = append(histograms, prompb.FromFloatHistogram(ts, fh)) default: return nil, ss.Warnings(), fmt.Errorf("unrecognized value type: %s", valType) } @@ -166,7 +165,7 @@ func ToQueryResult(ss storage.SeriesSet, sampleLimit int) (*prompb.QueryResult, } resp.Timeseries = append(resp.Timeseries, &prompb.TimeSeries{ - Labels: LabelsToLabelsProto(series.Labels(), nil), + Labels: prompb.FromLabels(series.Labels(), nil), Samples: samples, Histograms: histograms, }) @@ -182,7 +181,7 @@ func FromQueryResult(sortSeries bool, res *prompb.QueryResult) storage.SeriesSet if err := validateLabelsAndMetricName(ts.Labels); err != nil { return errSeriesSet{err: err} } - lbls := LabelProtosToLabels(&b, ts.Labels) + lbls := ts.ToLabels(&b, nil) series = append(series, &concreteSeries{labels: lbls, floats: ts.Samples, histograms: ts.Histograms}) } @@ -235,7 +234,7 @@ func StreamChunkedReadResponses( for ss.Next() { series := ss.At() iter = series.Iterator(iter) - lbls = MergeLabels(LabelsToLabelsProto(series.Labels(), lbls), sortedExternalLabels) + lbls = MergeLabels(prompb.FromLabels(series.Labels(), lbls), sortedExternalLabels) maxDataLength := maxBytesInFrame for _, lbl := range lbls { @@ -481,21 +480,16 @@ func (c *concreteSeriesIterator) AtHistogram(*histogram.Histogram) (int64, *hist panic("iterator is not on an integer histogram sample") } h := c.series.histograms[c.histogramsCur] - return h.Timestamp, HistogramProtoToHistogram(h) + return h.Timestamp, h.ToIntHistogram() } // AtFloatHistogram implements chunkenc.Iterator. func (c *concreteSeriesIterator) AtFloatHistogram(*histogram.FloatHistogram) (int64, *histogram.FloatHistogram) { - switch c.curValType { - case chunkenc.ValHistogram: + if c.curValType == chunkenc.ValHistogram || c.curValType == chunkenc.ValFloatHistogram { fh := c.series.histograms[c.histogramsCur] - return fh.Timestamp, HistogramProtoToFloatHistogram(fh) - case chunkenc.ValFloatHistogram: - fh := c.series.histograms[c.histogramsCur] - return fh.Timestamp, FloatHistogramProtoToFloatHistogram(fh) - default: - panic("iterator is not on a histogram sample") + return fh.Timestamp, fh.ToFloatHistogram() // integer will be auto-converted. } + panic("iterator is not on a histogram sample") } // AtT implements chunkenc.Iterator. @@ -618,141 +612,6 @@ func FromLabelMatchers(matchers []*prompb.LabelMatcher) ([]*labels.Matcher, erro return result, nil } -func exemplarProtoToExemplar(b *labels.ScratchBuilder, ep prompb.Exemplar) exemplar.Exemplar { - timestamp := ep.Timestamp - - return exemplar.Exemplar{ - Labels: LabelProtosToLabels(b, ep.Labels), - Value: ep.Value, - Ts: timestamp, - HasTs: timestamp != 0, - } -} - -// HistogramProtoToHistogram extracts a (normal integer) Histogram from the -// provided proto message. The caller has to make sure that the proto message -// represents an integer histogram and not a float histogram, or it panics. -func HistogramProtoToHistogram(hp prompb.Histogram) *histogram.Histogram { - if hp.IsFloatHistogram() { - panic("HistogramProtoToHistogram called with a float histogram") - } - return &histogram.Histogram{ - CounterResetHint: histogram.CounterResetHint(hp.ResetHint), - Schema: hp.Schema, - ZeroThreshold: hp.ZeroThreshold, - ZeroCount: hp.GetZeroCountInt(), - Count: hp.GetCountInt(), - Sum: hp.Sum, - PositiveSpans: spansProtoToSpans(hp.GetPositiveSpans()), - PositiveBuckets: hp.GetPositiveDeltas(), - NegativeSpans: spansProtoToSpans(hp.GetNegativeSpans()), - NegativeBuckets: hp.GetNegativeDeltas(), - } -} - -// FloatHistogramProtoToFloatHistogram extracts a float Histogram from the -// provided proto message to a Float Histogram. The caller has to make sure that -// the proto message represents a float histogram and not an integer histogram, -// or it panics. -func FloatHistogramProtoToFloatHistogram(hp prompb.Histogram) *histogram.FloatHistogram { - if !hp.IsFloatHistogram() { - panic("FloatHistogramProtoToFloatHistogram called with an integer histogram") - } - return &histogram.FloatHistogram{ - CounterResetHint: histogram.CounterResetHint(hp.ResetHint), - Schema: hp.Schema, - ZeroThreshold: hp.ZeroThreshold, - ZeroCount: hp.GetZeroCountFloat(), - Count: hp.GetCountFloat(), - Sum: hp.Sum, - PositiveSpans: spansProtoToSpans(hp.GetPositiveSpans()), - PositiveBuckets: hp.GetPositiveCounts(), - NegativeSpans: spansProtoToSpans(hp.GetNegativeSpans()), - NegativeBuckets: hp.GetNegativeCounts(), - } -} - -// HistogramProtoToFloatHistogram extracts and converts a (normal integer) histogram from the provided proto message -// to a float histogram. The caller has to make sure that the proto message represents an integer histogram and not a -// float histogram, or it panics. -func HistogramProtoToFloatHistogram(hp prompb.Histogram) *histogram.FloatHistogram { - if hp.IsFloatHistogram() { - panic("HistogramProtoToFloatHistogram called with a float histogram") - } - return &histogram.FloatHistogram{ - CounterResetHint: histogram.CounterResetHint(hp.ResetHint), - Schema: hp.Schema, - ZeroThreshold: hp.ZeroThreshold, - ZeroCount: float64(hp.GetZeroCountInt()), - Count: float64(hp.GetCountInt()), - Sum: hp.Sum, - PositiveSpans: spansProtoToSpans(hp.GetPositiveSpans()), - PositiveBuckets: deltasToCounts(hp.GetPositiveDeltas()), - NegativeSpans: spansProtoToSpans(hp.GetNegativeSpans()), - NegativeBuckets: deltasToCounts(hp.GetNegativeDeltas()), - } -} - -func spansProtoToSpans(s []prompb.BucketSpan) []histogram.Span { - spans := make([]histogram.Span, len(s)) - for i := 0; i < len(s); i++ { - spans[i] = histogram.Span{Offset: s[i].Offset, Length: s[i].Length} - } - - return spans -} - -func deltasToCounts(deltas []int64) []float64 { - counts := make([]float64, len(deltas)) - var cur float64 - for i, d := range deltas { - cur += float64(d) - counts[i] = cur - } - return counts -} - -func HistogramToHistogramProto(timestamp int64, h *histogram.Histogram) prompb.Histogram { - return prompb.Histogram{ - Count: &prompb.Histogram_CountInt{CountInt: h.Count}, - Sum: h.Sum, - Schema: h.Schema, - ZeroThreshold: h.ZeroThreshold, - ZeroCount: &prompb.Histogram_ZeroCountInt{ZeroCountInt: h.ZeroCount}, - NegativeSpans: spansToSpansProto(h.NegativeSpans), - NegativeDeltas: h.NegativeBuckets, - PositiveSpans: spansToSpansProto(h.PositiveSpans), - PositiveDeltas: h.PositiveBuckets, - ResetHint: prompb.Histogram_ResetHint(h.CounterResetHint), - Timestamp: timestamp, - } -} - -func FloatHistogramToHistogramProto(timestamp int64, fh *histogram.FloatHistogram) prompb.Histogram { - return prompb.Histogram{ - Count: &prompb.Histogram_CountFloat{CountFloat: fh.Count}, - Sum: fh.Sum, - Schema: fh.Schema, - ZeroThreshold: fh.ZeroThreshold, - ZeroCount: &prompb.Histogram_ZeroCountFloat{ZeroCountFloat: fh.ZeroCount}, - NegativeSpans: spansToSpansProto(fh.NegativeSpans), - NegativeCounts: fh.NegativeBuckets, - PositiveSpans: spansToSpansProto(fh.PositiveSpans), - PositiveCounts: fh.PositiveBuckets, - ResetHint: prompb.Histogram_ResetHint(fh.CounterResetHint), - Timestamp: timestamp, - } -} - -func spansToSpansProto(s []histogram.Span) []prompb.BucketSpan { - spans := make([]prompb.BucketSpan, len(s)) - for i := 0; i < len(s); i++ { - spans[i] = prompb.BucketSpan{Offset: s[i].Offset, Length: s[i].Length} - } - - return spans -} - // LabelProtosToMetric unpack a []*prompb.Label to a model.Metric. func LabelProtosToMetric(labelPairs []*prompb.Label) model.Metric { metric := make(model.Metric, len(labelPairs)) @@ -762,43 +621,9 @@ func LabelProtosToMetric(labelPairs []*prompb.Label) model.Metric { return metric } -// LabelProtosToLabels transforms prompb labels into labels. The labels builder -// will be used to build the returned labels. -func LabelProtosToLabels(b *labels.ScratchBuilder, labelPairs []prompb.Label) labels.Labels { - b.Reset() - for _, l := range labelPairs { - b.Add(l.Name, l.Value) - } - b.Sort() - return b.Labels() -} - -// LabelsToLabelsProto transforms labels into prompb labels. The buffer slice -// will be used to avoid allocations if it is big enough to store the labels. -func LabelsToLabelsProto(lbls labels.Labels, buf []prompb.Label) []prompb.Label { - result := buf[:0] - lbls.Range(func(l labels.Label) { - result = append(result, prompb.Label{ - Name: l.Name, - Value: l.Value, - }) - }) - return result -} - -// metricTypeToMetricTypeProto transforms a Prometheus metricType into prompb metricType. Since the former is a string we need to transform it to an enum. -func metricTypeToMetricTypeProto(t model.MetricType) prompb.MetricMetadata_MetricType { - mt := strings.ToUpper(string(t)) - v, ok := prompb.MetricMetadata_MetricType_value[mt] - if !ok { - return prompb.MetricMetadata_UNKNOWN - } - - return prompb.MetricMetadata_MetricType(v) -} - // DecodeWriteRequest from an io.Reader into a prompb.WriteRequest, handling // snappy decompression. +// Used also by documentation/examples/remote_storage. func DecodeWriteRequest(r io.Reader) (*prompb.WriteRequest, error) { compressed, err := io.ReadAll(r) if err != nil { @@ -818,6 +643,28 @@ func DecodeWriteRequest(r io.Reader) (*prompb.WriteRequest, error) { return &req, nil } +// DecodeWriteV2Request from an io.Reader into a writev2.Request, handling +// snappy decompression. +// Used also by documentation/examples/remote_storage. +func DecodeWriteV2Request(r io.Reader) (*writev2.Request, error) { + compressed, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + reqBuf, err := snappy.Decode(nil, compressed) + if err != nil { + return nil, err + } + + var req writev2.Request + if err := proto.Unmarshal(reqBuf, &req); err != nil { + return nil, err + } + + return &req, nil +} + func DecodeOTLPWriteRequest(r *http.Request) (pmetricotlp.ExportRequest, error) { contentType := r.Header.Get("Content-Type") var decoderFunc func(buf []byte) (pmetricotlp.ExportRequest, error) diff --git a/storage/remote/codec_test.go b/storage/remote/codec_test.go index c3a4cbc6dd..15f8fe1320 100644 --- a/storage/remote/codec_test.go +++ b/storage/remote/codec_test.go @@ -19,13 +19,16 @@ import ( "sync" "testing" + "github.com/go-kit/log" "github.com/gogo/protobuf/proto" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/metadata" "github.com/prometheus/prometheus/prompb" + writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2" "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/tsdb/chunkenc" "github.com/prometheus/prometheus/tsdb/chunks" @@ -57,7 +60,7 @@ var writeRequestFixture = &prompb.WriteRequest{ }, Samples: []prompb.Sample{{Value: 1, Timestamp: 0}}, Exemplars: []prompb.Exemplar{{Labels: []prompb.Label{{Name: "f", Value: "g"}}, Value: 1, Timestamp: 0}}, - Histograms: []prompb.Histogram{HistogramToHistogramProto(0, &testHistogram), FloatHistogramToHistogramProto(1, testHistogram.ToFloat(nil))}, + Histograms: []prompb.Histogram{prompb.FromIntHistogram(0, &testHistogram), prompb.FromFloatHistogram(1, testHistogram.ToFloat(nil))}, }, { Labels: []prompb.Label{ @@ -69,11 +72,59 @@ var writeRequestFixture = &prompb.WriteRequest{ }, Samples: []prompb.Sample{{Value: 2, Timestamp: 1}}, Exemplars: []prompb.Exemplar{{Labels: []prompb.Label{{Name: "h", Value: "i"}}, Value: 2, Timestamp: 1}}, - Histograms: []prompb.Histogram{HistogramToHistogramProto(2, &testHistogram), FloatHistogramToHistogramProto(3, testHistogram.ToFloat(nil))}, + Histograms: []prompb.Histogram{prompb.FromIntHistogram(2, &testHistogram), prompb.FromFloatHistogram(3, testHistogram.ToFloat(nil))}, }, }, } +var ( + writeV2RequestSeries1Metadata = metadata.Metadata{ + Type: model.MetricTypeGauge, + Help: "Test gauge for test purposes", + Unit: "Maybe op/sec who knows (:", + } + writeV2RequestSeries2Metadata = metadata.Metadata{ + Type: model.MetricTypeCounter, + Help: "Test counter for test purposes", + } + + // writeV2RequestFixture represents the same request as writeRequestFixture, but using the v2 representation. + writeV2RequestFixture = func() *writev2.Request { + st := writev2.NewSymbolTable() + b := labels.NewScratchBuilder(0) + labelRefs := st.SymbolizeLabels(writeRequestFixture.Timeseries[0].ToLabels(&b, nil), nil) + exemplar1LabelRefs := st.SymbolizeLabels(writeRequestFixture.Timeseries[0].Exemplars[0].ToExemplar(&b, nil).Labels, nil) + exemplar2LabelRefs := st.SymbolizeLabels(writeRequestFixture.Timeseries[0].Exemplars[0].ToExemplar(&b, nil).Labels, nil) + return &writev2.Request{ + Timeseries: []writev2.TimeSeries{ + { + LabelsRefs: labelRefs, + Metadata: writev2.Metadata{ + Type: writev2.Metadata_METRIC_TYPE_GAUGE, // Same as writeV2RequestSeries1Metadata.Type, but in writev2. + HelpRef: st.Symbolize(writeV2RequestSeries1Metadata.Help), + UnitRef: st.Symbolize(writeV2RequestSeries1Metadata.Unit), + }, + Samples: []writev2.Sample{{Value: 1, Timestamp: 0}}, + Exemplars: []writev2.Exemplar{{LabelsRefs: exemplar1LabelRefs, Value: 1, Timestamp: 0}}, + Histograms: []writev2.Histogram{writev2.FromIntHistogram(0, &testHistogram), writev2.FromFloatHistogram(1, testHistogram.ToFloat(nil))}, + }, + { + LabelsRefs: labelRefs, + Metadata: writev2.Metadata{ + Type: writev2.Metadata_METRIC_TYPE_COUNTER, // Same as writeV2RequestSeries2Metadata.Type, but in writev2. + HelpRef: st.Symbolize(writeV2RequestSeries2Metadata.Help), + // No unit. + }, + Samples: []writev2.Sample{{Value: 2, Timestamp: 1}}, + Exemplars: []writev2.Exemplar{{LabelsRefs: exemplar2LabelRefs, Value: 2, Timestamp: 1}}, + Histograms: []writev2.Histogram{writev2.FromIntHistogram(2, &testHistogram), writev2.FromFloatHistogram(3, testHistogram.ToFloat(nil))}, + }, + }, + Symbols: st.Symbols(), + } + }() +) + func TestValidateLabelsAndMetricName(t *testing.T) { tests := []struct { input []prompb.Label @@ -268,7 +319,7 @@ func TestConcreteSeriesIterator_HistogramSamples(t *testing.T) { } else { ts = int64(i) } - histProtos[i] = HistogramToHistogramProto(ts, h) + histProtos[i] = prompb.FromIntHistogram(ts, h) } series := &concreteSeries{ labels: labels.FromStrings("foo", "bar"), @@ -319,9 +370,9 @@ func TestConcreteSeriesIterator_FloatAndHistogramSamples(t *testing.T) { histProtos := make([]prompb.Histogram, len(histograms)) for i, h := range histograms { if i < 10 { - histProtos[i] = HistogramToHistogramProto(int64(i+1), h) + histProtos[i] = prompb.FromIntHistogram(int64(i+1), h) } else { - histProtos[i] = HistogramToHistogramProto(int64(i+6), h) + histProtos[i] = prompb.FromIntHistogram(int64(i+6), h) } } series := &concreteSeries{ @@ -401,7 +452,7 @@ func TestConcreteSeriesIterator_FloatAndHistogramSamples(t *testing.T) { require.Equal(t, chunkenc.ValHistogram, it.Next()) ts, fh = it.AtFloatHistogram(nil) require.Equal(t, int64(17), ts) - expected := HistogramProtoToFloatHistogram(HistogramToHistogramProto(int64(17), histograms[11])) + expected := prompb.FromIntHistogram(int64(17), histograms[11]).ToFloatHistogram() require.Equal(t, expected, fh) // Keep calling Next() until the end. @@ -485,39 +536,8 @@ func TestMergeLabels(t *testing.T) { } } -func TestMetricTypeToMetricTypeProto(t *testing.T) { - tc := []struct { - desc string - input model.MetricType - expected prompb.MetricMetadata_MetricType - }{ - { - desc: "with a single-word metric", - input: model.MetricTypeCounter, - expected: prompb.MetricMetadata_COUNTER, - }, - { - desc: "with a two-word metric", - input: model.MetricTypeStateset, - expected: prompb.MetricMetadata_STATESET, - }, - { - desc: "with an unknown metric", - input: "not-known", - expected: prompb.MetricMetadata_UNKNOWN, - }, - } - - for _, tt := range tc { - t.Run(tt.desc, func(t *testing.T) { - m := metricTypeToMetricTypeProto(tt.input) - require.Equal(t, tt.expected, m) - }) - } -} - func TestDecodeWriteRequest(t *testing.T) { - buf, _, _, err := buildWriteRequest(nil, writeRequestFixture.Timeseries, nil, nil, nil, nil) + buf, _, _, err := buildWriteRequest(nil, writeRequestFixture.Timeseries, nil, nil, nil, nil, "snappy") require.NoError(t, err) actual, err := DecodeWriteRequest(bytes.NewReader(buf)) @@ -525,212 +545,18 @@ func TestDecodeWriteRequest(t *testing.T) { require.Equal(t, writeRequestFixture, actual) } -func TestNilHistogramProto(*testing.T) { - // This function will panic if it impromperly handles nil - // values, causing the test to fail. - HistogramProtoToHistogram(prompb.Histogram{}) - HistogramProtoToFloatHistogram(prompb.Histogram{}) -} +func TestDecodeWriteV2Request(t *testing.T) { + buf, _, _, err := buildV2WriteRequest(log.NewNopLogger(), writeV2RequestFixture.Timeseries, writeV2RequestFixture.Symbols, nil, nil, nil, "snappy") + require.NoError(t, err) -func exampleHistogram() histogram.Histogram { - return histogram.Histogram{ - CounterResetHint: histogram.GaugeType, - Schema: 0, - Count: 19, - Sum: 2.7, - PositiveSpans: []histogram.Span{ - {Offset: 0, Length: 4}, - {Offset: 0, Length: 0}, - {Offset: 0, Length: 3}, - }, - PositiveBuckets: []int64{1, 2, -2, 1, -1, 0, 0}, - NegativeSpans: []histogram.Span{ - {Offset: 0, Length: 5}, - {Offset: 1, Length: 0}, - {Offset: 0, Length: 1}, - }, - NegativeBuckets: []int64{1, 2, -2, 1, -1, 0}, - } -} - -func exampleHistogramProto() prompb.Histogram { - return prompb.Histogram{ - Count: &prompb.Histogram_CountInt{CountInt: 19}, - Sum: 2.7, - Schema: 0, - ZeroThreshold: 0, - ZeroCount: &prompb.Histogram_ZeroCountInt{ZeroCountInt: 0}, - NegativeSpans: []prompb.BucketSpan{ - { - Offset: 0, - Length: 5, - }, - { - Offset: 1, - Length: 0, - }, - { - Offset: 0, - Length: 1, - }, - }, - NegativeDeltas: []int64{1, 2, -2, 1, -1, 0}, - PositiveSpans: []prompb.BucketSpan{ - { - Offset: 0, - Length: 4, - }, - { - Offset: 0, - Length: 0, - }, - { - Offset: 0, - Length: 3, - }, - }, - PositiveDeltas: []int64{1, 2, -2, 1, -1, 0, 0}, - ResetHint: prompb.Histogram_GAUGE, - Timestamp: 1337, - } -} - -func TestHistogramToProtoConvert(t *testing.T) { - tests := []struct { - input histogram.CounterResetHint - expected prompb.Histogram_ResetHint - }{ - { - input: histogram.UnknownCounterReset, - expected: prompb.Histogram_UNKNOWN, - }, - { - input: histogram.CounterReset, - expected: prompb.Histogram_YES, - }, - { - input: histogram.NotCounterReset, - expected: prompb.Histogram_NO, - }, - { - input: histogram.GaugeType, - expected: prompb.Histogram_GAUGE, - }, - } - - for _, test := range tests { - h := exampleHistogram() - h.CounterResetHint = test.input - p := exampleHistogramProto() - p.ResetHint = test.expected - - require.Equal(t, p, HistogramToHistogramProto(1337, &h)) - - require.Equal(t, h, *HistogramProtoToHistogram(p)) - } -} - -func exampleFloatHistogram() histogram.FloatHistogram { - return histogram.FloatHistogram{ - CounterResetHint: histogram.GaugeType, - Schema: 0, - Count: 19, - Sum: 2.7, - PositiveSpans: []histogram.Span{ - {Offset: 0, Length: 4}, - {Offset: 0, Length: 0}, - {Offset: 0, Length: 3}, - }, - PositiveBuckets: []float64{1, 2, -2, 1, -1, 0, 0}, - NegativeSpans: []histogram.Span{ - {Offset: 0, Length: 5}, - {Offset: 1, Length: 0}, - {Offset: 0, Length: 1}, - }, - NegativeBuckets: []float64{1, 2, -2, 1, -1, 0}, - } -} - -func exampleFloatHistogramProto() prompb.Histogram { - return prompb.Histogram{ - Count: &prompb.Histogram_CountFloat{CountFloat: 19}, - Sum: 2.7, - Schema: 0, - ZeroThreshold: 0, - ZeroCount: &prompb.Histogram_ZeroCountFloat{ZeroCountFloat: 0}, - NegativeSpans: []prompb.BucketSpan{ - { - Offset: 0, - Length: 5, - }, - { - Offset: 1, - Length: 0, - }, - { - Offset: 0, - Length: 1, - }, - }, - NegativeCounts: []float64{1, 2, -2, 1, -1, 0}, - PositiveSpans: []prompb.BucketSpan{ - { - Offset: 0, - Length: 4, - }, - { - Offset: 0, - Length: 0, - }, - { - Offset: 0, - Length: 3, - }, - }, - PositiveCounts: []float64{1, 2, -2, 1, -1, 0, 0}, - ResetHint: prompb.Histogram_GAUGE, - Timestamp: 1337, - } -} - -func TestFloatHistogramToProtoConvert(t *testing.T) { - tests := []struct { - input histogram.CounterResetHint - expected prompb.Histogram_ResetHint - }{ - { - input: histogram.UnknownCounterReset, - expected: prompb.Histogram_UNKNOWN, - }, - { - input: histogram.CounterReset, - expected: prompb.Histogram_YES, - }, - { - input: histogram.NotCounterReset, - expected: prompb.Histogram_NO, - }, - { - input: histogram.GaugeType, - expected: prompb.Histogram_GAUGE, - }, - } - - for _, test := range tests { - h := exampleFloatHistogram() - h.CounterResetHint = test.input - p := exampleFloatHistogramProto() - p.ResetHint = test.expected - - require.Equal(t, p, FloatHistogramToHistogramProto(1337, &h)) - - require.Equal(t, h, *FloatHistogramProtoToFloatHistogram(p)) - } + actual, err := DecodeWriteV2Request(bytes.NewReader(buf)) + require.NoError(t, err) + require.Equal(t, writeV2RequestFixture, actual) } func TestStreamResponse(t *testing.T) { - lbs1 := LabelsToLabelsProto(labels.FromStrings("instance", "localhost1", "job", "demo1"), nil) - lbs2 := LabelsToLabelsProto(labels.FromStrings("instance", "localhost2", "job", "demo2"), nil) + lbs1 := prompb.FromLabels(labels.FromStrings("instance", "localhost1", "job", "demo1"), nil) + lbs2 := prompb.FromLabels(labels.FromStrings("instance", "localhost2", "job", "demo2"), nil) chunk := prompb.Chunk{ Type: prompb.Chunk_XOR, Data: make([]byte, 100), @@ -802,7 +628,7 @@ func (c *mockChunkSeriesSet) Next() bool { func (c *mockChunkSeriesSet) At() storage.ChunkSeries { return &storage.ChunkSeriesEntry{ - Lset: LabelProtosToLabels(&c.builder, c.chunkedSeries[c.index].Labels), + Lset: c.chunkedSeries[c.index].ToLabels(&c.builder, nil), ChunkIteratorFn: func(chunks.Iterator) chunks.Iterator { return &mockChunkIterator{ chunks: c.chunkedSeries[c.index].Chunks, diff --git a/storage/remote/metadata_watcher.go b/storage/remote/metadata_watcher.go index abfea3c7b0..fdcd668f56 100644 --- a/storage/remote/metadata_watcher.go +++ b/storage/remote/metadata_watcher.go @@ -27,7 +27,7 @@ import ( // MetadataAppender is an interface used by the Metadata Watcher to send metadata, It is read from the scrape manager, on to somewhere else. type MetadataAppender interface { - AppendMetadata(context.Context, []scrape.MetricMetadata) + AppendWatcherMetadata(context.Context, []scrape.MetricMetadata) } // Watchable represents from where we fetch active targets for metadata. @@ -146,7 +146,7 @@ func (mw *MetadataWatcher) collect() { } // Blocks until the metadata is sent to the remote write endpoint or hardShutdownContext is expired. - mw.writer.AppendMetadata(mw.hardShutdownCtx, metadata) + mw.writer.AppendWatcherMetadata(mw.hardShutdownCtx, metadata) } func (mw *MetadataWatcher) ready() bool { diff --git a/storage/remote/metadata_watcher_test.go b/storage/remote/metadata_watcher_test.go index 0cd6027a83..ce9b9d022e 100644 --- a/storage/remote/metadata_watcher_test.go +++ b/storage/remote/metadata_watcher_test.go @@ -57,7 +57,7 @@ type writeMetadataToMock struct { metadataAppended int } -func (mwtm *writeMetadataToMock) AppendMetadata(_ context.Context, m []scrape.MetricMetadata) { +func (mwtm *writeMetadataToMock) AppendWatcherMetadata(_ context.Context, m []scrape.MetricMetadata) { mwtm.metadataAppended += len(m) } diff --git a/storage/remote/otlptranslator/prometheus/normalize_name.go b/storage/remote/otlptranslator/prometheus/normalize_name.go index 4cf36671aa..71bba40e48 100644 --- a/storage/remote/otlptranslator/prometheus/normalize_name.go +++ b/storage/remote/otlptranslator/prometheus/normalize_name.go @@ -29,7 +29,6 @@ import ( // Prometheus best practices for units: https://prometheus.io/docs/practices/naming/#base-units // OpenMetrics specification for units: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#units-and-base-units var unitMap = map[string]string{ - // Time "d": "days", "h": "hours", @@ -111,7 +110,6 @@ func BuildCompliantName(metric pmetric.Metric, namespace string, addMetricSuffix // Build a normalized name for the specified metric func normalizeName(metric pmetric.Metric, namespace string) string { - // Split metric name in "tokens" (remove all non-alphanumeric) nameTokens := strings.FieldsFunc( metric.Name(), diff --git a/storage/remote/otlptranslator/prometheus/unit_to_ucum.go b/storage/remote/otlptranslator/prometheus/unit_to_ucum.go index 1f8bf1a638..39a42734d7 100644 --- a/storage/remote/otlptranslator/prometheus/unit_to_ucum.go +++ b/storage/remote/otlptranslator/prometheus/unit_to_ucum.go @@ -19,7 +19,6 @@ package prometheus import "strings" var wordToUCUM = map[string]string{ - // Time "days": "d", "hours": "h", diff --git a/storage/remote/queue_manager.go b/storage/remote/queue_manager.go index dde78d35e5..fb13da70da 100644 --- a/storage/remote/queue_manager.go +++ b/storage/remote/queue_manager.go @@ -36,9 +36,11 @@ import ( "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/metadata" "github.com/prometheus/prometheus/model/relabel" "github.com/prometheus/prometheus/model/timestamp" "github.com/prometheus/prometheus/prompb" + writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2" "github.com/prometheus/prometheus/scrape" "github.com/prometheus/prometheus/tsdb/chunks" "github.com/prometheus/prometheus/tsdb/record" @@ -389,7 +391,7 @@ func (m *queueManagerMetrics) unregister() { // external timeseries database. type WriteClient interface { // Store stores the given samples in the remote storage. - Store(context.Context, []byte, int) error + Store(ctx context.Context, req []byte, retryAttempt int) error // Name uniquely identifies the remote storage. Name() string // Endpoint is the remote read or write endpoint for the storage client. @@ -418,11 +420,14 @@ type QueueManager struct { clientMtx sync.RWMutex storeClient WriteClient + protoMsg config.RemoteWriteProtoMsg + enc Compression - seriesMtx sync.Mutex // Covers seriesLabels, droppedSeries and builder. - seriesLabels map[chunks.HeadSeriesRef]labels.Labels - droppedSeries map[chunks.HeadSeriesRef]struct{} - builder *labels.Builder + seriesMtx sync.Mutex // Covers seriesLabels, seriesMetadata, droppedSeries and builder. + seriesLabels map[chunks.HeadSeriesRef]labels.Labels + seriesMetadata map[chunks.HeadSeriesRef]*metadata.Metadata + droppedSeries map[chunks.HeadSeriesRef]struct{} + builder *labels.Builder seriesSegmentMtx sync.Mutex // Covers seriesSegmentIndexes - if you also lock seriesMtx, take seriesMtx first. seriesSegmentIndexes map[chunks.HeadSeriesRef]int @@ -463,6 +468,7 @@ func NewQueueManager( sm ReadyScrapeManager, enableExemplarRemoteWrite bool, enableNativeHistogramRemoteWrite bool, + protoMsg config.RemoteWriteProtoMsg, ) *QueueManager { if logger == nil { logger = log.NewNopLogger() @@ -487,6 +493,7 @@ func NewQueueManager( sendNativeHistograms: enableNativeHistogramRemoteWrite, seriesLabels: make(map[chunks.HeadSeriesRef]labels.Labels), + seriesMetadata: make(map[chunks.HeadSeriesRef]*metadata.Metadata), seriesSegmentIndexes: make(map[chunks.HeadSeriesRef]int), droppedSeries: make(map[chunks.HeadSeriesRef]struct{}), builder: labels.NewBuilder(labels.EmptyLabels()), @@ -503,9 +510,26 @@ func NewQueueManager( metrics: metrics, interner: interner, highestRecvTimestamp: highestRecvTimestamp, + + protoMsg: protoMsg, + enc: SnappyBlockCompression, // Hardcoded for now, but scaffolding exists for likely future use. + } + + walMetadata := false + if t.protoMsg != config.RemoteWriteProtoMsgV1 { + walMetadata = true + } + t.watcher = wlog.NewWatcher(watcherMetrics, readerMetrics, logger, client.Name(), t, dir, enableExemplarRemoteWrite, enableNativeHistogramRemoteWrite, walMetadata) + + // The current MetadataWatcher implementation is mutually exclusive + // with the new approach, which stores metadata as WAL records and + // ships them alongside series. If both mechanisms are set, the new one + // takes precedence by implicitly disabling the older one. + if t.mcfg.Send && t.protoMsg != config.RemoteWriteProtoMsgV1 { + level.Warn(logger).Log("msg", "usage of 'metadata_config.send' is redundant when using remote write v2 (or higher) as metadata will always be gathered from the WAL and included for every series within each write request") + t.mcfg.Send = false } - t.watcher = wlog.NewWatcher(watcherMetrics, readerMetrics, logger, client.Name(), t, dir, enableExemplarRemoteWrite, enableNativeHistogramRemoteWrite) if t.mcfg.Send { t.metadataWatcher = NewMetadataWatcher(logger, sm, client.Name(), t, t.mcfg.SendInterval, flushDeadline) } @@ -514,14 +538,21 @@ func NewQueueManager( return t } -// AppendMetadata sends metadata to the remote storage. Metadata is sent in batches, but is not parallelized. -func (t *QueueManager) AppendMetadata(ctx context.Context, metadata []scrape.MetricMetadata) { +// AppendWatcherMetadata sends metadata to the remote storage. Metadata is sent in batches, but is not parallelized. +// This is only used for the metadata_config.send setting and 1.x Remote Write. +func (t *QueueManager) AppendWatcherMetadata(ctx context.Context, metadata []scrape.MetricMetadata) { + // no op for any newer proto format, which will cache metadata sent to it from the WAL watcher. + if t.protoMsg != config.RemoteWriteProtoMsgV1 { + return + } + + // 1.X will still get metadata in batches. mm := make([]prompb.MetricMetadata, 0, len(metadata)) for _, entry := range metadata { mm = append(mm, prompb.MetricMetadata{ MetricFamilyName: entry.Metric, Help: entry.Help, - Type: metricTypeToMetricTypeProto(entry.Type), + Type: prompb.FromMetadataType(entry.Type), Unit: entry.Unit, }) } @@ -542,8 +573,8 @@ func (t *QueueManager) AppendMetadata(ctx context.Context, metadata []scrape.Met } func (t *QueueManager) sendMetadataWithBackoff(ctx context.Context, metadata []prompb.MetricMetadata, pBuf *proto.Buffer) error { - // Build the WriteRequest with no samples. - req, _, _, err := buildWriteRequest(t.logger, nil, metadata, pBuf, nil, nil) + // Build the WriteRequest with no samples (v1 flow). + req, _, _, err := buildWriteRequest(t.logger, nil, metadata, pBuf, nil, nil, t.enc) if err != nil { return err } @@ -629,6 +660,36 @@ func isTimeSeriesOldFilter(metrics *queueManagerMetrics, baseTime time.Time, sam } } +func isV2TimeSeriesOldFilter(metrics *queueManagerMetrics, baseTime time.Time, sampleAgeLimit time.Duration) func(ts writev2.TimeSeries) bool { + return func(ts writev2.TimeSeries) bool { + if sampleAgeLimit == 0 { + // If sampleAgeLimit is unset, then we never skip samples due to their age. + return false + } + switch { + // Only the first element should be set in the series, therefore we only check the first element. + case len(ts.Samples) > 0: + if isSampleOld(baseTime, sampleAgeLimit, ts.Samples[0].Timestamp) { + metrics.droppedSamplesTotal.WithLabelValues(reasonTooOld).Inc() + return true + } + case len(ts.Histograms) > 0: + if isSampleOld(baseTime, sampleAgeLimit, ts.Histograms[0].Timestamp) { + metrics.droppedHistogramsTotal.WithLabelValues(reasonTooOld).Inc() + return true + } + case len(ts.Exemplars) > 0: + if isSampleOld(baseTime, sampleAgeLimit, ts.Exemplars[0].Timestamp) { + metrics.droppedExemplarsTotal.WithLabelValues(reasonTooOld).Inc() + return true + } + default: + return false + } + return false + } +} + // Append queues a sample to be sent to the remote storage. Blocks until all samples are // enqueued on their shards or a shutdown signal is received. func (t *QueueManager) Append(samples []record.RefSample) bool { @@ -652,6 +713,9 @@ outer: t.seriesMtx.Unlock() continue } + // TODO(cstyan): Handle or at least log an error if no metadata is found. + // See https://github.com/prometheus/prometheus/issues/14405 + meta := t.seriesMetadata[s.Ref] t.seriesMtx.Unlock() // Start with a very small backoff. This should not be t.cfg.MinBackoff // as it can happen without errors, and we want to pickup work after @@ -666,6 +730,7 @@ outer: } if t.shards.enqueue(s.Ref, timeSeries{ seriesLabels: lbls, + metadata: meta, timestamp: s.T, value: s.V, sType: tSample, @@ -711,6 +776,7 @@ outer: t.seriesMtx.Unlock() continue } + meta := t.seriesMetadata[e.Ref] t.seriesMtx.Unlock() // This will only loop if the queues are being resharded. backoff := t.cfg.MinBackoff @@ -722,6 +788,7 @@ outer: } if t.shards.enqueue(e.Ref, timeSeries{ seriesLabels: lbls, + metadata: meta, timestamp: e.T, value: e.V, exemplarLabels: e.Labels, @@ -765,6 +832,7 @@ outer: t.seriesMtx.Unlock() continue } + meta := t.seriesMetadata[h.Ref] t.seriesMtx.Unlock() backoff := model.Duration(5 * time.Millisecond) @@ -776,6 +844,7 @@ outer: } if t.shards.enqueue(h.Ref, timeSeries{ seriesLabels: lbls, + metadata: meta, timestamp: h.T, histogram: h.H, sType: tHistogram, @@ -818,6 +887,7 @@ outer: t.seriesMtx.Unlock() continue } + meta := t.seriesMetadata[h.Ref] t.seriesMtx.Unlock() backoff := model.Duration(5 * time.Millisecond) @@ -829,6 +899,7 @@ outer: } if t.shards.enqueue(h.Ref, timeSeries{ seriesLabels: lbls, + metadata: meta, timestamp: h.T, floatHistogram: h.FH, sType: tFloatHistogram, @@ -925,6 +996,23 @@ func (t *QueueManager) StoreSeries(series []record.RefSeries, index int) { } } +// StoreMetadata keeps track of known series' metadata for lookups when sending samples to remote. +func (t *QueueManager) StoreMetadata(meta []record.RefMetadata) { + if t.protoMsg == config.RemoteWriteProtoMsgV1 { + return + } + + t.seriesMtx.Lock() + defer t.seriesMtx.Unlock() + for _, m := range meta { + t.seriesMetadata[m.Ref] = &metadata.Metadata{ + Type: record.ToMetricType(m.Type), + Unit: m.Unit, + Help: m.Help, + } + } +} + // UpdateSeriesSegment updates the segment number held against the series, // so we can trim older ones in SeriesReset. func (t *QueueManager) UpdateSeriesSegment(series []record.RefSeries, index int) { @@ -950,6 +1038,7 @@ func (t *QueueManager) SeriesReset(index int) { delete(t.seriesSegmentIndexes, k) t.releaseLabels(t.seriesLabels[k]) delete(t.seriesLabels, k) + delete(t.seriesMetadata, k) delete(t.droppedSeries, k) } } @@ -1165,6 +1254,7 @@ type shards struct { samplesDroppedOnHardShutdown atomic.Uint32 exemplarsDroppedOnHardShutdown atomic.Uint32 histogramsDroppedOnHardShutdown atomic.Uint32 + metadataDroppedOnHardShutdown atomic.Uint32 } // start the shards; must be called before any call to enqueue. @@ -1193,6 +1283,7 @@ func (s *shards) start(n int) { s.samplesDroppedOnHardShutdown.Store(0) s.exemplarsDroppedOnHardShutdown.Store(0) s.histogramsDroppedOnHardShutdown.Store(0) + s.metadataDroppedOnHardShutdown.Store(0) for i := 0; i < n; i++ { go s.runShard(hardShutdownCtx, i, newQueues[i]) } @@ -1245,7 +1336,6 @@ func (s *shards) stop() { func (s *shards) enqueue(ref chunks.HeadSeriesRef, data timeSeries) bool { s.mtx.RLock() defer s.mtx.RUnlock() - shard := uint64(ref) % uint64(len(s.queues)) select { case <-s.softShutdown: @@ -1288,6 +1378,7 @@ type timeSeries struct { value float64 histogram *histogram.Histogram floatHistogram *histogram.FloatHistogram + metadata *metadata.Metadata timestamp int64 exemplarLabels labels.Labels // The type of series: sample, exemplar, or histogram. @@ -1301,6 +1392,7 @@ const ( tExemplar tHistogram tFloatHistogram + tMetadata ) func newQueue(batchSize, capacity int) *queue { @@ -1324,6 +1416,10 @@ func newQueue(batchSize, capacity int) *queue { func (q *queue) Append(datum timeSeries) bool { q.batchMtx.Lock() defer q.batchMtx.Unlock() + // TODO(cstyan): Check if metadata now means we've reduced the total # of samples + // we can batch together here, and if so find a way to not include metadata + // in the batch size calculation. + // See https://github.com/prometheus/prometheus/issues/14405 q.batch = append(q.batch, datum) if len(q.batch) == cap(q.batch) { select { @@ -1347,7 +1443,6 @@ func (q *queue) Chan() <-chan []timeSeries { func (q *queue) Batch() []timeSeries { q.batchMtx.Lock() defer q.batchMtx.Unlock() - select { case batch := <-q.batchQueue: return batch @@ -1419,19 +1514,23 @@ func (s *shards) runShard(ctx context.Context, shardID int, queue *queue) { }() shardNum := strconv.Itoa(shardID) + symbolTable := writev2.NewSymbolTable() // Send batches of at most MaxSamplesPerSend samples to the remote storage. // If we have fewer samples than that, flush them out after a deadline anyways. var ( max = s.qm.cfg.MaxSamplesPerSend - pBuf = proto.NewBuffer(nil) - buf []byte + pBuf = proto.NewBuffer(nil) + pBufRaw []byte + buf []byte ) + // TODO(@tpaschalis) Should we also raise the max if we have WAL metadata? if s.qm.sendExemplars { max += int(float64(max) * 0.1) } + // TODO: Dry all of this, we should make an interface/generic for the timeseries type. batchQueue := queue.Chan() pendingData := make([]prompb.TimeSeries, max) for i := range pendingData { @@ -1440,6 +1539,10 @@ func (s *shards) runShard(ctx context.Context, shardID int, queue *queue) { pendingData[i].Exemplars = []prompb.Exemplar{{}} } } + pendingDataV2 := make([]writev2.TimeSeries, max) + for i := range pendingDataV2 { + pendingDataV2[i].Samples = []writev2.Sample{{}} + } timer := time.NewTimer(time.Duration(s.qm.cfg.BatchSendDeadline)) stop := func() { @@ -1452,6 +1555,24 @@ func (s *shards) runShard(ctx context.Context, shardID int, queue *queue) { } defer stop() + sendBatch := func(batch []timeSeries, protoMsg config.RemoteWriteProtoMsg, enc Compression, timer bool) { + switch protoMsg { + case config.RemoteWriteProtoMsgV1: + nPendingSamples, nPendingExemplars, nPendingHistograms := populateTimeSeries(batch, pendingData, s.qm.sendExemplars, s.qm.sendNativeHistograms) + n := nPendingSamples + nPendingExemplars + nPendingHistograms + if timer { + level.Debug(s.qm.logger).Log("msg", "runShard timer ticked, sending buffered data", "samples", nPendingSamples, + "exemplars", nPendingExemplars, "shard", shardNum, "histograms", nPendingHistograms) + } + _ = s.sendSamples(ctx, pendingData[:n], nPendingSamples, nPendingExemplars, nPendingHistograms, pBuf, &buf, enc) + case config.RemoteWriteProtoMsgV2: + nPendingSamples, nPendingExemplars, nPendingHistograms, nPendingMetadata := populateV2TimeSeries(&symbolTable, batch, pendingDataV2, s.qm.sendExemplars, s.qm.sendNativeHistograms) + n := nPendingSamples + nPendingExemplars + nPendingHistograms + _ = s.sendV2Samples(ctx, pendingDataV2[:n], symbolTable.Symbols(), nPendingSamples, nPendingExemplars, nPendingHistograms, nPendingMetadata, &pBufRaw, &buf, enc) + symbolTable.Reset() + } + } + for { select { case <-ctx.Done(): @@ -1475,10 +1596,11 @@ func (s *shards) runShard(ctx context.Context, shardID int, queue *queue) { if !ok { return } - nPendingSamples, nPendingExemplars, nPendingHistograms := s.populateTimeSeries(batch, pendingData) + + sendBatch(batch, s.qm.protoMsg, s.qm.enc, false) + // TODO(bwplotka): Previously the return was between popular and send. + // Consider this when DRY-ing https://github.com/prometheus/prometheus/issues/14409 queue.ReturnForReuse(batch) - n := nPendingSamples + nPendingExemplars + nPendingHistograms - s.sendSamples(ctx, pendingData[:n], nPendingSamples, nPendingExemplars, nPendingHistograms, pBuf, &buf) stop() timer.Reset(time.Duration(s.qm.cfg.BatchSendDeadline)) @@ -1486,11 +1608,7 @@ func (s *shards) runShard(ctx context.Context, shardID int, queue *queue) { case <-timer.C: batch := queue.Batch() if len(batch) > 0 { - nPendingSamples, nPendingExemplars, nPendingHistograms := s.populateTimeSeries(batch, pendingData) - n := nPendingSamples + nPendingExemplars + nPendingHistograms - level.Debug(s.qm.logger).Log("msg", "runShard timer ticked, sending buffered data", "samples", nPendingSamples, - "exemplars", nPendingExemplars, "shard", shardNum, "histograms", nPendingHistograms) - s.sendSamples(ctx, pendingData[:n], nPendingSamples, nPendingExemplars, nPendingHistograms, pBuf, &buf) + sendBatch(batch, s.qm.protoMsg, s.qm.enc, true) } queue.ReturnForReuse(batch) timer.Reset(time.Duration(s.qm.cfg.BatchSendDeadline)) @@ -1498,21 +1616,22 @@ func (s *shards) runShard(ctx context.Context, shardID int, queue *queue) { } } -func (s *shards) populateTimeSeries(batch []timeSeries, pendingData []prompb.TimeSeries) (int, int, int) { +func populateTimeSeries(batch []timeSeries, pendingData []prompb.TimeSeries, sendExemplars, sendNativeHistograms bool) (int, int, int) { var nPendingSamples, nPendingExemplars, nPendingHistograms int for nPending, d := range batch { pendingData[nPending].Samples = pendingData[nPending].Samples[:0] - if s.qm.sendExemplars { + if sendExemplars { pendingData[nPending].Exemplars = pendingData[nPending].Exemplars[:0] } - if s.qm.sendNativeHistograms { + if sendNativeHistograms { pendingData[nPending].Histograms = pendingData[nPending].Histograms[:0] } // Number of pending samples is limited by the fact that sendSamples (via sendSamplesWithBackoff) // retries endlessly, so once we reach max samples, if we can never send to the endpoint we'll // stop reading from the queue. This makes it safe to reference pendingSamples by index. - pendingData[nPending].Labels = LabelsToLabelsProto(d.seriesLabels, pendingData[nPending].Labels) + pendingData[nPending].Labels = prompb.FromLabels(d.seriesLabels, pendingData[nPending].Labels) + switch d.sType { case tSample: pendingData[nPending].Samples = append(pendingData[nPending].Samples, prompb.Sample{ @@ -1522,25 +1641,39 @@ func (s *shards) populateTimeSeries(batch []timeSeries, pendingData []prompb.Tim nPendingSamples++ case tExemplar: pendingData[nPending].Exemplars = append(pendingData[nPending].Exemplars, prompb.Exemplar{ - Labels: LabelsToLabelsProto(d.exemplarLabels, nil), + Labels: prompb.FromLabels(d.exemplarLabels, nil), Value: d.value, Timestamp: d.timestamp, }) nPendingExemplars++ case tHistogram: - pendingData[nPending].Histograms = append(pendingData[nPending].Histograms, HistogramToHistogramProto(d.timestamp, d.histogram)) + pendingData[nPending].Histograms = append(pendingData[nPending].Histograms, prompb.FromIntHistogram(d.timestamp, d.histogram)) nPendingHistograms++ case tFloatHistogram: - pendingData[nPending].Histograms = append(pendingData[nPending].Histograms, FloatHistogramToHistogramProto(d.timestamp, d.floatHistogram)) + pendingData[nPending].Histograms = append(pendingData[nPending].Histograms, prompb.FromFloatHistogram(d.timestamp, d.floatHistogram)) nPendingHistograms++ } } return nPendingSamples, nPendingExemplars, nPendingHistograms } -func (s *shards) sendSamples(ctx context.Context, samples []prompb.TimeSeries, sampleCount, exemplarCount, histogramCount int, pBuf *proto.Buffer, buf *[]byte) { +func (s *shards) sendSamples(ctx context.Context, samples []prompb.TimeSeries, sampleCount, exemplarCount, histogramCount int, pBuf *proto.Buffer, buf *[]byte, enc Compression) error { begin := time.Now() - err := s.sendSamplesWithBackoff(ctx, samples, sampleCount, exemplarCount, histogramCount, pBuf, buf) + err := s.sendSamplesWithBackoff(ctx, samples, sampleCount, exemplarCount, histogramCount, 0, pBuf, buf, enc) + s.updateMetrics(ctx, err, sampleCount, exemplarCount, histogramCount, 0, time.Since(begin)) + return err +} + +// TODO(bwplotka): DRY this (have one logic for both v1 and v2). +// See https://github.com/prometheus/prometheus/issues/14409 +func (s *shards) sendV2Samples(ctx context.Context, samples []writev2.TimeSeries, labels []string, sampleCount, exemplarCount, histogramCount, metadataCount int, pBuf, buf *[]byte, enc Compression) error { + begin := time.Now() + err := s.sendV2SamplesWithBackoff(ctx, samples, labels, sampleCount, exemplarCount, histogramCount, metadataCount, pBuf, buf, enc) + s.updateMetrics(ctx, err, sampleCount, exemplarCount, histogramCount, metadataCount, time.Since(begin)) + return err +} + +func (s *shards) updateMetrics(_ context.Context, err error, sampleCount, exemplarCount, histogramCount, metadataCount int, duration time.Duration) { if err != nil { level.Error(s.qm.logger).Log("msg", "non-recoverable error", "count", sampleCount, "exemplarCount", exemplarCount, "histogramCount", histogramCount, "err", err) s.qm.metrics.failedSamplesTotal.Add(float64(sampleCount)) @@ -1550,8 +1683,8 @@ func (s *shards) sendSamples(ctx context.Context, samples []prompb.TimeSeries, s // These counters are used to calculate the dynamic sharding, and as such // should be maintained irrespective of success or failure. - s.qm.dataOut.incr(int64(len(samples))) - s.qm.dataOutDuration.incr(int64(time.Since(begin))) + s.qm.dataOut.incr(int64(sampleCount + exemplarCount + histogramCount + metadataCount)) + s.qm.dataOutDuration.incr(int64(duration)) s.qm.lastSendTimestamp.Store(time.Now().Unix()) // Pending samples/exemplars/histograms also should be subtracted, as an error means // they will not be retried. @@ -1564,9 +1697,9 @@ func (s *shards) sendSamples(ctx context.Context, samples []prompb.TimeSeries, s } // sendSamples to the remote storage with backoff for recoverable errors. -func (s *shards) sendSamplesWithBackoff(ctx context.Context, samples []prompb.TimeSeries, sampleCount, exemplarCount, histogramCount int, pBuf *proto.Buffer, buf *[]byte) error { +func (s *shards) sendSamplesWithBackoff(ctx context.Context, samples []prompb.TimeSeries, sampleCount, exemplarCount, histogramCount, metadataCount int, pBuf *proto.Buffer, buf *[]byte, enc Compression) error { // Build the WriteRequest with no metadata. - req, highest, lowest, err := buildWriteRequest(s.qm.logger, samples, nil, pBuf, *buf, nil) + req, highest, lowest, err := buildWriteRequest(s.qm.logger, samples, nil, pBuf, buf, nil, enc) s.qm.buildRequestLimitTimestamp.Store(lowest) if err != nil { // Failing to build the write request is non-recoverable, since it will @@ -1590,8 +1723,9 @@ func (s *shards) sendSamplesWithBackoff(ctx context.Context, samples []prompb.Ti samples, nil, pBuf, - *buf, + buf, isTimeSeriesOldFilter(s.qm.metrics, currentTime, time.Duration(s.qm.cfg.SampleAgeLimit)), + enc, ) s.qm.buildRequestLimitTimestamp.Store(lowest) if err != nil { @@ -1622,6 +1756,7 @@ func (s *shards) sendSamplesWithBackoff(ctx context.Context, samples []prompb.Ti s.qm.metrics.samplesTotal.Add(float64(sampleCount)) s.qm.metrics.exemplarsTotal.Add(float64(exemplarCount)) s.qm.metrics.histogramsTotal.Add(float64(histogramCount)) + s.qm.metrics.metadataTotal.Add(float64(metadataCount)) err := s.qm.client().Store(ctx, *buf, try) s.qm.metrics.sentBatchDuration.Observe(time.Since(begin).Seconds()) @@ -1652,6 +1787,148 @@ func (s *shards) sendSamplesWithBackoff(ctx context.Context, samples []prompb.Ti return err } +// sendV2Samples to the remote storage with backoff for recoverable errors. +func (s *shards) sendV2SamplesWithBackoff(ctx context.Context, samples []writev2.TimeSeries, labels []string, sampleCount, exemplarCount, histogramCount, metadataCount int, pBuf, buf *[]byte, enc Compression) error { + // Build the WriteRequest with no metadata. + req, highest, lowest, err := buildV2WriteRequest(s.qm.logger, samples, labels, pBuf, buf, nil, enc) + s.qm.buildRequestLimitTimestamp.Store(lowest) + if err != nil { + // Failing to build the write request is non-recoverable, since it will + // only error if marshaling the proto to bytes fails. + return err + } + + reqSize := len(req) + *buf = req + + // An anonymous function allows us to defer the completion of our per-try spans + // without causing a memory leak, and it has the nice effect of not propagating any + // parameters for sendSamplesWithBackoff/3. + attemptStore := func(try int) error { + currentTime := time.Now() + lowest := s.qm.buildRequestLimitTimestamp.Load() + if isSampleOld(currentTime, time.Duration(s.qm.cfg.SampleAgeLimit), lowest) { + // This will filter out old samples during retries. + req, _, lowest, err := buildV2WriteRequest( + s.qm.logger, + samples, + labels, + pBuf, + buf, + isV2TimeSeriesOldFilter(s.qm.metrics, currentTime, time.Duration(s.qm.cfg.SampleAgeLimit)), + enc, + ) + s.qm.buildRequestLimitTimestamp.Store(lowest) + if err != nil { + return err + } + *buf = req + } + + ctx, span := otel.Tracer("").Start(ctx, "Remote Send Batch") + defer span.End() + + span.SetAttributes( + attribute.Int("request_size", reqSize), + attribute.Int("samples", sampleCount), + attribute.Int("try", try), + attribute.String("remote_name", s.qm.storeClient.Name()), + attribute.String("remote_url", s.qm.storeClient.Endpoint()), + ) + + if exemplarCount > 0 { + span.SetAttributes(attribute.Int("exemplars", exemplarCount)) + } + if histogramCount > 0 { + span.SetAttributes(attribute.Int("histograms", histogramCount)) + } + + begin := time.Now() + s.qm.metrics.samplesTotal.Add(float64(sampleCount)) + s.qm.metrics.exemplarsTotal.Add(float64(exemplarCount)) + s.qm.metrics.histogramsTotal.Add(float64(histogramCount)) + s.qm.metrics.metadataTotal.Add(float64(metadataCount)) + err := s.qm.client().Store(ctx, *buf, try) + s.qm.metrics.sentBatchDuration.Observe(time.Since(begin).Seconds()) + + if err != nil { + span.RecordError(err) + return err + } + + return nil + } + + onRetry := func() { + s.qm.metrics.retriedSamplesTotal.Add(float64(sampleCount)) + s.qm.metrics.retriedExemplarsTotal.Add(float64(exemplarCount)) + s.qm.metrics.retriedHistogramsTotal.Add(float64(histogramCount)) + } + + err = s.qm.sendWriteRequestWithBackoff(ctx, attemptStore, onRetry) + if errors.Is(err, context.Canceled) { + // When there is resharding, we cancel the context for this queue, which means the data is not sent. + // So we exit early to not update the metrics. + return err + } + + s.qm.metrics.sentBytesTotal.Add(float64(reqSize)) + s.qm.metrics.highestSentTimestamp.Set(float64(highest / 1000)) + + return err +} + +func populateV2TimeSeries(symbolTable *writev2.SymbolsTable, batch []timeSeries, pendingData []writev2.TimeSeries, sendExemplars, sendNativeHistograms bool) (int, int, int, int) { + var nPendingSamples, nPendingExemplars, nPendingHistograms, nPendingMetadata int + for nPending, d := range batch { + pendingData[nPending].Samples = pendingData[nPending].Samples[:0] + // todo: should we also safeguard against empty metadata here? + if d.metadata != nil { + pendingData[nPending].Metadata.Type = writev2.FromMetadataType(d.metadata.Type) + pendingData[nPending].Metadata.HelpRef = symbolTable.Symbolize(d.metadata.Help) + pendingData[nPending].Metadata.HelpRef = symbolTable.Symbolize(d.metadata.Unit) + nPendingMetadata++ + } + + if sendExemplars { + pendingData[nPending].Exemplars = pendingData[nPending].Exemplars[:0] + } + if sendNativeHistograms { + pendingData[nPending].Histograms = pendingData[nPending].Histograms[:0] + } + + // Number of pending samples is limited by the fact that sendSamples (via sendSamplesWithBackoff) + // retries endlessly, so once we reach max samples, if we can never send to the endpoint we'll + // stop reading from the queue. This makes it safe to reference pendingSamples by index. + pendingData[nPending].LabelsRefs = symbolTable.SymbolizeLabels(d.seriesLabels, pendingData[nPending].LabelsRefs) + switch d.sType { + case tSample: + pendingData[nPending].Samples = append(pendingData[nPending].Samples, writev2.Sample{ + Value: d.value, + Timestamp: d.timestamp, + }) + nPendingSamples++ + case tExemplar: + pendingData[nPending].Exemplars = append(pendingData[nPending].Exemplars, writev2.Exemplar{ + LabelsRefs: symbolTable.SymbolizeLabels(d.exemplarLabels, nil), // TODO: optimize, reuse slice + Value: d.value, + Timestamp: d.timestamp, + }) + nPendingExemplars++ + case tHistogram: + pendingData[nPending].Histograms = append(pendingData[nPending].Histograms, writev2.FromIntHistogram(d.timestamp, d.histogram)) + nPendingHistograms++ + case tFloatHistogram: + pendingData[nPending].Histograms = append(pendingData[nPending].Histograms, writev2.FromFloatHistogram(d.timestamp, d.floatHistogram)) + nPendingHistograms++ + case tMetadata: + // TODO: log or return an error? + // we shouldn't receive metadata type data here, it should already be inserted into the timeSeries + } + } + return nPendingSamples, nPendingExemplars, nPendingHistograms, nPendingMetadata +} + func (t *QueueManager) sendWriteRequestWithBackoff(ctx context.Context, attempt func(int) error, onRetry func()) error { backoff := t.cfg.MinBackoff sleepDuration := model.Duration(0) @@ -1795,7 +2072,21 @@ func buildTimeSeries(timeSeries []prompb.TimeSeries, filter func(prompb.TimeSeri return highest, lowest, timeSeries, droppedSamples, droppedExemplars, droppedHistograms } -func buildWriteRequest(logger log.Logger, timeSeries []prompb.TimeSeries, metadata []prompb.MetricMetadata, pBuf *proto.Buffer, buf []byte, filter func(prompb.TimeSeries) bool) ([]byte, int64, int64, error) { +func compressPayload(tmpbuf *[]byte, inp []byte, enc Compression) (compressed []byte, _ error) { + switch enc { + case SnappyBlockCompression: + compressed = snappy.Encode(*tmpbuf, inp) + if n := snappy.MaxEncodedLen(len(inp)); n > len(*tmpbuf) { + // grow the buffer for the next time + *tmpbuf = make([]byte, n) + } + return compressed, nil + default: + return compressed, fmt.Errorf("Unknown compression scheme [%v]", enc) + } +} + +func buildWriteRequest(logger log.Logger, timeSeries []prompb.TimeSeries, metadata []prompb.MetricMetadata, pBuf *proto.Buffer, buf *[]byte, filter func(prompb.TimeSeries) bool, enc Compression) (compressed []byte, highest, lowest int64, _ error) { highest, lowest, timeSeries, droppedSamples, droppedExemplars, droppedHistograms := buildTimeSeries(timeSeries, filter) @@ -1821,8 +2112,105 @@ func buildWriteRequest(logger log.Logger, timeSeries []prompb.TimeSeries, metada // snappy uses len() to see if it needs to allocate a new slice. Make the // buffer as long as possible. if buf != nil { - buf = buf[0:cap(buf)] + *buf = (*buf)[0:cap(*buf)] + } else { + buf = &[]byte{} + } + + compressed, err = compressPayload(buf, pBuf.Bytes(), enc) + if err != nil { + return nil, highest, lowest, err } - compressed := snappy.Encode(buf, pBuf.Bytes()) return compressed, highest, lowest, nil } + +func buildV2WriteRequest(logger log.Logger, samples []writev2.TimeSeries, labels []string, pBuf, buf *[]byte, filter func(writev2.TimeSeries) bool, enc Compression) (compressed []byte, highest, lowest int64, _ error) { + highest, lowest, timeSeries, droppedSamples, droppedExemplars, droppedHistograms := buildV2TimeSeries(samples, filter) + + if droppedSamples > 0 || droppedExemplars > 0 || droppedHistograms > 0 { + level.Debug(logger).Log("msg", "dropped data due to their age", "droppedSamples", droppedSamples, "droppedExemplars", droppedExemplars, "droppedHistograms", droppedHistograms) + } + + req := &writev2.Request{ + Symbols: labels, + Timeseries: timeSeries, + } + + if pBuf == nil { + pBuf = &[]byte{} // For convenience in tests. Not efficient. + } + + data, err := req.OptimizedMarshal(*pBuf) + if err != nil { + return nil, highest, lowest, err + } + *pBuf = data + + // snappy uses len() to see if it needs to allocate a new slice. Make the + // buffer as long as possible. + if buf != nil { + *buf = (*buf)[0:cap(*buf)] + } else { + buf = &[]byte{} + } + + compressed, err = compressPayload(buf, data, enc) + if err != nil { + return nil, highest, lowest, err + } + return compressed, highest, lowest, nil +} + +func buildV2TimeSeries(timeSeries []writev2.TimeSeries, filter func(writev2.TimeSeries) bool) (int64, int64, []writev2.TimeSeries, int, int, int) { + var highest int64 + var lowest int64 + var droppedSamples, droppedExemplars, droppedHistograms int + + keepIdx := 0 + lowest = math.MaxInt64 + for i, ts := range timeSeries { + if filter != nil && filter(ts) { + if len(ts.Samples) > 0 { + droppedSamples++ + } + if len(ts.Exemplars) > 0 { + droppedExemplars++ + } + if len(ts.Histograms) > 0 { + droppedHistograms++ + } + continue + } + + // At the moment we only ever append a TimeSeries with a single sample or exemplar in it. + if len(ts.Samples) > 0 && ts.Samples[0].Timestamp > highest { + highest = ts.Samples[0].Timestamp + } + if len(ts.Exemplars) > 0 && ts.Exemplars[0].Timestamp > highest { + highest = ts.Exemplars[0].Timestamp + } + if len(ts.Histograms) > 0 && ts.Histograms[0].Timestamp > highest { + highest = ts.Histograms[0].Timestamp + } + + // Get the lowest timestamp. + if len(ts.Samples) > 0 && ts.Samples[0].Timestamp < lowest { + lowest = ts.Samples[0].Timestamp + } + if len(ts.Exemplars) > 0 && ts.Exemplars[0].Timestamp < lowest { + lowest = ts.Exemplars[0].Timestamp + } + if len(ts.Histograms) > 0 && ts.Histograms[0].Timestamp < lowest { + lowest = ts.Histograms[0].Timestamp + } + if i != keepIdx { + // We have to swap the kept timeseries with the one which should be dropped. + // Copying any elements within timeSeries could cause data corruptions when reusing the slice in a next batch (shards.populateTimeSeries). + timeSeries[keepIdx], timeSeries[i] = timeSeries[i], timeSeries[keepIdx] + } + keepIdx++ + } + + timeSeries = timeSeries[:keepIdx] + return highest, lowest, timeSeries, droppedSamples, droppedExemplars, droppedHistograms +} diff --git a/storage/remote/queue_manager_test.go b/storage/remote/queue_manager_test.go index 4d299994bd..9ab563edab 100644 --- a/storage/remote/queue_manager_test.go +++ b/storage/remote/queue_manager_test.go @@ -15,6 +15,7 @@ package remote import ( "context" + "errors" "fmt" "math" "math/rand" @@ -43,9 +44,11 @@ import ( "github.com/prometheus/prometheus/model/relabel" "github.com/prometheus/prometheus/model/timestamp" "github.com/prometheus/prometheus/prompb" + writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2" "github.com/prometheus/prometheus/scrape" "github.com/prometheus/prometheus/tsdb/chunks" "github.com/prometheus/prometheus/tsdb/record" + "github.com/prometheus/prometheus/util/runutil" "github.com/prometheus/prometheus/util/testutil" ) @@ -62,30 +65,150 @@ func newHighestTimestampMetric() *maxTimestamp { } } -func TestSampleDelivery(t *testing.T) { - testcases := []struct { - name string - samples bool - exemplars bool - histograms bool - floatHistograms bool - }{ - {samples: true, exemplars: false, histograms: false, floatHistograms: false, name: "samples only"}, - {samples: true, exemplars: true, histograms: true, floatHistograms: true, name: "samples, exemplars, and histograms"}, - {samples: false, exemplars: true, histograms: false, floatHistograms: false, name: "exemplars only"}, - {samples: false, exemplars: false, histograms: true, floatHistograms: false, name: "histograms only"}, - {samples: false, exemplars: false, histograms: false, floatHistograms: true, name: "float histograms only"}, +func TestBasicContentNegotiation(t *testing.T) { + queueConfig := config.DefaultQueueConfig + queueConfig.BatchSendDeadline = model.Duration(100 * time.Millisecond) + queueConfig.MaxShards = 1 + + // We need to set URL's so that metric creation doesn't panic. + writeConfig := baseRemoteWriteConfig("http://test-storage.com") + writeConfig.QueueConfig = queueConfig + + conf := &config.Config{ + GlobalConfig: config.DefaultGlobalConfig, + RemoteWriteConfigs: []*config.RemoteWriteConfig{ + writeConfig, + }, } - // Let's create an even number of send batches so we don't run into the + for _, tc := range []struct { + name string + senderProtoMsg config.RemoteWriteProtoMsg + receiverProtoMsg config.RemoteWriteProtoMsg + injectErrs []error + expectFail bool + }{ + { + name: "v2 happy path", + senderProtoMsg: config.RemoteWriteProtoMsgV2, receiverProtoMsg: config.RemoteWriteProtoMsgV2, + injectErrs: []error{nil}, + }, + { + name: "v1 happy path", + senderProtoMsg: config.RemoteWriteProtoMsgV1, receiverProtoMsg: config.RemoteWriteProtoMsgV1, + injectErrs: []error{nil}, + }, + // Test a case where the v1 request has a temporary delay but goes through on retry. + { + name: "v1 happy path with one 5xx retry", + senderProtoMsg: config.RemoteWriteProtoMsgV1, receiverProtoMsg: config.RemoteWriteProtoMsgV1, + injectErrs: []error{RecoverableError{errors.New("pretend 500"), 1}, nil}, + }, + // Repeat the above test but with v2. The request has a temporary delay but goes through on retry. + { + name: "v2 happy path with one 5xx retry", + senderProtoMsg: config.RemoteWriteProtoMsgV2, receiverProtoMsg: config.RemoteWriteProtoMsgV2, + injectErrs: []error{RecoverableError{errors.New("pretend 500"), 1}, nil}, + }, + // A few error cases of v2 talking to v1. + { + name: "v2 talks to v1 that gives 400 or 415", + senderProtoMsg: config.RemoteWriteProtoMsgV2, receiverProtoMsg: config.RemoteWriteProtoMsgV1, + injectErrs: []error{errors.New("pretend unrecoverable err")}, + expectFail: true, + }, + { + name: "v2 talks to v1 that tries to unmarshal v2 payload with v1 proto", + senderProtoMsg: config.RemoteWriteProtoMsgV2, receiverProtoMsg: config.RemoteWriteProtoMsgV1, + injectErrs: []error{nil}, + expectFail: true, // invalid request, no timeseries + }, + // Opposite, v1 talking to v2 only server. + { + name: "v1 talks to v2 that gives 400 or 415", + senderProtoMsg: config.RemoteWriteProtoMsgV1, receiverProtoMsg: config.RemoteWriteProtoMsgV2, + injectErrs: []error{errors.New("pretend unrecoverable err")}, + expectFail: true, + }, + { + name: "v1 talks to (broken) v2 that tries to unmarshal v1 payload with v2 proto", + senderProtoMsg: config.RemoteWriteProtoMsgV1, receiverProtoMsg: config.RemoteWriteProtoMsgV2, + injectErrs: []error{nil}, + expectFail: true, // invalid request, no timeseries + }, + } { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil, true) + defer s.Close() + + var ( + series []record.RefSeries + metadata []record.RefMetadata + samples []record.RefSample + ) + + // Generates same series in both cases. + samples, series = createTimeseries(1, 1) + metadata = createSeriesMetadata(series) + + // Apply new config. + queueConfig.Capacity = len(samples) + queueConfig.MaxSamplesPerSend = len(samples) + // For now we only ever have a single rw config in this test. + conf.RemoteWriteConfigs[0].ProtobufMessage = tc.senderProtoMsg + require.NoError(t, s.ApplyConfig(conf)) + hash, err := toHash(writeConfig) + require.NoError(t, err) + qm := s.rws.queues[hash] + + c := NewTestWriteClient(tc.receiverProtoMsg) + c.injectErrors(tc.injectErrs) + qm.SetClient(c) + + qm.StoreSeries(series, 0) + qm.StoreMetadata(metadata) + + // Do we expect some data back? + if !tc.expectFail { + c.expectSamples(samples, series) + } else { + c.expectSamples(nil, nil) + } + + // Schedule send. + qm.Append(samples) + + if !tc.expectFail { + // No error expected, so wait for data. + c.waitForExpectedData(t, 5*time.Second) + require.Equal(t, 1, c.writesReceived) + require.Equal(t, 0.0, client_testutil.ToFloat64(qm.metrics.failedSamplesTotal)) + } else { + // Wait for failure to be recorded in metrics. + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + require.NoError(t, runutil.Retry(500*time.Millisecond, ctx.Done(), func() error { + if client_testutil.ToFloat64(qm.metrics.failedSamplesTotal) != 1.0 { + return errors.New("expected one sample failed in qm metrics") + } + return nil + })) + require.Equal(t, 0, c.writesReceived) + } + + // samplesTotal means attempts. + require.Equal(t, float64(len(tc.injectErrs)), client_testutil.ToFloat64(qm.metrics.samplesTotal)) + require.Equal(t, float64(len(tc.injectErrs)-1), client_testutil.ToFloat64(qm.metrics.retriedSamplesTotal)) + }) + } +} + +func TestSampleDelivery(t *testing.T) { + // Let's create an even number of send batches, so we don't run into the // batch timeout case. n := 3 - dir := t.TempDir() - - s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil) - defer s.Close() - queueConfig := config.DefaultQueueConfig queueConfig.BatchSendDeadline = model.Duration(100 * time.Millisecond) queueConfig.MaxShards = 1 @@ -102,11 +225,36 @@ func TestSampleDelivery(t *testing.T) { writeConfig, }, } + for _, tc := range []struct { + protoMsg config.RemoteWriteProtoMsg + + name string + samples bool + exemplars bool + histograms bool + floatHistograms bool + }{ + {protoMsg: config.RemoteWriteProtoMsgV1, samples: true, exemplars: false, histograms: false, floatHistograms: false, name: "samples only"}, + {protoMsg: config.RemoteWriteProtoMsgV1, samples: true, exemplars: true, histograms: true, floatHistograms: true, name: "samples, exemplars, and histograms"}, + {protoMsg: config.RemoteWriteProtoMsgV1, samples: false, exemplars: true, histograms: false, floatHistograms: false, name: "exemplars only"}, + {protoMsg: config.RemoteWriteProtoMsgV1, samples: false, exemplars: false, histograms: true, floatHistograms: false, name: "histograms only"}, + {protoMsg: config.RemoteWriteProtoMsgV1, samples: false, exemplars: false, histograms: false, floatHistograms: true, name: "float histograms only"}, + + // TODO(alexg): update some portion of this test to check for the 2.0 metadata + {protoMsg: config.RemoteWriteProtoMsgV2, samples: true, exemplars: false, histograms: false, floatHistograms: false, name: "samples only"}, + {protoMsg: config.RemoteWriteProtoMsgV2, samples: true, exemplars: true, histograms: true, floatHistograms: true, name: "samples, exemplars, and histograms"}, + {protoMsg: config.RemoteWriteProtoMsgV2, samples: false, exemplars: true, histograms: false, floatHistograms: false, name: "exemplars only"}, + {protoMsg: config.RemoteWriteProtoMsgV2, samples: false, exemplars: false, histograms: true, floatHistograms: false, name: "histograms only"}, + {protoMsg: config.RemoteWriteProtoMsgV2, samples: false, exemplars: false, histograms: false, floatHistograms: true, name: "float histograms only"}, + } { + t.Run(fmt.Sprintf("%s-%s", tc.protoMsg, tc.name), func(t *testing.T) { + dir := t.TempDir() + s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil, true) + defer s.Close() - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { var ( series []record.RefSeries + metadata []record.RefMetadata samples []record.RefSample exemplars []record.RefExemplar histograms []record.RefHistogramSample @@ -126,19 +274,23 @@ func TestSampleDelivery(t *testing.T) { if tc.floatHistograms { _, floatHistograms, series = createHistograms(n, n, true) } + metadata = createSeriesMetadata(series) // Apply new config. queueConfig.Capacity = len(samples) queueConfig.MaxSamplesPerSend = len(samples) / 2 + // For now we only ever have a single rw config in this test. + conf.RemoteWriteConfigs[0].ProtobufMessage = tc.protoMsg require.NoError(t, s.ApplyConfig(conf)) hash, err := toHash(writeConfig) require.NoError(t, err) qm := s.rws.queues[hash] - c := NewTestWriteClient() + c := NewTestWriteClient(tc.protoMsg) qm.SetClient(c) qm.StoreSeries(series, 0) + qm.StoreMetadata(metadata) // Send first half of data. c.expectSamples(samples[:len(samples)/2], series) @@ -149,7 +301,7 @@ func TestSampleDelivery(t *testing.T) { qm.AppendExemplars(exemplars[:len(exemplars)/2]) qm.AppendHistograms(histograms[:len(histograms)/2]) qm.AppendFloatHistograms(floatHistograms[:len(floatHistograms)/2]) - c.waitForExpectedData(t) + c.waitForExpectedData(t, 30*time.Second) // Send second half of data. c.expectSamples(samples[len(samples)/2:], series) @@ -160,28 +312,35 @@ func TestSampleDelivery(t *testing.T) { qm.AppendExemplars(exemplars[len(exemplars)/2:]) qm.AppendHistograms(histograms[len(histograms)/2:]) qm.AppendFloatHistograms(floatHistograms[len(floatHistograms)/2:]) - c.waitForExpectedData(t) + c.waitForExpectedData(t, 30*time.Second) }) } } -func newTestClientAndQueueManager(t testing.TB, flushDeadline time.Duration) (*TestWriteClient, *QueueManager) { - c := NewTestWriteClient() +func newTestClientAndQueueManager(t testing.TB, flushDeadline time.Duration, protoMsg config.RemoteWriteProtoMsg) (*TestWriteClient, *QueueManager) { + c := NewTestWriteClient(protoMsg) cfg := config.DefaultQueueConfig mcfg := config.DefaultMetadataConfig - return c, newTestQueueManager(t, cfg, mcfg, flushDeadline, c) + return c, newTestQueueManager(t, cfg, mcfg, flushDeadline, c, protoMsg) } -func newTestQueueManager(t testing.TB, cfg config.QueueConfig, mcfg config.MetadataConfig, deadline time.Duration, c WriteClient) *QueueManager { +func newTestQueueManager(t testing.TB, cfg config.QueueConfig, mcfg config.MetadataConfig, deadline time.Duration, c WriteClient, protoMsg config.RemoteWriteProtoMsg) *QueueManager { dir := t.TempDir() metrics := newQueueManagerMetrics(nil, "", "") - m := NewQueueManager(metrics, nil, nil, nil, dir, newEWMARate(ewmaWeight, shardUpdateDuration), cfg, mcfg, labels.EmptyLabels(), nil, c, deadline, newPool(), newHighestTimestampMetric(), nil, false, false) + m := NewQueueManager(metrics, nil, nil, nil, dir, newEWMARate(ewmaWeight, shardUpdateDuration), cfg, mcfg, labels.EmptyLabels(), nil, c, deadline, newPool(), newHighestTimestampMetric(), nil, false, false, protoMsg) return m } +func testDefaultQueueConfig() config.QueueConfig { + cfg := config.DefaultQueueConfig + // For faster unit tests we don't wait default 5 seconds. + cfg.BatchSendDeadline = model.Duration(100 * time.Millisecond) + return cfg +} + func TestMetadataDelivery(t *testing.T) { - c, m := newTestClientAndQueueManager(t, defaultFlushDeadline) + c, m := newTestClientAndQueueManager(t, defaultFlushDeadline, config.RemoteWriteProtoMsgV1) m.Start() defer m.Stop() @@ -196,8 +355,9 @@ func TestMetadataDelivery(t *testing.T) { }) } - m.AppendMetadata(context.Background(), metadata) + m.AppendWatcherMetadata(context.Background(), metadata) + require.Equal(t, 0.0, client_testutil.ToFloat64(m.metrics.failedMetadataTotal)) require.Len(t, c.receivedMetadata, numMetadata) // One more write than the rounded qoutient should be performed in order to get samples that didn't // fit into MaxSamplesPerSend. @@ -206,58 +366,106 @@ func TestMetadataDelivery(t *testing.T) { require.Equal(t, c.receivedMetadata[metadata[len(metadata)-1].Metric][0].MetricFamilyName, metadata[len(metadata)-1].Metric) } -func TestSampleDeliveryTimeout(t *testing.T) { - // Let's send one less sample than batch size, and wait the timeout duration - n := 9 - samples, series := createTimeseries(n, n) +func TestWALMetadataDelivery(t *testing.T) { + dir := t.TempDir() + s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil, true) + defer s.Close() cfg := config.DefaultQueueConfig - cfg.MaxShards = 1 cfg.BatchSendDeadline = model.Duration(100 * time.Millisecond) + cfg.MaxShards = 1 - c := NewTestWriteClient() - m := newTestQueueManager(t, cfg, config.DefaultMetadataConfig, defaultFlushDeadline, c) - m.StoreSeries(series, 0) - m.Start() - defer m.Stop() + writeConfig := baseRemoteWriteConfig("http://test-storage.com") + writeConfig.QueueConfig = cfg + writeConfig.ProtobufMessage = config.RemoteWriteProtoMsgV2 - // Send the samples twice, waiting for the samples in the meantime. - c.expectSamples(samples, series) - m.Append(samples) - c.waitForExpectedData(t) + conf := &config.Config{ + GlobalConfig: config.DefaultGlobalConfig, + RemoteWriteConfigs: []*config.RemoteWriteConfig{ + writeConfig, + }, + } - c.expectSamples(samples, series) - m.Append(samples) - c.waitForExpectedData(t) + num := 3 + _, series := createTimeseries(0, num) + metadata := createSeriesMetadata(series) + + require.NoError(t, s.ApplyConfig(conf)) + hash, err := toHash(writeConfig) + require.NoError(t, err) + qm := s.rws.queues[hash] + + c := NewTestWriteClient(config.RemoteWriteProtoMsgV1) + qm.SetClient(c) + + qm.StoreSeries(series, 0) + qm.StoreMetadata(metadata) + + require.Len(t, qm.seriesLabels, num) + require.Len(t, qm.seriesMetadata, num) + + c.waitForExpectedData(t, 30*time.Second) +} + +func TestSampleDeliveryTimeout(t *testing.T) { + for _, protoMsg := range []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1, config.RemoteWriteProtoMsgV2} { + t.Run(fmt.Sprint(protoMsg), func(t *testing.T) { + // Let's send one less sample than batch size, and wait the timeout duration + n := 9 + samples, series := createTimeseries(n, n) + cfg := testDefaultQueueConfig() + mcfg := config.DefaultMetadataConfig + cfg.MaxShards = 1 + + c := NewTestWriteClient(protoMsg) + m := newTestQueueManager(t, cfg, mcfg, defaultFlushDeadline, c, protoMsg) + m.StoreSeries(series, 0) + m.Start() + defer m.Stop() + + // Send the samples twice, waiting for the samples in the meantime. + c.expectSamples(samples, series) + m.Append(samples) + c.waitForExpectedData(t, 30*time.Second) + + c.expectSamples(samples, series) + m.Append(samples) + c.waitForExpectedData(t, 30*time.Second) + }) + } } func TestSampleDeliveryOrder(t *testing.T) { - ts := 10 - n := config.DefaultQueueConfig.MaxSamplesPerSend * ts - samples := make([]record.RefSample, 0, n) - series := make([]record.RefSeries, 0, n) - for i := 0; i < n; i++ { - name := fmt.Sprintf("test_metric_%d", i%ts) - samples = append(samples, record.RefSample{ - Ref: chunks.HeadSeriesRef(i), - T: int64(i), - V: float64(i), - }) - series = append(series, record.RefSeries{ - Ref: chunks.HeadSeriesRef(i), - Labels: labels.FromStrings("__name__", name), + for _, protoMsg := range []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1, config.RemoteWriteProtoMsgV2} { + t.Run(fmt.Sprint(protoMsg), func(t *testing.T) { + ts := 10 + n := config.DefaultQueueConfig.MaxSamplesPerSend * ts + samples := make([]record.RefSample, 0, n) + series := make([]record.RefSeries, 0, n) + for i := 0; i < n; i++ { + name := fmt.Sprintf("test_metric_%d", i%ts) + samples = append(samples, record.RefSample{ + Ref: chunks.HeadSeriesRef(i), + T: int64(i), + V: float64(i), + }) + series = append(series, record.RefSeries{ + Ref: chunks.HeadSeriesRef(i), + Labels: labels.FromStrings("__name__", name), + }) + } + + c, m := newTestClientAndQueueManager(t, defaultFlushDeadline, protoMsg) + c.expectSamples(samples, series) + m.StoreSeries(series, 0) + + m.Start() + defer m.Stop() + // These should be received by the client. + m.Append(samples) + c.waitForExpectedData(t, 30*time.Second) }) } - - c, m := newTestClientAndQueueManager(t, defaultFlushDeadline) - c.expectSamples(samples, series) - m.StoreSeries(series, 0) - - m.Start() - defer m.Stop() - // These should be received by the client. - m.Append(samples) - c.waitForExpectedData(t) } func TestShutdown(t *testing.T) { @@ -267,7 +475,7 @@ func TestShutdown(t *testing.T) { cfg := config.DefaultQueueConfig mcfg := config.DefaultMetadataConfig - m := newTestQueueManager(t, cfg, mcfg, deadline, c) + m := newTestQueueManager(t, cfg, mcfg, deadline, c, config.RemoteWriteProtoMsgV1) n := 2 * config.DefaultQueueConfig.MaxSamplesPerSend samples, series := createTimeseries(n, n) m.StoreSeries(series, 0) @@ -302,8 +510,7 @@ func TestSeriesReset(t *testing.T) { cfg := config.DefaultQueueConfig mcfg := config.DefaultMetadataConfig - m := newTestQueueManager(t, cfg, mcfg, deadline, c) - + m := newTestQueueManager(t, cfg, mcfg, deadline, c, config.RemoteWriteProtoMsgV1) for i := 0; i < numSegments; i++ { series := []record.RefSeries{} for j := 0; j < numSeries; j++ { @@ -317,167 +524,186 @@ func TestSeriesReset(t *testing.T) { } func TestReshard(t *testing.T) { - size := 10 // Make bigger to find more races. - nSeries := 6 - nSamples := config.DefaultQueueConfig.Capacity * size - samples, series := createTimeseries(nSamples, nSeries) + for _, protoMsg := range []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1, config.RemoteWriteProtoMsgV2} { + t.Run(fmt.Sprint(protoMsg), func(t *testing.T) { + size := 10 // Make bigger to find more races. + nSeries := 6 + nSamples := config.DefaultQueueConfig.Capacity * size + samples, series := createTimeseries(nSamples, nSeries) - cfg := config.DefaultQueueConfig - cfg.MaxShards = 1 + cfg := config.DefaultQueueConfig + cfg.MaxShards = 1 - c := NewTestWriteClient() - m := newTestQueueManager(t, cfg, config.DefaultMetadataConfig, defaultFlushDeadline, c) - c.expectSamples(samples, series) - m.StoreSeries(series, 0) + c := NewTestWriteClient(protoMsg) + m := newTestQueueManager(t, cfg, config.DefaultMetadataConfig, defaultFlushDeadline, c, protoMsg) + c.expectSamples(samples, series) + m.StoreSeries(series, 0) - m.Start() - defer m.Stop() + m.Start() + defer m.Stop() - go func() { - for i := 0; i < len(samples); i += config.DefaultQueueConfig.Capacity { - sent := m.Append(samples[i : i+config.DefaultQueueConfig.Capacity]) - require.True(t, sent, "samples not sent") - time.Sleep(100 * time.Millisecond) - } - }() + go func() { + for i := 0; i < len(samples); i += config.DefaultQueueConfig.Capacity { + sent := m.Append(samples[i : i+config.DefaultQueueConfig.Capacity]) + require.True(t, sent, "samples not sent") + time.Sleep(100 * time.Millisecond) + } + }() - for i := 1; i < len(samples)/config.DefaultQueueConfig.Capacity; i++ { - m.shards.stop() - m.shards.start(i) - time.Sleep(100 * time.Millisecond) + for i := 1; i < len(samples)/config.DefaultQueueConfig.Capacity; i++ { + m.shards.stop() + m.shards.start(i) + time.Sleep(100 * time.Millisecond) + } + + c.waitForExpectedData(t, 30*time.Second) + }) } - - c.waitForExpectedData(t) } func TestReshardRaceWithStop(t *testing.T) { - c := NewTestWriteClient() - var m *QueueManager - h := sync.Mutex{} - - h.Lock() - - cfg := config.DefaultQueueConfig - mcfg := config.DefaultMetadataConfig - exitCh := make(chan struct{}) - go func() { - for { - m = newTestQueueManager(t, cfg, mcfg, defaultFlushDeadline, c) - m.Start() - h.Unlock() + for _, protoMsg := range []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1, config.RemoteWriteProtoMsgV2} { + t.Run(fmt.Sprint(protoMsg), func(t *testing.T) { + c := NewTestWriteClient(protoMsg) + var m *QueueManager + h := sync.Mutex{} h.Lock() - m.Stop() - select { - case exitCh <- struct{}{}: - return - default: + cfg := testDefaultQueueConfig() + mcfg := config.DefaultMetadataConfig + exitCh := make(chan struct{}) + go func() { + for { + m = newTestQueueManager(t, cfg, mcfg, defaultFlushDeadline, c, protoMsg) + + m.Start() + h.Unlock() + h.Lock() + m.Stop() + + select { + case exitCh <- struct{}{}: + return + default: + } + } + }() + + for i := 1; i < 100; i++ { + h.Lock() + m.reshardChan <- i + h.Unlock() } - } - }() - - for i := 1; i < 100; i++ { - h.Lock() - m.reshardChan <- i - h.Unlock() + <-exitCh + }) } - <-exitCh } func TestReshardPartialBatch(t *testing.T) { - samples, series := createTimeseries(1, 10) + for _, protoMsg := range []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1, config.RemoteWriteProtoMsgV2} { + t.Run(fmt.Sprint(protoMsg), func(t *testing.T) { + samples, series := createTimeseries(1, 10) - c := NewTestBlockedWriteClient() + c := NewTestBlockedWriteClient() - cfg := config.DefaultQueueConfig - mcfg := config.DefaultMetadataConfig - cfg.MaxShards = 1 - batchSendDeadline := time.Millisecond - flushDeadline := 10 * time.Millisecond - cfg.BatchSendDeadline = model.Duration(batchSendDeadline) + cfg := testDefaultQueueConfig() + mcfg := config.DefaultMetadataConfig + cfg.MaxShards = 1 + batchSendDeadline := time.Millisecond + flushDeadline := 10 * time.Millisecond + cfg.BatchSendDeadline = model.Duration(batchSendDeadline) - m := newTestQueueManager(t, cfg, mcfg, flushDeadline, c) - m.StoreSeries(series, 0) + m := newTestQueueManager(t, cfg, mcfg, flushDeadline, c, protoMsg) + m.StoreSeries(series, 0) - m.Start() + m.Start() - for i := 0; i < 100; i++ { - done := make(chan struct{}) - go func() { - m.Append(samples) - time.Sleep(batchSendDeadline) - m.shards.stop() - m.shards.start(1) - done <- struct{}{} - }() - select { - case <-done: - case <-time.After(2 * time.Second): - t.Error("Deadlock between sending and stopping detected") - pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) - t.FailNow() - } + for i := 0; i < 100; i++ { + done := make(chan struct{}) + go func() { + m.Append(samples) + time.Sleep(batchSendDeadline) + m.shards.stop() + m.shards.start(1) + done <- struct{}{} + }() + select { + case <-done: + case <-time.After(2 * time.Second): + t.Error("Deadlock between sending and stopping detected") + pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) + t.FailNow() + } + } + // We can only call stop if there was not a deadlock. + m.Stop() + }) } - // We can only call stop if there was not a deadlock. - m.Stop() } // TestQueueFilledDeadlock makes sure the code does not deadlock in the case // where a large scrape (> capacity + max samples per send) is appended at the // same time as a batch times out according to the batch send deadline. func TestQueueFilledDeadlock(t *testing.T) { - samples, series := createTimeseries(50, 1) + for _, protoMsg := range []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1, config.RemoteWriteProtoMsgV2} { + t.Run(fmt.Sprint(protoMsg), func(t *testing.T) { + samples, series := createTimeseries(50, 1) - c := NewNopWriteClient() + c := NewNopWriteClient() - cfg := config.DefaultQueueConfig - mcfg := config.DefaultMetadataConfig - cfg.MaxShards = 1 - cfg.MaxSamplesPerSend = 10 - cfg.Capacity = 20 - flushDeadline := time.Second - batchSendDeadline := time.Millisecond - cfg.BatchSendDeadline = model.Duration(batchSendDeadline) + cfg := testDefaultQueueConfig() + mcfg := config.DefaultMetadataConfig + cfg.MaxShards = 1 + cfg.MaxSamplesPerSend = 10 + cfg.Capacity = 20 + flushDeadline := time.Second + batchSendDeadline := time.Millisecond + cfg.BatchSendDeadline = model.Duration(batchSendDeadline) - m := newTestQueueManager(t, cfg, mcfg, flushDeadline, c) - m.StoreSeries(series, 0) - m.Start() - defer m.Stop() + m := newTestQueueManager(t, cfg, mcfg, flushDeadline, c, protoMsg) + m.StoreSeries(series, 0) + m.Start() + defer m.Stop() - for i := 0; i < 100; i++ { - done := make(chan struct{}) - go func() { - time.Sleep(batchSendDeadline) - m.Append(samples) - done <- struct{}{} - }() - select { - case <-done: - case <-time.After(2 * time.Second): - t.Error("Deadlock between sending and appending detected") - pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) - t.FailNow() - } + for i := 0; i < 100; i++ { + done := make(chan struct{}) + go func() { + time.Sleep(batchSendDeadline) + m.Append(samples) + done <- struct{}{} + }() + select { + case <-done: + case <-time.After(2 * time.Second): + t.Error("Deadlock between sending and appending detected") + pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) + t.FailNow() + } + } + }) } } func TestReleaseNoninternedString(t *testing.T) { - _, m := newTestClientAndQueueManager(t, defaultFlushDeadline) - m.Start() - defer m.Stop() + for _, protoMsg := range []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1, config.RemoteWriteProtoMsgV2} { + t.Run(fmt.Sprint(protoMsg), func(t *testing.T) { + _, m := newTestClientAndQueueManager(t, defaultFlushDeadline, protoMsg) + m.Start() + defer m.Stop() + for i := 1; i < 1000; i++ { + m.StoreSeries([]record.RefSeries{ + { + Ref: chunks.HeadSeriesRef(i), + Labels: labels.FromStrings("asdf", strconv.Itoa(i)), + }, + }, 0) + m.SeriesReset(1) + } - for i := 1; i < 1000; i++ { - m.StoreSeries([]record.RefSeries{ - { - Ref: chunks.HeadSeriesRef(i), - Labels: labels.FromStrings("asdf", strconv.Itoa(i)), - }, - }, 0) - m.SeriesReset(1) + metric := client_testutil.ToFloat64(noReferenceReleases) + require.Equal(t, 0.0, metric, "expected there to be no calls to release for strings that were not already interned: %d", int(metric)) + }) } - - metric := client_testutil.ToFloat64(noReferenceReleases) - require.Equal(t, 0.0, metric, "expected there to be no calls to release for strings that were not already interned: %d", int(metric)) } func TestShouldReshard(t *testing.T) { @@ -505,7 +731,7 @@ func TestShouldReshard(t *testing.T) { } for _, c := range cases { - _, m := newTestClientAndQueueManager(t, defaultFlushDeadline) + _, m := newTestClientAndQueueManager(t, defaultFlushDeadline, config.RemoteWriteProtoMsgV1) m.numShards = c.startingShards m.dataIn.incr(c.samplesIn) m.dataOut.incr(c.samplesOut) @@ -551,7 +777,7 @@ func TestDisableReshardOnRetry(t *testing.T) { } ) - m := NewQueueManager(metrics, nil, nil, nil, "", newEWMARate(ewmaWeight, shardUpdateDuration), cfg, mcfg, labels.EmptyLabels(), nil, client, 0, newPool(), newHighestTimestampMetric(), nil, false, false) + m := NewQueueManager(metrics, nil, nil, nil, "", newEWMARate(ewmaWeight, shardUpdateDuration), cfg, mcfg, labels.EmptyLabels(), nil, client, 0, newPool(), newHighestTimestampMetric(), nil, false, false, config.RemoteWriteProtoMsgV1) m.StoreSeries(fakeSeries, 0) // Attempt to samples while the manager is running. We immediately stop the @@ -601,6 +827,9 @@ func createTimeseries(numSamples, numSeries int, extraLabels ...labels.Label) ([ // Create Labels that is name of series plus any extra labels supplied. lb.Reset() lb.Add(labels.MetricName, name) + rand.Shuffle(len(extraLabels), func(i, j int) { + extraLabels[i], extraLabels[j] = extraLabels[j], extraLabels[i] + }) for _, l := range extraLabels { lb.Add(l.Name, l.Value) } @@ -705,10 +934,26 @@ func createHistograms(numSamples, numSeries int, floatHistogram bool) ([]record. return histograms, nil, series } +func createSeriesMetadata(series []record.RefSeries) []record.RefMetadata { + metas := make([]record.RefMetadata, len(series)) + + for _, s := range series { + metas = append(metas, record.RefMetadata{ + Ref: s.Ref, + Type: uint8(record.Counter), + Unit: "unit text", + Help: "help text", + }) + } + return metas +} + func getSeriesIDFromRef(r record.RefSeries) string { return r.Labels.String() } +// TestWriteClient represents write client which does not call remote storage, +// but instead re-implements fake WriteHandler for test purposes. type TestWriteClient struct { receivedSamples map[string][]prompb.Sample expectedSamples map[string][]prompb.Sample @@ -720,30 +965,37 @@ type TestWriteClient struct { expectedFloatHistograms map[string][]prompb.Histogram receivedMetadata map[string][]prompb.MetricMetadata writesReceived int - withWaitGroup bool - wg sync.WaitGroup mtx sync.Mutex buf []byte + protoMsg config.RemoteWriteProtoMsg + injectedErrs []error + currErr int + retry bool - storeWait time.Duration + storeWait time.Duration + // TODO(npazosmendez): maybe replaceable with injectedErrs? returnError error } -func NewTestWriteClient() *TestWriteClient { +// NewTestWriteClient creates a new testing write client. +func NewTestWriteClient(protoMsg config.RemoteWriteProtoMsg) *TestWriteClient { return &TestWriteClient{ - withWaitGroup: true, receivedSamples: map[string][]prompb.Sample{}, expectedSamples: map[string][]prompb.Sample{}, receivedMetadata: map[string][]prompb.MetricMetadata{}, + protoMsg: protoMsg, storeWait: 0, returnError: nil, } } +func (c *TestWriteClient) injectErrors(injectedErrs []error) { + c.injectedErrs = injectedErrs + c.currErr = -1 + c.retry = false +} + func (c *TestWriteClient) expectSamples(ss []record.RefSample, series []record.RefSeries) { - if !c.withWaitGroup { - return - } c.mtx.Lock() defer c.mtx.Unlock() @@ -757,16 +1009,9 @@ func (c *TestWriteClient) expectSamples(ss []record.RefSample, series []record.R Value: s.V, }) } - if !c.withWaitGroup { - return - } - c.wg.Add(len(ss)) } func (c *TestWriteClient) expectExemplars(ss []record.RefExemplar, series []record.RefSeries) { - if !c.withWaitGroup { - return - } c.mtx.Lock() defer c.mtx.Unlock() @@ -776,19 +1021,15 @@ func (c *TestWriteClient) expectExemplars(ss []record.RefExemplar, series []reco for _, s := range ss { tsID := getSeriesIDFromRef(series[s.Ref]) e := prompb.Exemplar{ - Labels: LabelsToLabelsProto(s.Labels, nil), + Labels: prompb.FromLabels(s.Labels, nil), Timestamp: s.T, Value: s.V, } c.expectedExemplars[tsID] = append(c.expectedExemplars[tsID], e) } - c.wg.Add(len(ss)) } func (c *TestWriteClient) expectHistograms(hh []record.RefHistogramSample, series []record.RefSeries) { - if !c.withWaitGroup { - return - } c.mtx.Lock() defer c.mtx.Unlock() @@ -797,15 +1038,11 @@ func (c *TestWriteClient) expectHistograms(hh []record.RefHistogramSample, serie for _, h := range hh { tsID := getSeriesIDFromRef(series[h.Ref]) - c.expectedHistograms[tsID] = append(c.expectedHistograms[tsID], HistogramToHistogramProto(h.T, h.H)) + c.expectedHistograms[tsID] = append(c.expectedHistograms[tsID], prompb.FromIntHistogram(h.T, h.H)) } - c.wg.Add(len(hh)) } func (c *TestWriteClient) expectFloatHistograms(fhs []record.RefFloatHistogramSample, series []record.RefSeries) { - if !c.withWaitGroup { - return - } c.mtx.Lock() defer c.mtx.Unlock() @@ -814,18 +1051,42 @@ func (c *TestWriteClient) expectFloatHistograms(fhs []record.RefFloatHistogramSa for _, fh := range fhs { tsID := getSeriesIDFromRef(series[fh.Ref]) - c.expectedFloatHistograms[tsID] = append(c.expectedFloatHistograms[tsID], FloatHistogramToHistogramProto(fh.T, fh.FH)) + c.expectedFloatHistograms[tsID] = append(c.expectedFloatHistograms[tsID], prompb.FromFloatHistogram(fh.T, fh.FH)) } - c.wg.Add(len(fhs)) } -func (c *TestWriteClient) waitForExpectedData(tb testing.TB) { - if !c.withWaitGroup { - return +func deepLen[M any](ms ...map[string][]M) int { + l := 0 + for _, m := range ms { + for _, v := range m { + l += len(v) + } } - c.wg.Wait() + return l +} + +func (c *TestWriteClient) waitForExpectedData(tb testing.TB, timeout time.Duration) { + tb.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + if err := runutil.Retry(500*time.Millisecond, ctx.Done(), func() error { + c.mtx.Lock() + exp := deepLen(c.expectedSamples) + deepLen(c.expectedExemplars) + deepLen(c.expectedHistograms, c.expectedFloatHistograms) + got := deepLen(c.receivedSamples) + deepLen(c.receivedExemplars) + deepLen(c.receivedHistograms, c.receivedFloatHistograms) + c.mtx.Unlock() + + if got < exp { + return fmt.Errorf("expected %v samples/exemplars/histograms/floathistograms, got %v", exp, got) + } + return nil + }); err != nil { + tb.Error(err) + } + c.mtx.Lock() defer c.mtx.Unlock() + for ts, expectedSamples := range c.expectedSamples { require.Equal(tb, expectedSamples, c.receivedSamples[ts], ts) } @@ -865,50 +1126,68 @@ func (c *TestWriteClient) Store(_ context.Context, req []byte, _ int) error { if c.buf != nil { c.buf = c.buf[:cap(c.buf)] } + reqBuf, err := snappy.Decode(c.buf, req) c.buf = reqBuf if err != nil { return err } - var reqProto prompb.WriteRequest - if err := proto.Unmarshal(reqBuf, &reqProto); err != nil { + // Check if we've been told to inject err for this call. + if len(c.injectedErrs) > 0 { + c.currErr++ + if err = c.injectedErrs[c.currErr]; err != nil { + return err + } + } + + var reqProto *prompb.WriteRequest + switch c.protoMsg { + case config.RemoteWriteProtoMsgV1: + reqProto = &prompb.WriteRequest{} + err = proto.Unmarshal(reqBuf, reqProto) + case config.RemoteWriteProtoMsgV2: + // NOTE(bwplotka): v1 msg can be unmarshaled to v2 sometimes, without + // errors. + var reqProtoV2 writev2.Request + err = proto.Unmarshal(reqBuf, &reqProtoV2) + if err == nil { + reqProto, err = v2RequestToWriteRequest(&reqProtoV2) + } + } + if err != nil { return err } - builder := labels.NewScratchBuilder(0) - count := 0 + + if len(reqProto.Timeseries) == 0 && len(reqProto.Metadata) == 0 { + return errors.New("invalid request, no timeseries") + } + + b := labels.NewScratchBuilder(0) for _, ts := range reqProto.Timeseries { - labels := LabelProtosToLabels(&builder, ts.Labels) + labels := ts.ToLabels(&b, nil) tsID := labels.String() - for _, sample := range ts.Samples { - count++ - c.receivedSamples[tsID] = append(c.receivedSamples[tsID], sample) + if len(ts.Samples) > 0 { + c.receivedSamples[tsID] = append(c.receivedSamples[tsID], ts.Samples...) } - for _, ex := range ts.Exemplars { - count++ - c.receivedExemplars[tsID] = append(c.receivedExemplars[tsID], ex) + if len(ts.Exemplars) > 0 { + c.receivedExemplars[tsID] = append(c.receivedExemplars[tsID], ts.Exemplars...) } - for _, histogram := range ts.Histograms { - count++ - if histogram.IsFloatHistogram() { - c.receivedFloatHistograms[tsID] = append(c.receivedFloatHistograms[tsID], histogram) + for _, h := range ts.Histograms { + if h.IsFloatHistogram() { + c.receivedFloatHistograms[tsID] = append(c.receivedFloatHistograms[tsID], h) } else { - c.receivedHistograms[tsID] = append(c.receivedHistograms[tsID], histogram) + c.receivedHistograms[tsID] = append(c.receivedHistograms[tsID], h) } } } - if c.withWaitGroup { - c.wg.Add(-count) - } - for _, m := range reqProto.Metadata { c.receivedMetadata[m.MetricFamilyName] = append(c.receivedMetadata[m.MetricFamilyName], m) } c.writesReceived++ - return nil } @@ -920,6 +1199,51 @@ func (c *TestWriteClient) Endpoint() string { return "http://test-remote.com/1234" } +func v2RequestToWriteRequest(v2Req *writev2.Request) (*prompb.WriteRequest, error) { + req := &prompb.WriteRequest{ + Timeseries: make([]prompb.TimeSeries, len(v2Req.Timeseries)), + // TODO handle metadata? + } + b := labels.NewScratchBuilder(0) + for i, rts := range v2Req.Timeseries { + rts.ToLabels(&b, v2Req.Symbols).Range(func(l labels.Label) { + req.Timeseries[i].Labels = append(req.Timeseries[i].Labels, prompb.Label{ + Name: l.Name, + Value: l.Value, + }) + }) + + exemplars := make([]prompb.Exemplar, len(rts.Exemplars)) + for j, e := range rts.Exemplars { + exemplars[j].Value = e.Value + exemplars[j].Timestamp = e.Timestamp + e.ToExemplar(&b, v2Req.Symbols).Labels.Range(func(l labels.Label) { + exemplars[j].Labels = append(exemplars[j].Labels, prompb.Label{ + Name: l.Name, + Value: l.Value, + }) + }) + } + req.Timeseries[i].Exemplars = exemplars + + req.Timeseries[i].Samples = make([]prompb.Sample, len(rts.Samples)) + for j, s := range rts.Samples { + req.Timeseries[i].Samples[j].Timestamp = s.Timestamp + req.Timeseries[i].Samples[j].Value = s.Value + } + + req.Timeseries[i].Histograms = make([]prompb.Histogram, len(rts.Histograms)) + for j, h := range rts.Histograms { + if h.IsFloatHistogram() { + req.Timeseries[i].Histograms[j] = prompb.FromFloatHistogram(h.Timestamp, h.ToFloatHistogram()) + continue + } + req.Timeseries[i].Histograms[j] = prompb.FromIntHistogram(h.Timestamp, h.ToIntHistogram()) + } + } + return req, nil +} + // TestBlockingWriteClient is a queue_manager WriteClient which will block // on any calls to Store(), until the request's Context is cancelled, at which // point the `numCalls` property will contain a count of how many times Store() @@ -953,10 +1277,12 @@ func (c *TestBlockingWriteClient) Endpoint() string { // For benchmarking the send and not the receive side. type NopWriteClient struct{} -func NewNopWriteClient() *NopWriteClient { return &NopWriteClient{} } -func (c *NopWriteClient) Store(context.Context, []byte, int) error { return nil } -func (c *NopWriteClient) Name() string { return "nopwriteclient" } -func (c *NopWriteClient) Endpoint() string { return "http://test-remote.com/1234" } +func NewNopWriteClient() *NopWriteClient { return &NopWriteClient{} } +func (c *NopWriteClient) Store(context.Context, []byte, int) error { + return nil +} +func (c *NopWriteClient) Name() string { return "nopwriteclient" } +func (c *NopWriteClient) Endpoint() string { return "http://test-remote.com/1234" } type MockWriteClient struct { StoreFunc func(context.Context, []byte, int) error @@ -998,13 +1324,14 @@ func BenchmarkSampleSend(b *testing.B) { c := NewNopWriteClient() - cfg := config.DefaultQueueConfig + cfg := testDefaultQueueConfig() mcfg := config.DefaultMetadataConfig cfg.BatchSendDeadline = model.Duration(100 * time.Millisecond) cfg.MinShards = 20 cfg.MaxShards = 20 - m := newTestQueueManager(b, cfg, mcfg, defaultFlushDeadline, c) + // todo: test with new proto type(s) + m := newTestQueueManager(b, cfg, mcfg, defaultFlushDeadline, c, config.RemoteWriteProtoMsgV1) m.StoreSeries(series, 0) // These should be received by the client. @@ -1058,12 +1385,12 @@ func BenchmarkStoreSeries(b *testing.B) { for _, tc := range testCases { b.Run(tc.name, func(b *testing.B) { for i := 0; i < b.N; i++ { - c := NewTestWriteClient() + c := NewTestWriteClient(config.RemoteWriteProtoMsgV1) dir := b.TempDir() cfg := config.DefaultQueueConfig mcfg := config.DefaultMetadataConfig metrics := newQueueManagerMetrics(nil, "", "") - m := NewQueueManager(metrics, nil, nil, nil, dir, newEWMARate(ewmaWeight, shardUpdateDuration), cfg, mcfg, labels.EmptyLabels(), nil, c, defaultFlushDeadline, newPool(), newHighestTimestampMetric(), nil, false, false) + m := NewQueueManager(metrics, nil, nil, nil, dir, newEWMARate(ewmaWeight, shardUpdateDuration), cfg, mcfg, labels.EmptyLabels(), nil, c, defaultFlushDeadline, newPool(), newHighestTimestampMetric(), nil, false, false, config.RemoteWriteProtoMsgV1) m.externalLabels = tc.externalLabels m.relabelConfigs = tc.relabelConfigs @@ -1095,14 +1422,15 @@ func BenchmarkStartup(b *testing.B) { logger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout)) logger = log.With(logger, "caller", log.DefaultCaller) - cfg := config.DefaultQueueConfig + cfg := testDefaultQueueConfig() mcfg := config.DefaultMetadataConfig for n := 0; n < b.N; n++ { metrics := newQueueManagerMetrics(nil, "", "") c := NewTestBlockedWriteClient() + // todo: test with new proto type(s) m := NewQueueManager(metrics, nil, nil, logger, dir, newEWMARate(ewmaWeight, shardUpdateDuration), - cfg, mcfg, labels.EmptyLabels(), nil, c, 1*time.Minute, newPool(), newHighestTimestampMetric(), nil, false, false) + cfg, mcfg, labels.EmptyLabels(), nil, c, 1*time.Minute, newPool(), newHighestTimestampMetric(), nil, false, false, config.RemoteWriteProtoMsgV1) m.watcher.SetStartTime(timestamp.Time(math.MaxInt64)) m.watcher.MaxSegment = segments[len(segments)-2] err := m.watcher.Run() @@ -1181,7 +1509,7 @@ func TestProcessExternalLabels(t *testing.T) { func TestCalculateDesiredShards(t *testing.T) { cfg := config.DefaultQueueConfig - _, m := newTestClientAndQueueManager(t, defaultFlushDeadline) + _, m := newTestClientAndQueueManager(t, defaultFlushDeadline, config.RemoteWriteProtoMsgV1) samplesIn := m.dataIn // Need to start the queue manager so the proper metrics are initialized. @@ -1251,7 +1579,7 @@ func TestCalculateDesiredShards(t *testing.T) { } func TestCalculateDesiredShardsDetail(t *testing.T) { - _, m := newTestClientAndQueueManager(t, defaultFlushDeadline) + _, m := newTestClientAndQueueManager(t, defaultFlushDeadline, config.RemoteWriteProtoMsgV1) samplesIn := m.dataIn for _, tc := range []struct { @@ -1464,27 +1792,179 @@ func TestQueue_FlushAndShutdownDoesNotDeadlock(t *testing.T) { } } +func createDummyTimeSeries(instances int) []timeSeries { + metrics := []labels.Labels{ + labels.FromStrings("__name__", "go_gc_duration_seconds", "quantile", "0"), + labels.FromStrings("__name__", "go_gc_duration_seconds", "quantile", "0.25"), + labels.FromStrings("__name__", "go_gc_duration_seconds", "quantile", "0.5"), + labels.FromStrings("__name__", "go_gc_duration_seconds", "quantile", "0.75"), + labels.FromStrings("__name__", "go_gc_duration_seconds", "quantile", "1"), + labels.FromStrings("__name__", "go_gc_duration_seconds_sum"), + labels.FromStrings("__name__", "go_gc_duration_seconds_count"), + labels.FromStrings("__name__", "go_memstats_alloc_bytes_total"), + labels.FromStrings("__name__", "go_memstats_frees_total"), + labels.FromStrings("__name__", "go_memstats_lookups_total"), + labels.FromStrings("__name__", "go_memstats_mallocs_total"), + labels.FromStrings("__name__", "go_goroutines"), + labels.FromStrings("__name__", "go_info", "version", "go1.19.3"), + labels.FromStrings("__name__", "go_memstats_alloc_bytes"), + labels.FromStrings("__name__", "go_memstats_buck_hash_sys_bytes"), + labels.FromStrings("__name__", "go_memstats_gc_sys_bytes"), + labels.FromStrings("__name__", "go_memstats_heap_alloc_bytes"), + labels.FromStrings("__name__", "go_memstats_heap_idle_bytes"), + labels.FromStrings("__name__", "go_memstats_heap_inuse_bytes"), + labels.FromStrings("__name__", "go_memstats_heap_objects"), + labels.FromStrings("__name__", "go_memstats_heap_released_bytes"), + labels.FromStrings("__name__", "go_memstats_heap_sys_bytes"), + labels.FromStrings("__name__", "go_memstats_last_gc_time_seconds"), + labels.FromStrings("__name__", "go_memstats_mcache_inuse_bytes"), + labels.FromStrings("__name__", "go_memstats_mcache_sys_bytes"), + labels.FromStrings("__name__", "go_memstats_mspan_inuse_bytes"), + labels.FromStrings("__name__", "go_memstats_mspan_sys_bytes"), + labels.FromStrings("__name__", "go_memstats_next_gc_bytes"), + labels.FromStrings("__name__", "go_memstats_other_sys_bytes"), + labels.FromStrings("__name__", "go_memstats_stack_inuse_bytes"), + labels.FromStrings("__name__", "go_memstats_stack_sys_bytes"), + labels.FromStrings("__name__", "go_memstats_sys_bytes"), + labels.FromStrings("__name__", "go_threads"), + } + + commonLabels := labels.FromStrings( + "cluster", "some-cluster-0", + "container", "prometheus", + "job", "some-namespace/prometheus", + "namespace", "some-namespace") + + var result []timeSeries + r := rand.New(rand.NewSource(0)) + for i := 0; i < instances; i++ { + b := labels.NewBuilder(commonLabels) + b.Set("pod", "prometheus-"+strconv.Itoa(i)) + for _, lbls := range metrics { + lbls.Range(func(l labels.Label) { + b.Set(l.Name, l.Value) + }) + result = append(result, timeSeries{ + seriesLabels: b.Labels(), + value: r.Float64(), + }) + } + } + return result +} + +func BenchmarkBuildWriteRequest(b *testing.B) { + noopLogger := log.NewNopLogger() + bench := func(b *testing.B, batch []timeSeries) { + buff := make([]byte, 0) + seriesBuff := make([]prompb.TimeSeries, len(batch)) + for i := range seriesBuff { + seriesBuff[i].Samples = []prompb.Sample{{}} + seriesBuff[i].Exemplars = []prompb.Exemplar{{}} + } + pBuf := proto.NewBuffer(nil) + + // Warmup buffers + for i := 0; i < 10; i++ { + populateTimeSeries(batch, seriesBuff, true, true) + buildWriteRequest(noopLogger, seriesBuff, nil, pBuf, &buff, nil, "snappy") + } + + b.ResetTimer() + totalSize := 0 + for i := 0; i < b.N; i++ { + populateTimeSeries(batch, seriesBuff, true, true) + req, _, _, err := buildWriteRequest(noopLogger, seriesBuff, nil, pBuf, &buff, nil, "snappy") + if err != nil { + b.Fatal(err) + } + totalSize += len(req) + b.ReportMetric(float64(totalSize)/float64(b.N), "compressedSize/op") + } + } + + twoBatch := createDummyTimeSeries(2) + tenBatch := createDummyTimeSeries(10) + hundredBatch := createDummyTimeSeries(100) + + b.Run("2 instances", func(b *testing.B) { + bench(b, twoBatch) + }) + + b.Run("10 instances", func(b *testing.B) { + bench(b, tenBatch) + }) + + b.Run("1k instances", func(b *testing.B) { + bench(b, hundredBatch) + }) +} + +func BenchmarkBuildV2WriteRequest(b *testing.B) { + noopLogger := log.NewNopLogger() + type testcase struct { + batch []timeSeries + } + testCases := []testcase{ + {createDummyTimeSeries(2)}, + {createDummyTimeSeries(10)}, + {createDummyTimeSeries(100)}, + } + for _, tc := range testCases { + symbolTable := writev2.NewSymbolTable() + buff := make([]byte, 0) + seriesBuff := make([]writev2.TimeSeries, len(tc.batch)) + for i := range seriesBuff { + seriesBuff[i].Samples = []writev2.Sample{{}} + seriesBuff[i].Exemplars = []writev2.Exemplar{{}} + } + pBuf := []byte{} + + // Warmup buffers + for i := 0; i < 10; i++ { + populateV2TimeSeries(&symbolTable, tc.batch, seriesBuff, true, true) + buildV2WriteRequest(noopLogger, seriesBuff, symbolTable.Symbols(), &pBuf, &buff, nil, "snappy") + } + + b.Run(fmt.Sprintf("%d-instances", len(tc.batch)), func(b *testing.B) { + totalSize := 0 + for j := 0; j < b.N; j++ { + populateV2TimeSeries(&symbolTable, tc.batch, seriesBuff, true, true) + b.ResetTimer() + req, _, _, err := buildV2WriteRequest(noopLogger, seriesBuff, symbolTable.Symbols(), &pBuf, &buff, nil, "snappy") + if err != nil { + b.Fatal(err) + } + symbolTable.Reset() + totalSize += len(req) + b.ReportMetric(float64(totalSize)/float64(b.N), "compressedSize/op") + } + }) + } +} + func TestDropOldTimeSeries(t *testing.T) { size := 10 nSeries := 6 nSamples := config.DefaultQueueConfig.Capacity * size samples, newSamples, series := createTimeseriesWithOldSamples(nSamples, nSeries) - c := NewTestWriteClient() + // TODO(alexg): test with new version + c := NewTestWriteClient(config.RemoteWriteProtoMsgV1) c.expectSamples(newSamples, series) cfg := config.DefaultQueueConfig mcfg := config.DefaultMetadataConfig cfg.MaxShards = 1 cfg.SampleAgeLimit = model.Duration(60 * time.Second) - m := newTestQueueManager(t, cfg, mcfg, defaultFlushDeadline, c) + m := newTestQueueManager(t, cfg, mcfg, defaultFlushDeadline, c, config.RemoteWriteProtoMsgV1) m.StoreSeries(series, 0) m.Start() defer m.Stop() m.Append(samples) - c.waitForExpectedData(t) + c.waitForExpectedData(t, 30*time.Second) } func TestIsSampleOld(t *testing.T) { @@ -1511,9 +1991,8 @@ func TestSendSamplesWithBackoffWithSampleAgeLimit(t *testing.T) { metadataCfg.Send = true metadataCfg.SendInterval = model.Duration(time.Second * 60) metadataCfg.MaxSamplesPerSend = maxSamplesPerSend - c := NewTestWriteClient() - c.withWaitGroup = false - m := newTestQueueManager(t, cfg, metadataCfg, time.Second, c) + c := NewTestWriteClient(config.RemoteWriteProtoMsgV1) + m := newTestQueueManager(t, cfg, metadataCfg, time.Second, c, config.RemoteWriteProtoMsgV1) m.Start() diff --git a/storage/remote/read_handler_test.go b/storage/remote/read_handler_test.go index 452b292210..a681872687 100644 --- a/storage/remote/read_handler_test.go +++ b/storage/remote/read_handler_test.go @@ -124,7 +124,7 @@ func TestSampledReadEndpoint(t *testing.T) { {Name: "d", Value: "e"}, }, Histograms: []prompb.Histogram{ - FloatHistogramToHistogramProto(0, tsdbutil.GenerateTestFloatHistogram(0)), + prompb.FromFloatHistogram(0, tsdbutil.GenerateTestFloatHistogram(0)), }, }, }, diff --git a/storage/remote/read_test.go b/storage/remote/read_test.go index 810009af0f..357bdba1f5 100644 --- a/storage/remote/read_test.go +++ b/storage/remote/read_test.go @@ -92,7 +92,7 @@ func TestNoDuplicateReadConfigs(t *testing.T) { for _, tc := range cases { t.Run("", func(t *testing.T) { - s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil) + s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil, false) conf := &config.Config{ GlobalConfig: config.DefaultGlobalConfig, RemoteReadConfigs: tc.cfgs, @@ -172,12 +172,12 @@ func TestSeriesSetFilter(t *testing.T) { toRemove: []string{"foo"}, in: &prompb.QueryResult{ Timeseries: []*prompb.TimeSeries{ - {Labels: LabelsToLabelsProto(labels.FromStrings("foo", "bar", "a", "b"), nil)}, + {Labels: prompb.FromLabels(labels.FromStrings("foo", "bar", "a", "b"), nil)}, }, }, expected: &prompb.QueryResult{ Timeseries: []*prompb.TimeSeries{ - {Labels: LabelsToLabelsProto(labels.FromStrings("a", "b"), nil)}, + {Labels: prompb.FromLabels(labels.FromStrings("a", "b"), nil)}, }, }, }, @@ -211,7 +211,7 @@ func (c *mockedRemoteClient) Read(_ context.Context, query *prompb.Query) (*prom q := &prompb.QueryResult{} for _, s := range c.store { - l := LabelProtosToLabels(&c.b, s.Labels) + l := s.ToLabels(&c.b, nil) var notMatch bool for _, m := range matchers { diff --git a/storage/remote/storage.go b/storage/remote/storage.go index 758ba3cc91..afa2d411a9 100644 --- a/storage/remote/storage.go +++ b/storage/remote/storage.go @@ -62,7 +62,7 @@ type Storage struct { } // NewStorage returns a remote.Storage. -func NewStorage(l log.Logger, reg prometheus.Registerer, stCallback startTimeCallback, walDir string, flushDeadline time.Duration, sm ReadyScrapeManager) *Storage { +func NewStorage(l log.Logger, reg prometheus.Registerer, stCallback startTimeCallback, walDir string, flushDeadline time.Duration, sm ReadyScrapeManager, metadataInWAL bool) *Storage { if l == nil { l = log.NewNopLogger() } @@ -72,7 +72,7 @@ func NewStorage(l log.Logger, reg prometheus.Registerer, stCallback startTimeCal logger: logger, localStartTimeCallback: stCallback, } - s.rws = NewWriteStorage(s.logger, reg, walDir, flushDeadline, sm) + s.rws = NewWriteStorage(s.logger, reg, walDir, flushDeadline, sm, metadataInWAL) return s } diff --git a/storage/remote/storage_test.go b/storage/remote/storage_test.go index a62cd2da39..8c97d870e9 100644 --- a/storage/remote/storage_test.go +++ b/storage/remote/storage_test.go @@ -29,7 +29,7 @@ import ( func TestStorageLifecycle(t *testing.T) { dir := t.TempDir() - s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil) + s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil, false) conf := &config.Config{ GlobalConfig: config.DefaultGlobalConfig, RemoteWriteConfigs: []*config.RemoteWriteConfig{ @@ -56,7 +56,7 @@ func TestStorageLifecycle(t *testing.T) { func TestUpdateRemoteReadConfigs(t *testing.T) { dir := t.TempDir() - s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil) + s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil, false) conf := &config.Config{ GlobalConfig: config.GlobalConfig{}, @@ -77,7 +77,7 @@ func TestUpdateRemoteReadConfigs(t *testing.T) { func TestFilterExternalLabels(t *testing.T) { dir := t.TempDir() - s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil) + s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil, false) conf := &config.Config{ GlobalConfig: config.GlobalConfig{ @@ -102,7 +102,7 @@ func TestFilterExternalLabels(t *testing.T) { func TestIgnoreExternalLabels(t *testing.T) { dir := t.TempDir() - s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil) + s := NewStorage(nil, nil, nil, dir, defaultFlushDeadline, nil, false) conf := &config.Config{ GlobalConfig: config.GlobalConfig{ @@ -154,7 +154,7 @@ func baseRemoteReadConfig(host string) *config.RemoteReadConfig { // ApplyConfig runs concurrently with Notify // See https://github.com/prometheus/prometheus/issues/12747 func TestWriteStorageApplyConfigsDuringCommit(t *testing.T) { - s := NewStorage(nil, nil, nil, t.TempDir(), defaultFlushDeadline, nil) + s := NewStorage(nil, nil, nil, t.TempDir(), defaultFlushDeadline, nil, false) var wg sync.WaitGroup wg.Add(2000) diff --git a/storage/remote/write.go b/storage/remote/write.go index 66455cb4dd..cd8cd588ca 100644 --- a/storage/remote/write.go +++ b/storage/remote/write.go @@ -15,6 +15,7 @@ package remote import ( "context" + "errors" "fmt" "math" "sync" @@ -65,6 +66,7 @@ type WriteStorage struct { externalLabels labels.Labels dir string queues map[string]*QueueManager + metadataInWAL bool samplesIn *ewmaRate flushDeadline time.Duration interner *pool @@ -76,7 +78,7 @@ type WriteStorage struct { } // NewWriteStorage creates and runs a WriteStorage. -func NewWriteStorage(logger log.Logger, reg prometheus.Registerer, dir string, flushDeadline time.Duration, sm ReadyScrapeManager) *WriteStorage { +func NewWriteStorage(logger log.Logger, reg prometheus.Registerer, dir string, flushDeadline time.Duration, sm ReadyScrapeManager, metadataInWal bool) *WriteStorage { if logger == nil { logger = log.NewNopLogger() } @@ -92,6 +94,7 @@ func NewWriteStorage(logger log.Logger, reg prometheus.Registerer, dir string, f interner: newPool(), scraper: sm, quit: make(chan struct{}), + metadataInWAL: metadataInWal, highestTimestamp: &maxTimestamp{ Gauge: prometheus.NewGauge(prometheus.GaugeOpts{ Namespace: namespace, @@ -145,6 +148,9 @@ func (rws *WriteStorage) ApplyConfig(conf *config.Config) error { newQueues := make(map[string]*QueueManager) newHashes := []string{} for _, rwConf := range conf.RemoteWriteConfigs { + if rwConf.ProtobufMessage == config.RemoteWriteProtoMsgV2 && !rws.metadataInWAL { + return errors.New("invalid remote write configuration, if you are using remote write version 2.0 the `--enable-feature=metadata-wal-records` feature flag must be enabled") + } hash, err := toHash(rwConf) if err != nil { return err @@ -165,6 +171,7 @@ func (rws *WriteStorage) ApplyConfig(conf *config.Config) error { c, err := NewWriteClient(name, &ClientConfig{ URL: rwConf.URL, + WriteProtoMsg: rwConf.ProtobufMessage, Timeout: rwConf.RemoteTimeout, HTTPClientConfig: rwConf.HTTPClientConfig, SigV4Config: rwConf.SigV4Config, @@ -207,6 +214,7 @@ func (rws *WriteStorage) ApplyConfig(conf *config.Config) error { rws.scraper, rwConf.SendExemplars, rwConf.SendNativeHistograms, + rwConf.ProtobufMessage, ) // Keep track of which queues are new so we know which to start. newHashes = append(newHashes, hash) diff --git a/storage/remote/write_handler.go b/storage/remote/write_handler.go index 0832c65abe..9997811ab0 100644 --- a/storage/remote/write_handler.go +++ b/storage/remote/write_handler.go @@ -17,19 +17,24 @@ import ( "context" "errors" "fmt" + "io" "net/http" + "strings" "time" "github.com/go-kit/log" "github.com/go-kit/log/level" - + "github.com/gogo/protobuf/proto" + "github.com/golang/snappy" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/model/exemplar" "github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/timestamp" "github.com/prometheus/prometheus/prompb" + writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2" "github.com/prometheus/prometheus/storage" otlptranslator "github.com/prometheus/prometheus/storage/remote/otlptranslator/prometheusremotewrite" ) @@ -39,17 +44,23 @@ type writeHandler struct { appendable storage.Appendable samplesWithInvalidLabelsTotal prometheus.Counter + + acceptedProtoMsgs map[config.RemoteWriteProtoMsg]struct{} } const maxAheadTime = 10 * time.Minute -// NewWriteHandler creates a http.Handler that accepts remote write requests and -// writes them to the provided appendable. -func NewWriteHandler(logger log.Logger, reg prometheus.Registerer, appendable storage.Appendable) http.Handler { +// NewWriteHandler creates a http.Handler that accepts remote write requests with +// the given message in acceptedProtoMsgs and writes them to the provided appendable. +func NewWriteHandler(logger log.Logger, reg prometheus.Registerer, appendable storage.Appendable, acceptedProtoMsgs []config.RemoteWriteProtoMsg) http.Handler { + protoMsgs := map[config.RemoteWriteProtoMsg]struct{}{} + for _, acc := range acceptedProtoMsgs { + protoMsgs[acc] = struct{}{} + } h := &writeHandler{ - logger: logger, - appendable: appendable, - + logger: logger, + appendable: appendable, + acceptedProtoMsgs: protoMsgs, samplesWithInvalidLabelsTotal: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: "prometheus", Subsystem: "api", @@ -63,15 +74,107 @@ func NewWriteHandler(logger log.Logger, reg prometheus.Registerer, appendable st return h } +func (h *writeHandler) parseProtoMsg(contentType string) (config.RemoteWriteProtoMsg, error) { + contentType = strings.TrimSpace(contentType) + + parts := strings.Split(contentType, ";") + if parts[0] != appProtoContentType { + return "", fmt.Errorf("expected %v as the first (media) part, got %v content-type", appProtoContentType, contentType) + } + // Parse potential https://www.rfc-editor.org/rfc/rfc9110#parameter + for _, p := range parts[1:] { + pair := strings.Split(p, "=") + if len(pair) != 2 { + return "", fmt.Errorf("as per https://www.rfc-editor.org/rfc/rfc9110#parameter expected parameters to be key-values, got %v in %v content-type", p, contentType) + } + if pair[0] == "proto" { + ret := config.RemoteWriteProtoMsg(pair[1]) + if err := ret.Validate(); err != nil { + return "", fmt.Errorf("got %v content type; %w", contentType, err) + } + return ret, nil + } + } + // No "proto=" parameter, assuming v1. + return config.RemoteWriteProtoMsgV1, nil +} + func (h *writeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - req, err := DecodeWriteRequest(r.Body) + contentType := r.Header.Get("Content-Type") + if contentType == "" { + // Don't break yolo 1.0 clients if not needed. This is similar to what we did + // before 2.0: https://github.com/prometheus/prometheus/blob/d78253319daa62c8f28ed47e40bafcad2dd8b586/storage/remote/write_handler.go#L62 + // We could give http.StatusUnsupportedMediaType, but let's assume 1.0 message by default. + contentType = appProtoContentType + } + + msg, err := h.parseProtoMsg(contentType) + if err != nil { + level.Error(h.logger).Log("msg", "Error decoding remote write request", "err", err) + http.Error(w, err.Error(), http.StatusUnsupportedMediaType) + return + } + + if _, ok := h.acceptedProtoMsgs[msg]; !ok { + err := fmt.Errorf("%v protobuf message is not accepted by this server; accepted %v", msg, func() (ret []string) { + for k := range h.acceptedProtoMsgs { + ret = append(ret, string(k)) + } + return ret + }()) + level.Error(h.logger).Log("msg", "Error decoding remote write request", "err", err) + http.Error(w, err.Error(), http.StatusUnsupportedMediaType) + } + + enc := r.Header.Get("Content-Encoding") + if enc == "" { + // Don't break yolo 1.0 clients if not needed. This is similar to what we did + // before 2.0: https://github.com/prometheus/prometheus/blob/d78253319daa62c8f28ed47e40bafcad2dd8b586/storage/remote/write_handler.go#L62 + // We could give http.StatusUnsupportedMediaType, but let's assume snappy by default. + } else if enc != string(SnappyBlockCompression) { + err := fmt.Errorf("%v encoding (compression) is not accepted by this server; only %v is acceptable", enc, SnappyBlockCompression) + level.Error(h.logger).Log("msg", "Error decoding remote write request", "err", err) + http.Error(w, err.Error(), http.StatusUnsupportedMediaType) + } + + // Read the request body. + body, err := io.ReadAll(r.Body) if err != nil { level.Error(h.logger).Log("msg", "Error decoding remote write request", "err", err.Error()) http.Error(w, err.Error(), http.StatusBadRequest) return } - err = h.write(r.Context(), req) + decompressed, err := snappy.Decode(nil, body) + if err != nil { + // TODO(bwplotka): Add more context to responded error? + level.Error(h.logger).Log("msg", "Error decompressing remote write request", "err", err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Now we have a decompressed buffer we can unmarshal it. + switch msg { + case config.RemoteWriteProtoMsgV1: + var req prompb.WriteRequest + if err := proto.Unmarshal(decompressed, &req); err != nil { + // TODO(bwplotka): Add more context to responded error? + level.Error(h.logger).Log("msg", "Error decoding v1 remote write request", "protobuf_message", msg, "err", err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + err = h.write(r.Context(), &req) + case config.RemoteWriteProtoMsgV2: + var req writev2.Request + if err := proto.Unmarshal(decompressed, &req); err != nil { + // TODO(bwplotka): Add more context to responded error? + level.Error(h.logger).Log("msg", "Error decoding v2 remote write request", "protobuf_message", msg, "err", err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + err = h.writeV2(r.Context(), &req) + } + switch { case err == nil: case errors.Is(err, storage.ErrOutOfOrderSample), errors.Is(err, storage.ErrOutOfBounds), errors.Is(err, storage.ErrDuplicateSampleForTimestamp), errors.Is(err, storage.ErrTooOldSample): @@ -123,62 +226,27 @@ func (h *writeHandler) write(ctx context.Context, req *prompb.WriteRequest) (err }() b := labels.NewScratchBuilder(0) - var exemplarErr error - for _, ts := range req.Timeseries { - labels := LabelProtosToLabels(&b, ts.Labels) - if !labels.IsValid() { - level.Warn(h.logger).Log("msg", "Invalid metric names or labels", "got", labels.String()) + ls := ts.ToLabels(&b, nil) + if !ls.IsValid() { + level.Warn(h.logger).Log("msg", "Invalid metric names or labels", "got", ls.String()) samplesWithInvalidLabels++ continue } - var ref storage.SeriesRef - for _, s := range ts.Samples { - ref, err = timeLimitApp.Append(ref, labels, s.Timestamp, s.Value) - if err != nil { - unwrappedErr := errors.Unwrap(err) - if unwrappedErr == nil { - unwrappedErr = err - } - if errors.Is(err, storage.ErrOutOfOrderSample) || errors.Is(unwrappedErr, storage.ErrOutOfBounds) || errors.Is(unwrappedErr, storage.ErrDuplicateSampleForTimestamp) { - level.Error(h.logger).Log("msg", "Out of order sample from remote write", "err", err.Error(), "series", labels.String(), "timestamp", s.Timestamp) - } - return err - } + + err := h.appendSamples(timeLimitApp, ts.Samples, ls) + if err != nil { + return err } for _, ep := range ts.Exemplars { - e := exemplarProtoToExemplar(&b, ep) - - _, exemplarErr = timeLimitApp.AppendExemplar(0, labels, e) - exemplarErr = h.checkAppendExemplarError(exemplarErr, e, &outOfOrderExemplarErrs) - if exemplarErr != nil { - // Since exemplar storage is still experimental, we don't fail the request on ingestion errors. - level.Debug(h.logger).Log("msg", "Error while adding exemplar in AddExemplar", "exemplar", fmt.Sprintf("%+v", e), "err", exemplarErr) - } + e := ep.ToExemplar(&b, nil) + h.appendExemplar(timeLimitApp, e, ls, &outOfOrderExemplarErrs) } - for _, hp := range ts.Histograms { - if hp.IsFloatHistogram() { - fhs := FloatHistogramProtoToFloatHistogram(hp) - _, err = timeLimitApp.AppendHistogram(0, labels, hp.Timestamp, nil, fhs) - } else { - hs := HistogramProtoToHistogram(hp) - _, err = timeLimitApp.AppendHistogram(0, labels, hp.Timestamp, hs, nil) - } - - if err != nil { - unwrappedErr := errors.Unwrap(err) - if unwrappedErr == nil { - unwrappedErr = err - } - // Although AppendHistogram does not currently return ErrDuplicateSampleForTimestamp there is - // a note indicating its inclusion in the future. - if errors.Is(unwrappedErr, storage.ErrOutOfOrderSample) || errors.Is(unwrappedErr, storage.ErrOutOfBounds) || errors.Is(unwrappedErr, storage.ErrDuplicateSampleForTimestamp) { - level.Error(h.logger).Log("msg", "Out of order histogram from remote write", "err", err.Error(), "series", labels.String(), "timestamp", hp.Timestamp) - } - return err - } + err = h.appendHistograms(timeLimitApp, ts.Histograms, ls) + if err != nil { + return err } } @@ -192,6 +260,149 @@ func (h *writeHandler) write(ctx context.Context, req *prompb.WriteRequest) (err return nil } +func (h *writeHandler) writeV2(ctx context.Context, req *writev2.Request) (err error) { + outOfOrderExemplarErrs := 0 + + timeLimitApp := &timeLimitAppender{ + Appender: h.appendable.Appender(ctx), + maxTime: timestamp.FromTime(time.Now().Add(maxAheadTime)), + } + + defer func() { + if err != nil { + _ = timeLimitApp.Rollback() + return + } + err = timeLimitApp.Commit() + }() + + b := labels.NewScratchBuilder(0) + for _, ts := range req.Timeseries { + ls := ts.ToLabels(&b, req.Symbols) + + err := h.appendSamplesV2(timeLimitApp, ts.Samples, ls) + if err != nil { + return err + } + + for _, ep := range ts.Exemplars { + e := ep.ToExemplar(&b, req.Symbols) + h.appendExemplar(timeLimitApp, e, ls, &outOfOrderExemplarErrs) + } + + err = h.appendHistogramsV2(timeLimitApp, ts.Histograms, ls) + if err != nil { + return err + } + + m := ts.ToMetadata(req.Symbols) + if _, err = timeLimitApp.UpdateMetadata(0, ls, m); err != nil { + level.Debug(h.logger).Log("msg", "error while updating metadata from remote write", "err", err) + } + } + + if outOfOrderExemplarErrs > 0 { + _ = level.Warn(h.logger).Log("msg", "Error on ingesting out-of-order exemplars", "num_dropped", outOfOrderExemplarErrs) + } + + return nil +} + +func (h *writeHandler) appendExemplar(app storage.Appender, e exemplar.Exemplar, labels labels.Labels, outOfOrderExemplarErrs *int) { + _, err := app.AppendExemplar(0, labels, e) + err = h.checkAppendExemplarError(err, e, outOfOrderExemplarErrs) + if err != nil { + // Since exemplar storage is still experimental, we don't fail the request on ingestion errors + level.Debug(h.logger).Log("msg", "Error while adding exemplar in AddExemplar", "exemplar", fmt.Sprintf("%+v", e), "err", err) + } +} + +func (h *writeHandler) appendSamples(app storage.Appender, ss []prompb.Sample, labels labels.Labels) error { + var ref storage.SeriesRef + var err error + for _, s := range ss { + ref, err = app.Append(ref, labels, s.GetTimestamp(), s.GetValue()) + if err != nil { + unwrappedErr := errors.Unwrap(err) + if unwrappedErr == nil { + unwrappedErr = err + } + if errors.Is(err, storage.ErrOutOfOrderSample) || errors.Is(unwrappedErr, storage.ErrOutOfBounds) || errors.Is(unwrappedErr, storage.ErrDuplicateSampleForTimestamp) { + level.Error(h.logger).Log("msg", "Out of order sample from remote write", "err", err.Error(), "series", labels.String(), "timestamp", s.Timestamp) + } + return err + } + } + return nil +} + +func (h *writeHandler) appendSamplesV2(app storage.Appender, ss []writev2.Sample, labels labels.Labels) error { + var ref storage.SeriesRef + var err error + for _, s := range ss { + ref, err = app.Append(ref, labels, s.GetTimestamp(), s.GetValue()) + if err != nil { + unwrappedErr := errors.Unwrap(err) + if unwrappedErr == nil { + unwrappedErr = err + } + if errors.Is(err, storage.ErrOutOfOrderSample) || errors.Is(unwrappedErr, storage.ErrOutOfBounds) || errors.Is(unwrappedErr, storage.ErrDuplicateSampleForTimestamp) { + level.Error(h.logger).Log("msg", "Out of order sample from remote write", "err", err.Error(), "series", labels.String(), "timestamp", s.Timestamp) + } + return err + } + } + return nil +} + +func (h *writeHandler) appendHistograms(app storage.Appender, hh []prompb.Histogram, labels labels.Labels) error { + var err error + for _, hp := range hh { + if hp.IsFloatHistogram() { + _, err = app.AppendHistogram(0, labels, hp.Timestamp, nil, hp.ToFloatHistogram()) + } else { + _, err = app.AppendHistogram(0, labels, hp.Timestamp, hp.ToIntHistogram(), nil) + } + if err != nil { + unwrappedErr := errors.Unwrap(err) + if unwrappedErr == nil { + unwrappedErr = err + } + // Although AppendHistogram does not currently return ErrDuplicateSampleForTimestamp there is + // a note indicating its inclusion in the future. + if errors.Is(unwrappedErr, storage.ErrOutOfOrderSample) || errors.Is(unwrappedErr, storage.ErrOutOfBounds) || errors.Is(unwrappedErr, storage.ErrDuplicateSampleForTimestamp) { + level.Error(h.logger).Log("msg", "Out of order histogram from remote write", "err", err.Error(), "series", labels.String(), "timestamp", hp.Timestamp) + } + return err + } + } + return nil +} + +func (h *writeHandler) appendHistogramsV2(app storage.Appender, hh []writev2.Histogram, labels labels.Labels) error { + var err error + for _, hp := range hh { + if hp.IsFloatHistogram() { + _, err = app.AppendHistogram(0, labels, hp.Timestamp, nil, hp.ToFloatHistogram()) + } else { + _, err = app.AppendHistogram(0, labels, hp.Timestamp, hp.ToIntHistogram(), nil) + } + if err != nil { + unwrappedErr := errors.Unwrap(err) + if unwrappedErr == nil { + unwrappedErr = err + } + // Although AppendHistogram does not currently return ErrDuplicateSampleForTimestamp there is + // a note indicating its inclusion in the future. + if errors.Is(unwrappedErr, storage.ErrOutOfOrderSample) || errors.Is(unwrappedErr, storage.ErrOutOfBounds) || errors.Is(unwrappedErr, storage.ErrDuplicateSampleForTimestamp) { + level.Error(h.logger).Log("msg", "Out of order histogram from remote write", "err", err.Error(), "series", labels.String(), "timestamp", hp.Timestamp) + } + return err + } + } + return nil +} + // NewOTLPWriteHandler creates a http.Handler that accepts OTLP write requests and // writes them to the provided appendable. func NewOTLPWriteHandler(logger log.Logger, appendable storage.Appendable) http.Handler { diff --git a/storage/remote/write_handler_test.go b/storage/remote/write_handler_test.go index 30dc1b3d69..24bd7059ae 100644 --- a/storage/remote/write_handler_test.go +++ b/storage/remote/write_handler_test.go @@ -30,25 +30,230 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" + "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/model/exemplar" "github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/metadata" "github.com/prometheus/prometheus/prompb" + writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2" "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/tsdb" "github.com/prometheus/prometheus/util/testutil" ) -func TestRemoteWriteHandler(t *testing.T) { - buf, _, _, err := buildWriteRequest(nil, writeRequestFixture.Timeseries, nil, nil, nil, nil) +func TestRemoteWriteHandlerHeadersHandling_V1Message(t *testing.T) { + payload, _, _, err := buildWriteRequest(nil, writeRequestFixture.Timeseries, nil, nil, nil, nil, "snappy") require.NoError(t, err) - req, err := http.NewRequest("", "", bytes.NewReader(buf)) + for _, tc := range []struct { + name string + reqHeaders map[string]string + expectedCode int + }{ + // Generally Prometheus 1.0 Receiver never checked for existence of the headers, so + // we keep things permissive. + { + name: "correct PRW 1.0 headers", + reqHeaders: map[string]string{ + "Content-Type": remoteWriteContentTypeHeaders[config.RemoteWriteProtoMsgV1], + "Content-Encoding": string(SnappyBlockCompression), + RemoteWriteVersionHeader: RemoteWriteVersion20HeaderValue, + }, + expectedCode: http.StatusNoContent, + }, + { + name: "missing remote write version", + reqHeaders: map[string]string{ + "Content-Type": remoteWriteContentTypeHeaders[config.RemoteWriteProtoMsgV1], + "Content-Encoding": string(SnappyBlockCompression), + }, + expectedCode: http.StatusNoContent, + }, + { + name: "no headers", + reqHeaders: map[string]string{}, + expectedCode: http.StatusNoContent, + }, + { + name: "missing content-type", + reqHeaders: map[string]string{ + "Content-Encoding": string(SnappyBlockCompression), + RemoteWriteVersionHeader: RemoteWriteVersion20HeaderValue, + }, + expectedCode: http.StatusNoContent, + }, + { + name: "missing content-encoding", + reqHeaders: map[string]string{ + "Content-Type": remoteWriteContentTypeHeaders[config.RemoteWriteProtoMsgV1], + RemoteWriteVersionHeader: RemoteWriteVersion20HeaderValue, + }, + expectedCode: http.StatusNoContent, + }, + { + name: "wrong content-type", + reqHeaders: map[string]string{ + "Content-Type": "yolo", + "Content-Encoding": string(SnappyBlockCompression), + RemoteWriteVersionHeader: RemoteWriteVersion20HeaderValue, + }, + expectedCode: http.StatusUnsupportedMediaType, + }, + { + name: "wrong content-type2", + reqHeaders: map[string]string{ + "Content-Type": appProtoContentType + ";proto=yolo", + "Content-Encoding": string(SnappyBlockCompression), + RemoteWriteVersionHeader: RemoteWriteVersion20HeaderValue, + }, + expectedCode: http.StatusUnsupportedMediaType, + }, + { + name: "not supported content-encoding", + reqHeaders: map[string]string{ + "Content-Type": remoteWriteContentTypeHeaders[config.RemoteWriteProtoMsgV1], + "Content-Encoding": "zstd", + RemoteWriteVersionHeader: RemoteWriteVersion20HeaderValue, + }, + expectedCode: http.StatusUnsupportedMediaType, + }, + } { + t.Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest("", "", bytes.NewReader(payload)) + require.NoError(t, err) + for k, v := range tc.reqHeaders { + req.Header.Set(k, v) + } + + appendable := &mockAppendable{} + handler := NewWriteHandler(log.NewNopLogger(), nil, appendable, []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1}) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + resp := recorder.Result() + out, err := io.ReadAll(resp.Body) + require.NoError(t, err) + _ = resp.Body.Close() + require.Equal(t, tc.expectedCode, resp.StatusCode, string(out)) + }) + } +} + +func TestRemoteWriteHandlerHeadersHandling_V2Message(t *testing.T) { + payload, _, _, err := buildV2WriteRequest(log.NewNopLogger(), writeV2RequestFixture.Timeseries, writeV2RequestFixture.Symbols, nil, nil, nil, "snappy") require.NoError(t, err) + for _, tc := range []struct { + name string + reqHeaders map[string]string + expectedCode int + }{ + { + name: "correct PRW 2.0 headers", + reqHeaders: map[string]string{ + "Content-Type": remoteWriteContentTypeHeaders[config.RemoteWriteProtoMsgV2], + "Content-Encoding": string(SnappyBlockCompression), + RemoteWriteVersionHeader: RemoteWriteVersion20HeaderValue, + }, + expectedCode: http.StatusNoContent, + }, + { + name: "missing remote write version", + reqHeaders: map[string]string{ + "Content-Type": remoteWriteContentTypeHeaders[config.RemoteWriteProtoMsgV2], + "Content-Encoding": string(SnappyBlockCompression), + }, + expectedCode: http.StatusNoContent, // We don't check for now. + }, + { + name: "no headers", + reqHeaders: map[string]string{}, + expectedCode: http.StatusUnsupportedMediaType, + }, + { + name: "missing content-type", + reqHeaders: map[string]string{ + "Content-Encoding": string(SnappyBlockCompression), + RemoteWriteVersionHeader: RemoteWriteVersion20HeaderValue, + }, + // This only gives 415, because we explicitly only support 2.0. If we supported both + // (default) it would be empty message parsed and ok response. + // This is perhaps better, than 415 for previously working 1.0 flow with + // no content-type. + expectedCode: http.StatusUnsupportedMediaType, + }, + { + name: "missing content-encoding", + reqHeaders: map[string]string{ + "Content-Type": remoteWriteContentTypeHeaders[config.RemoteWriteProtoMsgV2], + RemoteWriteVersionHeader: RemoteWriteVersion20HeaderValue, + }, + expectedCode: http.StatusNoContent, // Similar to 1.0 impl, we default to Snappy, so it works. + }, + { + name: "wrong content-type", + reqHeaders: map[string]string{ + "Content-Type": "yolo", + "Content-Encoding": string(SnappyBlockCompression), + RemoteWriteVersionHeader: RemoteWriteVersion20HeaderValue, + }, + expectedCode: http.StatusUnsupportedMediaType, + }, + { + name: "wrong content-type2", + reqHeaders: map[string]string{ + "Content-Type": appProtoContentType + ";proto=yolo", + "Content-Encoding": string(SnappyBlockCompression), + RemoteWriteVersionHeader: RemoteWriteVersion20HeaderValue, + }, + expectedCode: http.StatusUnsupportedMediaType, + }, + { + name: "not supported content-encoding", + reqHeaders: map[string]string{ + "Content-Type": remoteWriteContentTypeHeaders[config.RemoteWriteProtoMsgV2], + "Content-Encoding": "zstd", + RemoteWriteVersionHeader: RemoteWriteVersion20HeaderValue, + }, + expectedCode: http.StatusUnsupportedMediaType, + }, + } { + t.Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest("", "", bytes.NewReader(payload)) + require.NoError(t, err) + for k, v := range tc.reqHeaders { + req.Header.Set(k, v) + } + + appendable := &mockAppendable{} + handler := NewWriteHandler(log.NewNopLogger(), nil, appendable, []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV2}) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + resp := recorder.Result() + out, err := io.ReadAll(resp.Body) + require.NoError(t, err) + _ = resp.Body.Close() + require.Equal(t, tc.expectedCode, resp.StatusCode, string(out)) + }) + } +} + +func TestRemoteWriteHandler_V1Message(t *testing.T) { + payload, _, _, err := buildWriteRequest(nil, writeRequestFixture.Timeseries, nil, nil, nil, nil, "snappy") + require.NoError(t, err) + + req, err := http.NewRequest("", "", bytes.NewReader(payload)) + require.NoError(t, err) + + // NOTE: Strictly speaking, even for 1.0 we require headers, but we never verified those + // in Prometheus, so keeping like this to not break existing 1.0 clients. + appendable := &mockAppendable{} - handler := NewWriteHandler(nil, nil, appendable) + handler := NewWriteHandler(log.NewNopLogger(), nil, appendable, []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1}) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, req) @@ -61,24 +266,22 @@ func TestRemoteWriteHandler(t *testing.T) { j := 0 k := 0 for _, ts := range writeRequestFixture.Timeseries { - labels := LabelProtosToLabels(&b, ts.Labels) + labels := ts.ToLabels(&b, nil) for _, s := range ts.Samples { requireEqual(t, mockSample{labels, s.Timestamp, s.Value}, appendable.samples[i]) i++ } - for _, e := range ts.Exemplars { - exemplarLabels := LabelProtosToLabels(&b, e.Labels) + exemplarLabels := e.ToExemplar(&b, nil).Labels requireEqual(t, mockExemplar{labels, exemplarLabels, e.Timestamp, e.Value}, appendable.exemplars[j]) j++ } - for _, hp := range ts.Histograms { if hp.IsFloatHistogram() { - fh := FloatHistogramProtoToFloatHistogram(hp) + fh := hp.ToFloatHistogram() requireEqual(t, mockHistogram{labels, hp.Timestamp, nil, fh}, appendable.histograms[k]) } else { - h := HistogramProtoToHistogram(hp) + h := hp.ToIntHistogram() requireEqual(t, mockHistogram{labels, hp.Timestamp, h, nil}, appendable.histograms[k]) } @@ -87,8 +290,66 @@ func TestRemoteWriteHandler(t *testing.T) { } } -func TestOutOfOrderSample(t *testing.T) { - tests := []struct { +func TestRemoteWriteHandler_V2Message(t *testing.T) { + payload, _, _, err := buildV2WriteRequest(log.NewNopLogger(), writeV2RequestFixture.Timeseries, writeV2RequestFixture.Symbols, nil, nil, nil, "snappy") + require.NoError(t, err) + + req, err := http.NewRequest("", "", bytes.NewReader(payload)) + require.NoError(t, err) + + req.Header.Set("Content-Type", remoteWriteContentTypeHeaders[config.RemoteWriteProtoMsgV2]) + req.Header.Set("Content-Encoding", string(SnappyBlockCompression)) + req.Header.Set(RemoteWriteVersionHeader, RemoteWriteVersion20HeaderValue) + + appendable := &mockAppendable{} + handler := NewWriteHandler(log.NewNopLogger(), nil, appendable, []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV2}) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + resp := recorder.Result() + require.Equal(t, http.StatusNoContent, resp.StatusCode) + + b := labels.NewScratchBuilder(0) + i := 0 + j := 0 + k := 0 + for _, ts := range writeV2RequestFixture.Timeseries { + ls := ts.ToLabels(&b, writeV2RequestFixture.Symbols) + + for _, s := range ts.Samples { + requireEqual(t, mockSample{ls, s.Timestamp, s.Value}, appendable.samples[i]) + + switch i { + case 0: + requireEqual(t, mockMetadata{ls, writeV2RequestSeries1Metadata}, appendable.metadata[i]) + case 1: + requireEqual(t, mockMetadata{ls, writeV2RequestSeries2Metadata}, appendable.metadata[i]) + default: + t.Fatal("more series/samples then expected") + } + i++ + } + for _, e := range ts.Exemplars { + exemplarLabels := e.ToExemplar(&b, writeV2RequestFixture.Symbols).Labels + requireEqual(t, mockExemplar{ls, exemplarLabels, e.Timestamp, e.Value}, appendable.exemplars[j]) + j++ + } + for _, hp := range ts.Histograms { + if hp.IsFloatHistogram() { + fh := hp.ToFloatHistogram() + requireEqual(t, mockHistogram{ls, hp.Timestamp, nil, fh}, appendable.histograms[k]) + } else { + h := hp.ToIntHistogram() + requireEqual(t, mockHistogram{ls, hp.Timestamp, h, nil}, appendable.histograms[k]) + } + k++ + } + } +} + +func TestOutOfOrderSample_V1Message(t *testing.T) { + for _, tc := range []struct { Name string Timestamp int64 }{ @@ -100,23 +361,59 @@ func TestOutOfOrderSample(t *testing.T) { Name: "future", Timestamp: math.MaxInt64, }, - } - - for _, tc := range tests { + } { t.Run(tc.Name, func(t *testing.T) { - buf, _, _, err := buildWriteRequest(nil, []prompb.TimeSeries{{ + payload, _, _, err := buildWriteRequest(nil, []prompb.TimeSeries{{ Labels: []prompb.Label{{Name: "__name__", Value: "test_metric"}}, Samples: []prompb.Sample{{Value: 1, Timestamp: tc.Timestamp}}, - }}, nil, nil, nil, nil) + }}, nil, nil, nil, nil, "snappy") require.NoError(t, err) - req, err := http.NewRequest("", "", bytes.NewReader(buf)) + req, err := http.NewRequest("", "", bytes.NewReader(payload)) require.NoError(t, err) - appendable := &mockAppendable{ - latestSample: 100, - } - handler := NewWriteHandler(log.NewNopLogger(), nil, appendable) + appendable := &mockAppendable{latestSample: 100} + handler := NewWriteHandler(log.NewNopLogger(), nil, appendable, []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1}) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + resp := recorder.Result() + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + } +} + +func TestOutOfOrderSample_V2Message(t *testing.T) { + for _, tc := range []struct { + Name string + Timestamp int64 + }{ + { + Name: "historic", + Timestamp: 0, + }, + { + Name: "future", + Timestamp: math.MaxInt64, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + payload, _, _, err := buildV2WriteRequest(nil, []writev2.TimeSeries{{ + LabelsRefs: []uint32{1, 2}, + Samples: []writev2.Sample{{Value: 1, Timestamp: tc.Timestamp}}, + }}, []string{"", "__name__", "metric1"}, nil, nil, nil, "snappy") + require.NoError(t, err) + + req, err := http.NewRequest("", "", bytes.NewReader(payload)) + require.NoError(t, err) + + req.Header.Set("Content-Type", remoteWriteContentTypeHeaders[config.RemoteWriteProtoMsgV2]) + req.Header.Set("Content-Encoding", string(SnappyBlockCompression)) + req.Header.Set(RemoteWriteVersionHeader, RemoteWriteVersion20HeaderValue) + + appendable := &mockAppendable{latestSample: 100} + handler := NewWriteHandler(log.NewNopLogger(), nil, appendable, []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV2}) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, req) @@ -128,9 +425,9 @@ func TestOutOfOrderSample(t *testing.T) { } // This test case currently aims to verify that the WriteHandler endpoint -// don't fail on ingestion errors since the exemplar storage is +// don't fail on exemplar ingestion errors since the exemplar storage is // still experimental. -func TestOutOfOrderExemplar(t *testing.T) { +func TestOutOfOrderExemplar_V1Message(t *testing.T) { tests := []struct { Name string Timestamp int64 @@ -147,19 +444,17 @@ func TestOutOfOrderExemplar(t *testing.T) { for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { - buf, _, _, err := buildWriteRequest(nil, []prompb.TimeSeries{{ + payload, _, _, err := buildWriteRequest(nil, []prompb.TimeSeries{{ Labels: []prompb.Label{{Name: "__name__", Value: "test_metric"}}, Exemplars: []prompb.Exemplar{{Labels: []prompb.Label{{Name: "foo", Value: "bar"}}, Value: 1, Timestamp: tc.Timestamp}}, - }}, nil, nil, nil, nil) + }}, nil, nil, nil, nil, "snappy") require.NoError(t, err) - req, err := http.NewRequest("", "", bytes.NewReader(buf)) + req, err := http.NewRequest("", "", bytes.NewReader(payload)) require.NoError(t, err) - appendable := &mockAppendable{ - latestExemplar: 100, - } - handler := NewWriteHandler(log.NewNopLogger(), nil, appendable) + appendable := &mockAppendable{latestExemplar: 100} + handler := NewWriteHandler(log.NewNopLogger(), nil, appendable, []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1}) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, req) @@ -171,7 +466,7 @@ func TestOutOfOrderExemplar(t *testing.T) { } } -func TestOutOfOrderHistogram(t *testing.T) { +func TestOutOfOrderExemplar_V2Message(t *testing.T) { tests := []struct { Name string Timestamp int64 @@ -188,19 +483,58 @@ func TestOutOfOrderHistogram(t *testing.T) { for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { - buf, _, _, err := buildWriteRequest(nil, []prompb.TimeSeries{{ + payload, _, _, err := buildV2WriteRequest(nil, []writev2.TimeSeries{{ + LabelsRefs: []uint32{1, 2}, + Exemplars: []writev2.Exemplar{{LabelsRefs: []uint32{3, 4}, Value: 1, Timestamp: tc.Timestamp}}, + }}, []string{"", "__name__", "metric1", "foo", "bar"}, nil, nil, nil, "snappy") + require.NoError(t, err) + + req, err := http.NewRequest("", "", bytes.NewReader(payload)) + require.NoError(t, err) + + req.Header.Set("Content-Type", remoteWriteContentTypeHeaders[config.RemoteWriteProtoMsgV2]) + req.Header.Set("Content-Encoding", string(SnappyBlockCompression)) + req.Header.Set(RemoteWriteVersionHeader, RemoteWriteVersion20HeaderValue) + + appendable := &mockAppendable{latestExemplar: 100} + handler := NewWriteHandler(log.NewNopLogger(), nil, appendable, []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV2}) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + resp := recorder.Result() + // TODO: update to require.Equal(t, http.StatusConflict, resp.StatusCode) once exemplar storage is not experimental. + require.Equal(t, http.StatusNoContent, resp.StatusCode) + }) + } +} + +func TestOutOfOrderHistogram_V1Message(t *testing.T) { + for _, tc := range []struct { + Name string + Timestamp int64 + }{ + { + Name: "historic", + Timestamp: 0, + }, + { + Name: "future", + Timestamp: math.MaxInt64, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + payload, _, _, err := buildWriteRequest(nil, []prompb.TimeSeries{{ Labels: []prompb.Label{{Name: "__name__", Value: "test_metric"}}, - Histograms: []prompb.Histogram{HistogramToHistogramProto(tc.Timestamp, &testHistogram), FloatHistogramToHistogramProto(1, testHistogram.ToFloat(nil))}, - }}, nil, nil, nil, nil) + Histograms: []prompb.Histogram{prompb.FromIntHistogram(tc.Timestamp, &testHistogram), prompb.FromFloatHistogram(1, testHistogram.ToFloat(nil))}, + }}, nil, nil, nil, nil, "snappy") require.NoError(t, err) - req, err := http.NewRequest("", "", bytes.NewReader(buf)) + req, err := http.NewRequest("", "", bytes.NewReader(payload)) require.NoError(t, err) - appendable := &mockAppendable{ - latestHistogram: 100, - } - handler := NewWriteHandler(log.NewNopLogger(), nil, appendable) + appendable := &mockAppendable{latestHistogram: 100} + handler := NewWriteHandler(log.NewNopLogger(), nil, appendable, []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1}) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, req) @@ -211,9 +545,49 @@ func TestOutOfOrderHistogram(t *testing.T) { } } -func BenchmarkRemoteWritehandler(b *testing.B) { +func TestOutOfOrderHistogram_V2Message(t *testing.T) { + for _, tc := range []struct { + Name string + Timestamp int64 + }{ + { + Name: "historic", + Timestamp: 0, + }, + { + Name: "future", + Timestamp: math.MaxInt64, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + payload, _, _, err := buildV2WriteRequest(nil, []writev2.TimeSeries{{ + LabelsRefs: []uint32{0, 1}, + Histograms: []writev2.Histogram{writev2.FromIntHistogram(0, &testHistogram), writev2.FromFloatHistogram(1, testHistogram.ToFloat(nil))}, + }}, []string{"__name__", "metric1"}, nil, nil, nil, "snappy") + require.NoError(t, err) + + req, err := http.NewRequest("", "", bytes.NewReader(payload)) + require.NoError(t, err) + + req.Header.Set("Content-Type", remoteWriteContentTypeHeaders[config.RemoteWriteProtoMsgV2]) + req.Header.Set("Content-Encoding", string(SnappyBlockCompression)) + req.Header.Set(RemoteWriteVersionHeader, RemoteWriteVersion20HeaderValue) + + appendable := &mockAppendable{latestHistogram: 100} + handler := NewWriteHandler(log.NewNopLogger(), nil, appendable, []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV2}) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + resp := recorder.Result() + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + } +} + +func BenchmarkRemoteWriteHandler(b *testing.B) { const labelValue = "abcdefg'hijlmn234!@#$%^&*()_+~`\"{}[],./<>?hello0123hiOlá你好Dzieńdobry9Zd8ra765v4stvuyte" - reqs := []*http.Request{} + var reqs []*http.Request for i := 0; i < b.N; i++ { num := strings.Repeat(strconv.Itoa(i), 16) buf, _, _, err := buildWriteRequest(nil, []prompb.TimeSeries{{ @@ -221,8 +595,8 @@ func BenchmarkRemoteWritehandler(b *testing.B) { {Name: "__name__", Value: "test_metric"}, {Name: "test_label_name_" + num, Value: labelValue + num}, }, - Histograms: []prompb.Histogram{HistogramToHistogramProto(0, &testHistogram)}, - }}, nil, nil, nil, nil) + Histograms: []prompb.Histogram{prompb.FromIntHistogram(0, &testHistogram)}, + }}, nil, nil, nil, nil, "snappy") require.NoError(b, err) req, err := http.NewRequest("", "", bytes.NewReader(buf)) require.NoError(b, err) @@ -230,7 +604,8 @@ func BenchmarkRemoteWritehandler(b *testing.B) { } appendable := &mockAppendable{} - handler := NewWriteHandler(log.NewNopLogger(), nil, appendable) + // TODO: test with other proto format(s) + handler := NewWriteHandler(log.NewNopLogger(), nil, appendable, []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1}) recorder := httptest.NewRecorder() b.ResetTimer() @@ -239,17 +614,39 @@ func BenchmarkRemoteWritehandler(b *testing.B) { } } -func TestCommitErr(t *testing.T) { - buf, _, _, err := buildWriteRequest(nil, writeRequestFixture.Timeseries, nil, nil, nil, nil) +func TestCommitErr_V1Message(t *testing.T) { + payload, _, _, err := buildWriteRequest(nil, writeRequestFixture.Timeseries, nil, nil, nil, nil, "snappy") require.NoError(t, err) - req, err := http.NewRequest("", "", bytes.NewReader(buf)) + req, err := http.NewRequest("", "", bytes.NewReader(payload)) require.NoError(t, err) - appendable := &mockAppendable{ - commitErr: fmt.Errorf("commit error"), - } - handler := NewWriteHandler(log.NewNopLogger(), nil, appendable) + appendable := &mockAppendable{commitErr: fmt.Errorf("commit error")} + handler := NewWriteHandler(log.NewNopLogger(), nil, appendable, []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1}) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + resp := recorder.Result() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusInternalServerError, resp.StatusCode) + require.Equal(t, "commit error\n", string(body)) +} + +func TestCommitErr_V2Message(t *testing.T) { + payload, _, _, err := buildV2WriteRequest(log.NewNopLogger(), writeV2RequestFixture.Timeseries, writeV2RequestFixture.Symbols, nil, nil, nil, "snappy") + require.NoError(t, err) + + req, err := http.NewRequest("", "", bytes.NewReader(payload)) + require.NoError(t, err) + + req.Header.Set("Content-Type", remoteWriteContentTypeHeaders[config.RemoteWriteProtoMsgV2]) + req.Header.Set("Content-Encoding", string(SnappyBlockCompression)) + req.Header.Set(RemoteWriteVersionHeader, RemoteWriteVersion20HeaderValue) + + appendable := &mockAppendable{commitErr: fmt.Errorf("commit error")} + handler := NewWriteHandler(log.NewNopLogger(), nil, appendable, []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV2}) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, req) @@ -275,10 +672,10 @@ func BenchmarkRemoteWriteOOOSamples(b *testing.B) { b.Cleanup(func() { require.NoError(b, db.Close()) }) + // TODO: test with other proto format(s) + handler := NewWriteHandler(log.NewNopLogger(), nil, db.Head(), []config.RemoteWriteProtoMsg{config.RemoteWriteProtoMsgV1}) - handler := NewWriteHandler(log.NewNopLogger(), nil, db.Head()) - - buf, _, _, err := buildWriteRequest(nil, genSeriesWithSample(1000, 200*time.Minute.Milliseconds()), nil, nil, nil, nil) + buf, _, _, err := buildWriteRequest(nil, genSeriesWithSample(1000, 200*time.Minute.Milliseconds()), nil, nil, nil, nil, "snappy") require.NoError(b, err) req, err := http.NewRequest("", "", bytes.NewReader(buf)) @@ -291,7 +688,7 @@ func BenchmarkRemoteWriteOOOSamples(b *testing.B) { var bufRequests [][]byte for i := 0; i < 100; i++ { - buf, _, _, err = buildWriteRequest(nil, genSeriesWithSample(1000, int64(80+i)*time.Minute.Milliseconds()), nil, nil, nil, nil) + buf, _, _, err = buildWriteRequest(nil, genSeriesWithSample(1000, int64(80+i)*time.Minute.Milliseconds()), nil, nil, nil, nil, "snappy") require.NoError(b, err) bufRequests = append(bufRequests, buf) } @@ -328,7 +725,9 @@ type mockAppendable struct { exemplars []mockExemplar latestHistogram int64 histograms []mockHistogram - commitErr error + metadata []mockMetadata + + commitErr error } type mockSample struct { @@ -351,10 +750,17 @@ type mockHistogram struct { fh *histogram.FloatHistogram } +type mockMetadata struct { + l labels.Labels + m metadata.Metadata +} + // Wrapper to instruct go-cmp package to compare a list of structs with unexported fields. func requireEqual(t *testing.T, expected, actual interface{}, msgAndArgs ...interface{}) { + t.Helper() + testutil.RequireEqualWithOptions(t, expected, actual, - []cmp.Option{cmp.AllowUnexported(mockSample{}), cmp.AllowUnexported(mockExemplar{}), cmp.AllowUnexported(mockHistogram{})}, + []cmp.Option{cmp.AllowUnexported(mockSample{}), cmp.AllowUnexported(mockExemplar{}), cmp.AllowUnexported(mockHistogram{}), cmp.AllowUnexported(mockMetadata{})}, msgAndArgs...) } @@ -400,13 +806,14 @@ func (m *mockAppendable) AppendHistogram(_ storage.SeriesRef, l labels.Labels, t return 0, nil } -func (m *mockAppendable) UpdateMetadata(_ storage.SeriesRef, _ labels.Labels, _ metadata.Metadata) (storage.SeriesRef, error) { - // TODO: Wire metadata in a mockAppendable field when we get around to handling metadata in remote_write. - // UpdateMetadata is no-op for remote write (where mockAppendable is being used to test) for now. +func (m *mockAppendable) UpdateMetadata(_ storage.SeriesRef, l labels.Labels, mp metadata.Metadata) (storage.SeriesRef, error) { + m.metadata = append(m.metadata, mockMetadata{l: l, m: mp}) return 0, nil } func (m *mockAppendable) AppendCTZeroSample(_ storage.SeriesRef, _ labels.Labels, _, _ int64) (storage.SeriesRef, error) { // AppendCTZeroSample is no-op for remote-write for now. + // TODO(bwplotka): Add support for PRW 2.0 for CT zero feature (but also we might + // replace this with in-metadata CT storage, see https://github.com/prometheus/prometheus/issues/14218). return 0, nil } diff --git a/storage/remote/write_test.go b/storage/remote/write_test.go index c79ac3ab7d..648ec4b17f 100644 --- a/storage/remote/write_test.go +++ b/storage/remote/write_test.go @@ -15,6 +15,7 @@ package remote import ( "bytes" + "errors" "net/http" "net/http/httptest" "net/url" @@ -43,11 +44,12 @@ func testRemoteWriteConfig() *config.RemoteWriteConfig { Host: "localhost", }, }, - QueueConfig: config.DefaultQueueConfig, + QueueConfig: config.DefaultQueueConfig, + ProtobufMessage: config.RemoteWriteProtoMsgV1, } } -func TestNoDuplicateWriteConfigs(t *testing.T) { +func TestWriteStorageApplyConfig_NoDuplicateWriteConfigs(t *testing.T) { dir := t.TempDir() cfg1 := config.RemoteWriteConfig{ @@ -58,7 +60,8 @@ func TestNoDuplicateWriteConfigs(t *testing.T) { Host: "localhost", }, }, - QueueConfig: config.DefaultQueueConfig, + QueueConfig: config.DefaultQueueConfig, + ProtobufMessage: config.RemoteWriteProtoMsgV1, } cfg2 := config.RemoteWriteConfig{ Name: "write-2", @@ -68,7 +71,8 @@ func TestNoDuplicateWriteConfigs(t *testing.T) { Host: "localhost", }, }, - QueueConfig: config.DefaultQueueConfig, + QueueConfig: config.DefaultQueueConfig, + ProtobufMessage: config.RemoteWriteProtoMsgV1, } cfg3 := config.RemoteWriteConfig{ URL: &common_config.URL{ @@ -77,61 +81,49 @@ func TestNoDuplicateWriteConfigs(t *testing.T) { Host: "localhost", }, }, - QueueConfig: config.DefaultQueueConfig, + QueueConfig: config.DefaultQueueConfig, + ProtobufMessage: config.RemoteWriteProtoMsgV1, } - type testcase struct { - cfgs []*config.RemoteWriteConfig - err bool - } - - cases := []testcase{ + for _, tc := range []struct { + cfgs []*config.RemoteWriteConfig + expectedErr error + }{ { // Two duplicates, we should get an error. - cfgs: []*config.RemoteWriteConfig{ - &cfg1, - &cfg1, - }, - err: true, + cfgs: []*config.RemoteWriteConfig{&cfg1, &cfg1}, + expectedErr: errors.New("duplicate remote write configs are not allowed, found duplicate for URL: http://localhost"), }, { // Duplicates but with different names, we should not get an error. - cfgs: []*config.RemoteWriteConfig{ - &cfg1, - &cfg2, - }, - err: false, + cfgs: []*config.RemoteWriteConfig{&cfg1, &cfg2}, }, { // Duplicates but one with no name, we should not get an error. - cfgs: []*config.RemoteWriteConfig{ - &cfg1, - &cfg3, - }, - err: false, + cfgs: []*config.RemoteWriteConfig{&cfg1, &cfg3}, }, { // Duplicates both with no name, we should get an error. - cfgs: []*config.RemoteWriteConfig{ - &cfg3, - &cfg3, - }, - err: true, + cfgs: []*config.RemoteWriteConfig{&cfg3, &cfg3}, + expectedErr: errors.New("duplicate remote write configs are not allowed, found duplicate for URL: http://localhost"), }, - } + } { + t.Run("", func(t *testing.T) { + s := NewWriteStorage(nil, nil, dir, time.Millisecond, nil, false) + conf := &config.Config{ + GlobalConfig: config.DefaultGlobalConfig, + RemoteWriteConfigs: tc.cfgs, + } + err := s.ApplyConfig(conf) + if tc.expectedErr == nil { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Equal(t, tc.expectedErr, err) + } - for _, tc := range cases { - s := NewWriteStorage(nil, nil, dir, time.Millisecond, nil) - conf := &config.Config{ - GlobalConfig: config.DefaultGlobalConfig, - RemoteWriteConfigs: tc.cfgs, - } - err := s.ApplyConfig(conf) - gotError := err != nil - require.Equal(t, tc.err, gotError) - - err = s.Close() - require.NoError(t, err) + require.NoError(t, s.Close()) + }) } } -func TestRestartOnNameChange(t *testing.T) { +func TestWriteStorageApplyConfig_RestartOnNameChange(t *testing.T) { dir := t.TempDir() cfg := testRemoteWriteConfig() @@ -139,13 +131,11 @@ func TestRestartOnNameChange(t *testing.T) { hash, err := toHash(cfg) require.NoError(t, err) - s := NewWriteStorage(nil, nil, dir, time.Millisecond, nil) + s := NewWriteStorage(nil, nil, dir, time.Millisecond, nil, false) conf := &config.Config{ - GlobalConfig: config.DefaultGlobalConfig, - RemoteWriteConfigs: []*config.RemoteWriteConfig{ - cfg, - }, + GlobalConfig: config.DefaultGlobalConfig, + RemoteWriteConfigs: []*config.RemoteWriteConfig{cfg}, } require.NoError(t, s.ApplyConfig(conf)) require.Equal(t, s.queues[hash].client().Name(), cfg.Name) @@ -157,14 +147,13 @@ func TestRestartOnNameChange(t *testing.T) { require.NoError(t, err) require.Equal(t, s.queues[hash].client().Name(), conf.RemoteWriteConfigs[0].Name) - err = s.Close() - require.NoError(t, err) + require.NoError(t, s.Close()) } -func TestUpdateWithRegisterer(t *testing.T) { +func TestWriteStorageApplyConfig_UpdateWithRegisterer(t *testing.T) { dir := t.TempDir() - s := NewWriteStorage(nil, prometheus.NewRegistry(), dir, time.Millisecond, nil) + s := NewWriteStorage(nil, prometheus.NewRegistry(), dir, time.Millisecond, nil, false) c1 := &config.RemoteWriteConfig{ Name: "named", URL: &common_config.URL{ @@ -173,7 +162,8 @@ func TestUpdateWithRegisterer(t *testing.T) { Host: "localhost", }, }, - QueueConfig: config.DefaultQueueConfig, + QueueConfig: config.DefaultQueueConfig, + ProtobufMessage: config.RemoteWriteProtoMsgV1, } c2 := &config.RemoteWriteConfig{ URL: &common_config.URL{ @@ -182,7 +172,8 @@ func TestUpdateWithRegisterer(t *testing.T) { Host: "localhost", }, }, - QueueConfig: config.DefaultQueueConfig, + QueueConfig: config.DefaultQueueConfig, + ProtobufMessage: config.RemoteWriteProtoMsgV1, } conf := &config.Config{ GlobalConfig: config.DefaultGlobalConfig, @@ -197,14 +188,13 @@ func TestUpdateWithRegisterer(t *testing.T) { require.Equal(t, 10, queue.cfg.MaxShards) } - err := s.Close() - require.NoError(t, err) + require.NoError(t, s.Close()) } -func TestWriteStorageLifecycle(t *testing.T) { +func TestWriteStorageApplyConfig_Lifecycle(t *testing.T) { dir := t.TempDir() - s := NewWriteStorage(nil, nil, dir, defaultFlushDeadline, nil) + s := NewWriteStorage(nil, nil, dir, defaultFlushDeadline, nil, false) conf := &config.Config{ GlobalConfig: config.DefaultGlobalConfig, RemoteWriteConfigs: []*config.RemoteWriteConfig{ @@ -214,14 +204,13 @@ func TestWriteStorageLifecycle(t *testing.T) { require.NoError(t, s.ApplyConfig(conf)) require.Len(t, s.queues, 1) - err := s.Close() - require.NoError(t, err) + require.NoError(t, s.Close()) } -func TestUpdateExternalLabels(t *testing.T) { +func TestWriteStorageApplyConfig_UpdateExternalLabels(t *testing.T) { dir := t.TempDir() - s := NewWriteStorage(nil, prometheus.NewRegistry(), dir, time.Second, nil) + s := NewWriteStorage(nil, prometheus.NewRegistry(), dir, time.Second, nil, false) externalLabels := labels.FromStrings("external", "true") conf := &config.Config{ @@ -243,15 +232,13 @@ func TestUpdateExternalLabels(t *testing.T) { require.Len(t, s.queues, 1) require.Equal(t, []labels.Label{{Name: "external", Value: "true"}}, s.queues[hash].externalLabels) - err = s.Close() - require.NoError(t, err) + require.NoError(t, s.Close()) } -func TestWriteStorageApplyConfigsIdempotent(t *testing.T) { +func TestWriteStorageApplyConfig_Idempotent(t *testing.T) { dir := t.TempDir() - s := NewWriteStorage(nil, nil, dir, defaultFlushDeadline, nil) - + s := NewWriteStorage(nil, nil, dir, defaultFlushDeadline, nil, false) conf := &config.Config{ GlobalConfig: config.GlobalConfig{}, RemoteWriteConfigs: []*config.RemoteWriteConfig{ @@ -269,14 +256,13 @@ func TestWriteStorageApplyConfigsIdempotent(t *testing.T) { _, hashExists := s.queues[hash] require.True(t, hashExists, "Queue pointer should have remained the same") - err = s.Close() - require.NoError(t, err) + require.NoError(t, s.Close()) } -func TestWriteStorageApplyConfigsPartialUpdate(t *testing.T) { +func TestWriteStorageApplyConfig_PartialUpdate(t *testing.T) { dir := t.TempDir() - s := NewWriteStorage(nil, nil, dir, defaultFlushDeadline, nil) + s := NewWriteStorage(nil, nil, dir, defaultFlushDeadline, nil, false) c0 := &config.RemoteWriteConfig{ RemoteTimeout: model.Duration(10 * time.Second), @@ -286,6 +272,7 @@ func TestWriteStorageApplyConfigsPartialUpdate(t *testing.T) { Regex: relabel.MustNewRegexp(".+"), }, }, + ProtobufMessage: config.RemoteWriteProtoMsgV1, } c1 := &config.RemoteWriteConfig{ RemoteTimeout: model.Duration(20 * time.Second), @@ -293,10 +280,12 @@ func TestWriteStorageApplyConfigsPartialUpdate(t *testing.T) { HTTPClientConfig: common_config.HTTPClientConfig{ BearerToken: "foo", }, + ProtobufMessage: config.RemoteWriteProtoMsgV1, } c2 := &config.RemoteWriteConfig{ - RemoteTimeout: model.Duration(30 * time.Second), - QueueConfig: config.DefaultQueueConfig, + RemoteTimeout: model.Duration(30 * time.Second), + QueueConfig: config.DefaultQueueConfig, + ProtobufMessage: config.RemoteWriteProtoMsgV1, } conf := &config.Config{ @@ -376,8 +365,7 @@ func TestWriteStorageApplyConfigsPartialUpdate(t *testing.T) { _, hashExists = s.queues[hashes[2]] require.True(t, hashExists, "Pointer of unchanged queue should have remained the same") - err = s.Close() - require.NoError(t, err) + require.NoError(t, s.Close()) } func TestOTLPWriteHandler(t *testing.T) { diff --git a/tsdb/agent/db_test.go b/tsdb/agent/db_test.go index b984e6bc09..b31041b1b9 100644 --- a/tsdb/agent/db_test.go +++ b/tsdb/agent/db_test.go @@ -89,7 +89,7 @@ func createTestAgentDB(t testing.TB, reg prometheus.Registerer, opts *Options) * t.Helper() dbDir := t.TempDir() - rs := remote.NewStorage(log.NewNopLogger(), reg, startTime, dbDir, time.Second*30, nil) + rs := remote.NewStorage(log.NewNopLogger(), reg, startTime, dbDir, time.Second*30, nil, false) t.Cleanup(func() { require.NoError(t, rs.Close()) }) @@ -585,7 +585,7 @@ func TestLockfile(t *testing.T) { tsdbutil.TestDirLockerUsage(t, func(t *testing.T, data string, createLock bool) (*tsdbutil.DirLocker, testutil.Closer) { logger := log.NewNopLogger() reg := prometheus.NewRegistry() - rs := remote.NewStorage(logger, reg, startTime, data, time.Second*30, nil) + rs := remote.NewStorage(logger, reg, startTime, data, time.Second*30, nil, false) t.Cleanup(func() { require.NoError(t, rs.Close()) }) @@ -605,7 +605,7 @@ func TestLockfile(t *testing.T) { func Test_ExistingWAL_NextRef(t *testing.T) { dbDir := t.TempDir() - rs := remote.NewStorage(log.NewNopLogger(), nil, startTime, dbDir, time.Second*30, nil) + rs := remote.NewStorage(log.NewNopLogger(), nil, startTime, dbDir, time.Second*30, nil, false) defer func() { require.NoError(t, rs.Close()) }() diff --git a/tsdb/wlog/watcher.go b/tsdb/wlog/watcher.go index 8ebd9249aa..3d74a551db 100644 --- a/tsdb/wlog/watcher.go +++ b/tsdb/wlog/watcher.go @@ -57,6 +57,7 @@ type WriteTo interface { AppendHistograms([]record.RefHistogramSample) bool AppendFloatHistograms([]record.RefFloatHistogramSample) bool StoreSeries([]record.RefSeries, int) + StoreMetadata([]record.RefMetadata) // Next two methods are intended for garbage-collection: first we call // UpdateSeriesSegment on all current series @@ -88,6 +89,7 @@ type Watcher struct { lastCheckpoint string sendExemplars bool sendHistograms bool + sendMetadata bool metrics *WatcherMetrics readerMetrics *LiveReaderMetrics @@ -170,7 +172,7 @@ func NewWatcherMetrics(reg prometheus.Registerer) *WatcherMetrics { } // NewWatcher creates a new WAL watcher for a given WriteTo. -func NewWatcher(metrics *WatcherMetrics, readerMetrics *LiveReaderMetrics, logger log.Logger, name string, writer WriteTo, dir string, sendExemplars, sendHistograms bool) *Watcher { +func NewWatcher(metrics *WatcherMetrics, readerMetrics *LiveReaderMetrics, logger log.Logger, name string, writer WriteTo, dir string, sendExemplars, sendHistograms, sendMetadata bool) *Watcher { if logger == nil { logger = log.NewNopLogger() } @@ -183,6 +185,7 @@ func NewWatcher(metrics *WatcherMetrics, readerMetrics *LiveReaderMetrics, logge name: name, sendExemplars: sendExemplars, sendHistograms: sendHistograms, + sendMetadata: sendMetadata, readNotify: make(chan struct{}), quit: make(chan struct{}), @@ -541,6 +544,7 @@ func (w *Watcher) readSegment(r *LiveReader, segmentNum int, tail bool) error { histogramsToSend []record.RefHistogramSample floatHistograms []record.RefFloatHistogramSample floatHistogramsToSend []record.RefFloatHistogramSample + metadata []record.RefMetadata ) for r.Next() && !isClosed(w.quit) { rec := r.Record() @@ -652,6 +656,17 @@ func (w *Watcher) readSegment(r *LiveReader, segmentNum int, tail bool) error { w.writer.AppendFloatHistograms(floatHistogramsToSend) floatHistogramsToSend = floatHistogramsToSend[:0] } + + case record.Metadata: + if !w.sendMetadata || !tail { + break + } + meta, err := dec.Metadata(rec, metadata[:0]) + if err != nil { + w.recordDecodeFailsMetric.Inc() + return err + } + w.writer.StoreMetadata(meta) case record.Tombstones: default: diff --git a/tsdb/wlog/watcher_test.go b/tsdb/wlog/watcher_test.go index ff006cb817..824010f30e 100644 --- a/tsdb/wlog/watcher_test.go +++ b/tsdb/wlog/watcher_test.go @@ -92,6 +92,8 @@ func (wtm *writeToMock) StoreSeries(series []record.RefSeries, index int) { wtm.UpdateSeriesSegment(series, index) } +func (wtm *writeToMock) StoreMetadata(_ []record.RefMetadata) { /* no-op */ } + func (wtm *writeToMock) UpdateSeriesSegment(series []record.RefSeries, index int) { wtm.seriesLock.Lock() defer wtm.seriesLock.Unlock() @@ -219,7 +221,7 @@ func TestTailSamples(t *testing.T) { require.NoError(t, err) wt := newWriteToMock(0) - watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, true, true) + watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, true, true, true) watcher.SetStartTime(now) // Set the Watcher's metrics so they're not nil pointers. @@ -304,7 +306,7 @@ func TestReadToEndNoCheckpoint(t *testing.T) { require.NoError(t, err) wt := newWriteToMock(0) - watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false) + watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false, false) go watcher.Start() expected := seriesCount @@ -393,7 +395,7 @@ func TestReadToEndWithCheckpoint(t *testing.T) { require.NoError(t, err) readTimeout = time.Second wt := newWriteToMock(0) - watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false) + watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false, false) go watcher.Start() expected := seriesCount * 2 @@ -464,7 +466,7 @@ func TestReadCheckpoint(t *testing.T) { require.NoError(t, err) wt := newWriteToMock(0) - watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false) + watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false, false) go watcher.Start() expectedSeries := seriesCount @@ -533,7 +535,7 @@ func TestReadCheckpointMultipleSegments(t *testing.T) { } wt := newWriteToMock(0) - watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false) + watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false, false) watcher.MaxSegment = -1 // Set the Watcher's metrics so they're not nil pointers. @@ -606,7 +608,7 @@ func TestCheckpointSeriesReset(t *testing.T) { readTimeout = time.Second wt := newWriteToMock(0) - watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false) + watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false, false) watcher.MaxSegment = -1 go watcher.Start() @@ -685,7 +687,7 @@ func TestRun_StartupTime(t *testing.T) { require.NoError(t, w.Close()) wt := newWriteToMock(0) - watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false) + watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false, false) watcher.MaxSegment = segments watcher.setMetrics() @@ -774,7 +776,7 @@ func TestRun_AvoidNotifyWhenBehind(t *testing.T) { }() wt := newWriteToMock(time.Millisecond) - watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false) + watcher := NewWatcher(wMetrics, nil, nil, "", wt, dir, false, false, false) watcher.MaxSegment = segments watcher.setMetrics() diff --git a/web/api/v1/api.go b/web/api/v1/api.go index b95ff25cf9..c93892f008 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -248,6 +248,7 @@ func NewAPI( registerer prometheus.Registerer, statsRenderer StatsRenderer, rwEnabled bool, + acceptRemoteWriteProtoMsgs []config.RemoteWriteProtoMsg, otlpEnabled bool, ) *API { a := &API{ @@ -290,7 +291,7 @@ func NewAPI( } if rwEnabled { - a.remoteWriteHandler = remote.NewWriteHandler(logger, registerer, ap) + a.remoteWriteHandler = remote.NewWriteHandler(logger, registerer, ap, acceptRemoteWriteProtoMsgs) } if otlpEnabled { a.otlpWriteHandler = remote.NewOTLPWriteHandler(logger, ap) diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index 74cd2239d5..9eb7d08c35 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -455,7 +455,7 @@ func TestEndpoints(t *testing.T) { remote := remote.NewStorage(promlog.New(&promlogConfig), prometheus.DefaultRegisterer, func() (int64, error) { return 0, nil - }, dbDir, 1*time.Second, nil) + }, dbDir, 1*time.Second, nil, false) err = remote.ApplyConfig(&config.Config{ RemoteReadConfigs: []*config.RemoteReadConfig{ diff --git a/web/api/v1/errors_test.go b/web/api/v1/errors_test.go index e76a1a3d35..a83bfe0173 100644 --- a/web/api/v1/errors_test.go +++ b/web/api/v1/errors_test.go @@ -135,6 +135,7 @@ func createPrometheusAPI(q storage.SampleAndChunkQueryable) *route.Router { nil, nil, false, + config.RemoteWriteProtoMsgs{config.RemoteWriteProtoMsgV1, config.RemoteWriteProtoMsgV2}, false, ) diff --git a/web/web.go b/web/web.go index a87759fb2e..9426ed935a 100644 --- a/web/web.go +++ b/web/web.go @@ -265,6 +265,8 @@ type Options struct { IsAgent bool AppName string + AcceptRemoteWriteProtoMsgs []config.RemoteWriteProtoMsg + Gatherer prometheus.Gatherer Registerer prometheus.Registerer } @@ -353,6 +355,7 @@ func New(logger log.Logger, o *Options) *Handler { o.Registerer, nil, o.EnableRemoteWriteReceiver, + o.AcceptRemoteWriteProtoMsgs, o.EnableOTLPWriteReceiver, ) From 10ecd7664e1d5119f0c44259940b3a1f6c45950e Mon Sep 17 00:00:00 2001 From: bwplotka Date: Tue, 2 Jul 2024 12:14:49 +0100 Subject: [PATCH 19/29] writev2: Add basic support for nhcb. Signed-off-by: bwplotka --- prompb/io/prometheus/write/v2/codec.go | 7 ++-- prompb/rwcommon/codec_test.go | 44 ++++++++++++++++++-------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/prompb/io/prometheus/write/v2/codec.go b/prompb/io/prometheus/write/v2/codec.go index 2939941a88..25fa0d4035 100644 --- a/prompb/io/prometheus/write/v2/codec.go +++ b/prompb/io/prometheus/write/v2/codec.go @@ -86,7 +86,6 @@ func (h Histogram) IsFloatHistogram() bool { // ToIntHistogram returns integer Prometheus histogram from the remote implementation // of integer histogram. If it's a float histogram, the method returns nil. -// TODO(bwplotka): Add support for incoming NHCB. func (h Histogram) ToIntHistogram() *histogram.Histogram { if h.IsFloatHistogram() { return nil @@ -102,13 +101,13 @@ func (h Histogram) ToIntHistogram() *histogram.Histogram { PositiveBuckets: h.GetPositiveDeltas(), NegativeSpans: spansProtoToSpans(h.GetNegativeSpans()), NegativeBuckets: h.GetNegativeDeltas(), + CustomValues: h.GetCustomValues(), } } // ToFloatHistogram returns float Prometheus histogram from the remote implementation // of float histogram. If the underlying implementation is an integer histogram, a // conversion is performed. -// TODO(bwplotka): Add support for incoming NHCB. func (h Histogram) ToFloatHistogram() *histogram.FloatHistogram { if h.IsFloatHistogram() { return &histogram.FloatHistogram{ @@ -122,6 +121,7 @@ func (h Histogram) ToFloatHistogram() *histogram.FloatHistogram { PositiveBuckets: h.GetPositiveCounts(), NegativeSpans: spansProtoToSpans(h.GetNegativeSpans()), NegativeBuckets: h.GetNegativeCounts(), + CustomValues: h.GetCustomValues(), } } // Conversion from integer histogram. @@ -136,6 +136,7 @@ func (h Histogram) ToFloatHistogram() *histogram.FloatHistogram { PositiveBuckets: deltasToCounts(h.GetPositiveDeltas()), NegativeSpans: spansProtoToSpans(h.GetNegativeSpans()), NegativeBuckets: deltasToCounts(h.GetNegativeDeltas()), + CustomValues: h.GetCustomValues(), } } @@ -171,6 +172,7 @@ func FromIntHistogram(timestamp int64, h *histogram.Histogram) Histogram { PositiveSpans: spansToSpansProto(h.PositiveSpans), PositiveDeltas: h.PositiveBuckets, ResetHint: Histogram_ResetHint(h.CounterResetHint), + CustomValues: h.CustomValues, Timestamp: timestamp, } } @@ -188,6 +190,7 @@ func FromFloatHistogram(timestamp int64, fh *histogram.FloatHistogram) Histogram PositiveSpans: spansToSpansProto(fh.PositiveSpans), PositiveCounts: fh.PositiveBuckets, ResetHint: Histogram_ResetHint(fh.CounterResetHint), + CustomValues: fh.CustomValues, Timestamp: timestamp, } } diff --git a/prompb/rwcommon/codec_test.go b/prompb/rwcommon/codec_test.go index 08e9e62d22..2ab95e0d19 100644 --- a/prompb/rwcommon/codec_test.go +++ b/prompb/rwcommon/codec_test.go @@ -144,10 +144,12 @@ func TestToHistogram_Empty(t *testing.T) { }) } +// NOTE(bwplotka): This is technically not a valid histogram, but it represents +// important cases to test when copying or converting to/from int/float histograms. func testIntHistogram() histogram.Histogram { return histogram.Histogram{ CounterResetHint: histogram.GaugeType, - Schema: 0, + Schema: 1, Count: 19, Sum: 2.7, ZeroThreshold: 1e-128, @@ -163,13 +165,16 @@ func testIntHistogram() histogram.Histogram { {Offset: 0, Length: 1}, }, NegativeBuckets: []int64{1, 2, -2, 1, -1, 0}, + CustomValues: []float64{21421, 523}, } } +// NOTE(bwplotka): This is technically not a valid histogram, but it represents +// important cases to test when copying or converting to/from int/float histograms. func testFloatHistogram() histogram.FloatHistogram { return histogram.FloatHistogram{ CounterResetHint: histogram.GaugeType, - Schema: 0, + Schema: 1, Count: 19, Sum: 2.7, ZeroThreshold: 1e-128, @@ -185,22 +190,29 @@ func testFloatHistogram() histogram.FloatHistogram { {Offset: 0, Length: 1}, }, NegativeBuckets: []float64{1, 3, 1, 2, 1, 1}, + CustomValues: []float64{21421, 523}, } } func TestFromIntToFloatOrIntHistogram(t *testing.T) { - testIntHist := testIntHistogram() - testFloatHist := testFloatHistogram() - t.Run("v1", func(t *testing.T) { - h := prompb.FromIntHistogram(123, testIntHist.Copy()) + // v1 does not support nhcb. + testIntHistWithoutNHCB := testIntHistogram() + testIntHistWithoutNHCB.CustomValues = nil + testFloatHistWithoutNHCB := testFloatHistogram() + testFloatHistWithoutNHCB.CustomValues = nil + + h := prompb.FromIntHistogram(123, &testIntHistWithoutNHCB) require.False(t, h.IsFloatHistogram()) require.Equal(t, int64(123), h.Timestamp) - require.Equal(t, testIntHist, *h.ToIntHistogram()) - require.Equal(t, testFloatHist, *h.ToFloatHistogram()) + require.Equal(t, testIntHistWithoutNHCB, *h.ToIntHistogram()) + require.Equal(t, testFloatHistWithoutNHCB, *h.ToFloatHistogram()) }) t.Run("v2", func(t *testing.T) { - h := writev2.FromIntHistogram(123, testIntHist.Copy()) + testIntHist := testIntHistogram() + testFloatHist := testFloatHistogram() + + h := writev2.FromIntHistogram(123, &testIntHist) require.False(t, h.IsFloatHistogram()) require.Equal(t, int64(123), h.Timestamp) require.Equal(t, testIntHist, *h.ToIntHistogram()) @@ -209,17 +221,21 @@ func TestFromIntToFloatOrIntHistogram(t *testing.T) { } func TestFromFloatToFloatHistogram(t *testing.T) { - testFloatHist := testFloatHistogram() - t.Run("v1", func(t *testing.T) { - h := prompb.FromFloatHistogram(123, testFloatHist.Copy()) + // v1 does not support nhcb. + testFloatHistWithoutNHCB := testFloatHistogram() + testFloatHistWithoutNHCB.CustomValues = nil + + h := prompb.FromFloatHistogram(123, &testFloatHistWithoutNHCB) require.True(t, h.IsFloatHistogram()) require.Equal(t, int64(123), h.Timestamp) require.Nil(t, h.ToIntHistogram()) - require.Equal(t, testFloatHist, *h.ToFloatHistogram()) + require.Equal(t, testFloatHistWithoutNHCB, *h.ToFloatHistogram()) }) t.Run("v2", func(t *testing.T) { - h := writev2.FromFloatHistogram(123, testFloatHist.Copy()) + testFloatHist := testFloatHistogram() + + h := writev2.FromFloatHistogram(123, &testFloatHist) require.True(t, h.IsFloatHistogram()) require.Equal(t, int64(123), h.Timestamp) require.Nil(t, h.ToIntHistogram()) From 709c5d6fc30db1ba1d8c41538bb25f6fe6237890 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 5 Jul 2024 10:11:32 +0100 Subject: [PATCH 20/29] TSDB: Lock around access to labels in head under -tags dedupelabels (#14322) * TSDB: Document what needs locking in memSeries * TSDB: Lock around access to series labels So we can modify them to reset the symbol-table. * TSDB: Make label locking conditional on build tag --------- Signed-off-by: Bryan Boreham --- tsdb/head.go | 21 ++++++++++++--------- tsdb/head_append.go | 6 +++--- tsdb/head_dedupelabels.go | 27 +++++++++++++++++++++++++++ tsdb/head_other.go | 25 +++++++++++++++++++++++++ tsdb/head_read.go | 8 ++++---- tsdb/head_wal.go | 8 ++++---- tsdb/ooo_head_read.go | 2 +- 7 files changed, 76 insertions(+), 21 deletions(-) create mode 100644 tsdb/head_dedupelabels.go create mode 100644 tsdb/head_other.go diff --git a/tsdb/head.go b/tsdb/head.go index 5972a9c5d6..30ad8139ac 100644 --- a/tsdb/head.go +++ b/tsdb/head.go @@ -1759,12 +1759,12 @@ type seriesHashmap struct { func (m *seriesHashmap) get(hash uint64, lset labels.Labels) *memSeries { if s, found := m.unique[hash]; found { - if labels.Equal(s.lset, lset) { + if labels.Equal(s.labels(), lset) { return s } } for _, s := range m.conflicts[hash] { - if labels.Equal(s.lset, lset) { + if labels.Equal(s.labels(), lset) { return s } } @@ -1772,7 +1772,7 @@ func (m *seriesHashmap) get(hash uint64, lset labels.Labels) *memSeries { } func (m *seriesHashmap) set(hash uint64, s *memSeries) { - if existing, found := m.unique[hash]; !found || labels.Equal(existing.lset, s.lset) { + if existing, found := m.unique[hash]; !found || labels.Equal(existing.labels(), s.labels()) { m.unique[hash] = s return } @@ -1781,7 +1781,7 @@ func (m *seriesHashmap) set(hash uint64, s *memSeries) { } l := m.conflicts[hash] for i, prev := range l { - if labels.Equal(prev.lset, s.lset) { + if labels.Equal(prev.labels(), s.labels()) { l[i] = s return } @@ -1931,7 +1931,7 @@ func (s *stripeSeries) gc(mint int64, minOOOMmapRef chunks.ChunkDiskMapperRef) ( series.lset.Range(func(l labels.Label) { affected[l] = struct{}{} }) s.hashes[hashShard].del(hash, series.ref) delete(s.series[refShard], series.ref) - deletedForCallback[series.ref] = series.lset + deletedForCallback[series.ref] = series.lset // OK to access lset; series is locked at the top of this function. } s.iterForDeletion(check) @@ -2023,7 +2023,7 @@ func (s *stripeSeries) getOrSet(hash uint64, lset labels.Labels, createSeries fu } // Setting the series in the s.hashes marks the creation of series // as any further calls to this methods would return that series. - s.seriesLifecycleCallback.PostCreation(series.lset) + s.seriesLifecycleCallback.PostCreation(series.labels()) i = uint64(series.ref) & uint64(s.size-1) @@ -2064,16 +2064,19 @@ func (s sample) Type() chunkenc.ValueType { // memSeries is the in-memory representation of a series. None of its methods // are goroutine safe and it is the caller's responsibility to lock it. type memSeries struct { - sync.Mutex - + // Members up to the Mutex are not changed after construction, so can be accessed without a lock. ref chunks.HeadSeriesRef - lset labels.Labels meta *metadata.Metadata // Series labels hash to use for sharding purposes. The value is always 0 when sharding has not // been explicitly enabled in TSDB. shardHash uint64 + // Everything after here should only be accessed with the lock held. + sync.Mutex + + lset labels.Labels // Locking required with -tags dedupelabels, not otherwise. + // Immutable chunks on disk that have not yet gone into a block, in order of ascending time stamps. // When compaction runs, chunks get moved into a block and all pointers are shifted like so: // diff --git a/tsdb/head_append.go b/tsdb/head_append.go index 62c3727e28..f45ab606ba 100644 --- a/tsdb/head_append.go +++ b/tsdb/head_append.go @@ -554,7 +554,7 @@ func (a *headAppender) AppendExemplar(ref storage.SeriesRef, lset labels.Labels, // Ensure no empty labels have gotten through. e.Labels = e.Labels.WithoutEmpty() - err := a.head.exemplars.ValidateExemplar(s.lset, e) + err := a.head.exemplars.ValidateExemplar(s.labels(), e) if err != nil { if errors.Is(err, storage.ErrDuplicateExemplar) || errors.Is(err, storage.ErrExemplarsDisabled) { // Duplicate, don't return an error but don't accept the exemplar. @@ -708,7 +708,7 @@ func (a *headAppender) GetRef(lset labels.Labels, hash uint64) (storage.SeriesRe return 0, labels.EmptyLabels() } // returned labels must be suitable to pass to Append() - return storage.SeriesRef(s.ref), s.lset + return storage.SeriesRef(s.ref), s.labels() } // log writes all headAppender's data to the WAL. @@ -816,7 +816,7 @@ func (a *headAppender) Commit() (err error) { continue } // We don't instrument exemplar appends here, all is instrumented by storage. - if err := a.head.exemplars.AddExemplar(s.lset, e.exemplar); err != nil { + if err := a.head.exemplars.AddExemplar(s.labels(), e.exemplar); err != nil { if errors.Is(err, storage.ErrOutOfOrderExemplar) { continue } diff --git a/tsdb/head_dedupelabels.go b/tsdb/head_dedupelabels.go new file mode 100644 index 0000000000..203f92e6a8 --- /dev/null +++ b/tsdb/head_dedupelabels.go @@ -0,0 +1,27 @@ +// Copyright 2024 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build dedupelabels + +package tsdb + +import ( + "github.com/prometheus/prometheus/model/labels" +) + +// Helper method to access labels under lock. +func (s *memSeries) labels() labels.Labels { + s.Lock() + defer s.Unlock() + return s.lset +} diff --git a/tsdb/head_other.go b/tsdb/head_other.go new file mode 100644 index 0000000000..9306913d8f --- /dev/null +++ b/tsdb/head_other.go @@ -0,0 +1,25 @@ +// Copyright 2024 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !dedupelabels + +package tsdb + +import ( + "github.com/prometheus/prometheus/model/labels" +) + +// Helper method to access labels; trivial when not using dedupelabels. +func (s *memSeries) labels() labels.Labels { + return s.lset +} diff --git a/tsdb/head_read.go b/tsdb/head_read.go index b47e24f9e4..9ba8785ad2 100644 --- a/tsdb/head_read.go +++ b/tsdb/head_read.go @@ -142,7 +142,7 @@ func (h *headIndexReader) SortedPostings(p index.Postings) index.Postings { } slices.SortFunc(series, func(a, b *memSeries) int { - return labels.Compare(a.lset, b.lset) + return labels.Compare(a.labels(), b.labels()) }) // Convert back to list. @@ -189,7 +189,7 @@ func (h *headIndexReader) Series(ref storage.SeriesRef, builder *labels.ScratchB h.head.metrics.seriesNotFound.Inc() return storage.ErrNotFound } - builder.Assign(s.lset) + builder.Assign(s.labels()) if chks == nil { return nil @@ -259,7 +259,7 @@ func (h *headIndexReader) LabelValueFor(_ context.Context, id storage.SeriesRef, return "", storage.ErrNotFound } - value := memSeries.lset.Get(label) + value := memSeries.labels().Get(label) if value == "" { return "", storage.ErrNotFound } @@ -283,7 +283,7 @@ func (h *headIndexReader) LabelNamesFor(ctx context.Context, series index.Postin // when series was garbage collected after the caller got the series IDs. continue } - memSeries.lset.Range(func(lbl labels.Label) { + memSeries.labels().Range(func(lbl labels.Label) { namesMap[lbl.Name] = struct{}{} }) } diff --git a/tsdb/head_wal.go b/tsdb/head_wal.go index 41f7dd46b2..787cb7c267 100644 --- a/tsdb/head_wal.go +++ b/tsdb/head_wal.go @@ -126,7 +126,7 @@ func (h *Head) loadWAL(r *wlog.Reader, syms *labels.SymbolTable, multiRef map[ch } // At the moment the only possible error here is out of order exemplars, which we shouldn't see when // replaying the WAL, so lets just log the error if it's not that type. - err = h.exemplars.AddExemplar(ms.lset, exemplar.Exemplar{Ts: e.T, Value: e.V, Labels: e.Labels}) + err = h.exemplars.AddExemplar(ms.labels(), exemplar.Exemplar{Ts: e.T, Value: e.V, Labels: e.Labels}) if err != nil && errors.Is(err, storage.ErrOutOfOrderExemplar) { level.Warn(h.logger).Log("msg", "Unexpected error when replaying WAL on exemplar record", "err", err) } @@ -448,7 +448,7 @@ func (h *Head) resetSeriesWithMMappedChunks(mSeries *memSeries, mmc, oooMmc []*m ) { level.Debug(h.logger).Log( "msg", "M-mapped chunks overlap on a duplicate series record", - "series", mSeries.lset.String(), + "series", mSeries.labels().String(), "oldref", mSeries.ref, "oldmint", mSeries.mmappedChunks[0].minTime, "oldmaxt", mSeries.mmappedChunks[len(mSeries.mmappedChunks)-1].maxTime, @@ -932,7 +932,7 @@ func (s *memSeries) encodeToSnapshotRecord(b []byte) []byte { buf.PutByte(chunkSnapshotRecordTypeSeries) buf.PutBE64(uint64(s.ref)) - record.EncodeLabels(&buf, s.lset) + record.EncodeLabels(&buf, s.labels()) buf.PutBE64int64(0) // Backwards-compatibility; was chunkRange but now unused. s.Lock() @@ -1485,7 +1485,7 @@ Outer: continue } - if err := h.exemplars.AddExemplar(ms.lset, exemplar.Exemplar{ + if err := h.exemplars.AddExemplar(ms.labels(), exemplar.Exemplar{ Labels: e.Labels, Value: e.V, Ts: e.T, diff --git a/tsdb/ooo_head_read.go b/tsdb/ooo_head_read.go index 47972c3cce..4e8329c99b 100644 --- a/tsdb/ooo_head_read.go +++ b/tsdb/ooo_head_read.go @@ -78,7 +78,7 @@ func (oh *OOOHeadIndexReader) series(ref storage.SeriesRef, builder *labels.Scra oh.head.metrics.seriesNotFound.Inc() return storage.ErrNotFound } - builder.Assign(s.lset) + builder.Assign(s.labels()) if chks == nil { return nil From 7ca31c66beb92230df4c68f42992007614af5b0e Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 2 Apr 2024 14:56:19 +0100 Subject: [PATCH 21/29] Scraping: add metric for symbol table size Signed-off-by: Bryan Boreham --- scrape/metrics.go | 10 ++++++++++ scrape/scrape.go | 2 ++ 2 files changed, 12 insertions(+) diff --git a/scrape/metrics.go b/scrape/metrics.go index b67d0686b6..e7395c6191 100644 --- a/scrape/metrics.go +++ b/scrape/metrics.go @@ -34,6 +34,7 @@ type scrapeMetrics struct { targetScrapePoolExceededTargetLimit prometheus.Counter targetScrapePoolTargetLimit *prometheus.GaugeVec targetScrapePoolTargetsAdded *prometheus.GaugeVec + targetScrapePoolSymbolTableItems *prometheus.GaugeVec targetSyncIntervalLength *prometheus.SummaryVec targetSyncFailed *prometheus.CounterVec @@ -129,6 +130,13 @@ func newScrapeMetrics(reg prometheus.Registerer) (*scrapeMetrics, error) { }, []string{"scrape_job"}, ) + sm.targetScrapePoolSymbolTableItems = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "prometheus_target_scrape_pool_symboltable_items", + Help: "Current number of symbols in table for this scrape pool.", + }, + []string{"scrape_job"}, + ) sm.targetScrapePoolSyncsCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "prometheus_target_scrape_pool_sync_total", @@ -234,6 +242,7 @@ func newScrapeMetrics(reg prometheus.Registerer) (*scrapeMetrics, error) { sm.targetScrapePoolExceededTargetLimit, sm.targetScrapePoolTargetLimit, sm.targetScrapePoolTargetsAdded, + sm.targetScrapePoolSymbolTableItems, sm.targetSyncFailed, // Used by targetScraper. sm.targetScrapeExceededBodySizeLimit, @@ -274,6 +283,7 @@ func (sm *scrapeMetrics) Unregister() { sm.reg.Unregister(sm.targetScrapePoolExceededTargetLimit) sm.reg.Unregister(sm.targetScrapePoolTargetLimit) sm.reg.Unregister(sm.targetScrapePoolTargetsAdded) + sm.reg.Unregister(sm.targetScrapePoolSymbolTableItems) sm.reg.Unregister(sm.targetSyncFailed) sm.reg.Unregister(sm.targetScrapeExceededBodySizeLimit) sm.reg.Unregister(sm.targetScrapeCacheFlushForced) diff --git a/scrape/scrape.go b/scrape/scrape.go index 17e9913e8c..c16f14cec8 100644 --- a/scrape/scrape.go +++ b/scrape/scrape.go @@ -246,6 +246,7 @@ func (sp *scrapePool) stop() { sp.metrics.targetScrapePoolSyncsCounter.DeleteLabelValues(sp.config.JobName) sp.metrics.targetScrapePoolTargetLimit.DeleteLabelValues(sp.config.JobName) sp.metrics.targetScrapePoolTargetsAdded.DeleteLabelValues(sp.config.JobName) + sp.metrics.targetScrapePoolSymbolTableItems.DeleteLabelValues(sp.config.JobName) sp.metrics.targetSyncIntervalLength.DeleteLabelValues(sp.config.JobName) sp.metrics.targetSyncFailed.DeleteLabelValues(sp.config.JobName) } @@ -408,6 +409,7 @@ func (sp *scrapePool) Sync(tgs []*targetgroup.Group) { } } } + sp.metrics.targetScrapePoolSymbolTableItems.WithLabelValues(sp.config.JobName).Set(float64(sp.symbolTable.Len())) sp.targetMtx.Unlock() sp.sync(all) From b42b5fbd74627d5c5ea9fb38857fc7a4b55ebfe1 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 2 Apr 2024 18:42:40 +0100 Subject: [PATCH 22/29] Scraping: check symbol-table on sync Previously they were only checked on a change of config. Signed-off-by: Bryan Boreham --- scrape/scrape.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scrape/scrape.go b/scrape/scrape.go index c16f14cec8..2655ffd167 100644 --- a/scrape/scrape.go +++ b/scrape/scrape.go @@ -357,7 +357,11 @@ func (sp *scrapePool) reload(cfg *config.ScrapeConfig) error { sp.metrics.targetReloadIntervalLength.WithLabelValues(interval.String()).Observe( time.Since(start).Seconds(), ) + return nil +} +// Must be called with sp.mtx held. +func (sp *scrapePool) checkSymbolTable() { // Here we take steps to clear out the symbol table if it has grown a lot. // After waiting some time for things to settle, we take the size of the symbol-table. // If, after some more time, the table has grown to twice that size, we start a new one. @@ -371,8 +375,6 @@ func (sp *scrapePool) reload(cfg *config.ScrapeConfig) error { } sp.lastSymbolTableCheck = time.Now() } - - return nil } // Sync converts target groups into actual scrape targets and synchronizes @@ -412,6 +414,7 @@ func (sp *scrapePool) Sync(tgs []*targetgroup.Group) { sp.metrics.targetScrapePoolSymbolTableItems.WithLabelValues(sp.config.JobName).Set(float64(sp.symbolTable.Len())) sp.targetMtx.Unlock() sp.sync(all) + sp.checkSymbolTable() sp.metrics.targetSyncIntervalLength.WithLabelValues(sp.config.JobName).Observe( time.Since(start).Seconds(), From 74b1f3daa604c3b5801b67bbea8de780f93a6470 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 9 Apr 2024 18:43:49 +0100 Subject: [PATCH 23/29] Refactor: scraping: extract method restartLoops Signed-off-by: Bryan Boreham --- scrape/scrape.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/scrape/scrape.go b/scrape/scrape.go index 2655ffd167..57bb164b7d 100644 --- a/scrape/scrape.go +++ b/scrape/scrape.go @@ -274,6 +274,15 @@ func (sp *scrapePool) reload(cfg *config.ScrapeConfig) error { sp.metrics.targetScrapePoolTargetLimit.WithLabelValues(sp.config.JobName).Set(float64(sp.config.TargetLimit)) + sp.restartLoops(reuseCache) + oldClient.CloseIdleConnections() + sp.metrics.targetReloadIntervalLength.WithLabelValues(time.Duration(sp.config.ScrapeInterval).String()).Observe( + time.Since(start).Seconds(), + ) + return nil +} + +func (sp *scrapePool) restartLoops(reuseCache bool) { var ( wg sync.WaitGroup interval = time.Duration(sp.config.ScrapeInterval) @@ -314,7 +323,7 @@ func (sp *scrapePool) reload(cfg *config.ScrapeConfig) error { client: sp.client, timeout: timeout, bodySizeLimit: bodySizeLimit, - acceptHeader: acceptHeader(cfg.ScrapeProtocols), + acceptHeader: acceptHeader(sp.config.ScrapeProtocols), acceptEncodingHeader: acceptEncodingHeader(enableCompression), } newLoop = sp.newLoop(scrapeLoopOptions{ @@ -353,11 +362,6 @@ func (sp *scrapePool) reload(cfg *config.ScrapeConfig) error { sp.targetMtx.Unlock() wg.Wait() - oldClient.CloseIdleConnections() - sp.metrics.targetReloadIntervalLength.WithLabelValues(interval.String()).Observe( - time.Since(start).Seconds(), - ) - return nil } // Must be called with sp.mtx held. From e6356e64bd6fe68e7568699e2c7f345745a12b3b Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 2 Apr 2024 17:07:00 +0100 Subject: [PATCH 24/29] Scraping: drop series cache when resizing symbol table Clear caches by restarting scraping loops: each loop assumes it has exclusive ownership of its cache, so we can't come in from another goroutine and change it. Signed-off-by: Bryan Boreham --- scrape/scrape.go | 1 + 1 file changed, 1 insertion(+) diff --git a/scrape/scrape.go b/scrape/scrape.go index 57bb164b7d..68411a62e0 100644 --- a/scrape/scrape.go +++ b/scrape/scrape.go @@ -376,6 +376,7 @@ func (sp *scrapePool) checkSymbolTable() { } else if sp.symbolTable.Len() > 2*sp.initialSymbolTableLen { sp.symbolTable = labels.NewSymbolTable() sp.initialSymbolTableLen = 0 + sp.restartLoops(false) // To drop all caches. } sp.lastSymbolTableCheck = time.Now() } From 5281a6bc1b8e36b0c778f6b2a41119a18c185aa5 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 2 Apr 2024 15:08:58 +0100 Subject: [PATCH 25/29] TSDB: rebuild labels symbol-table on each compaction Log begin/end for timing, plus some stats. Signed-off-by: Bryan Boreham --- tsdb/db.go | 3 +++ tsdb/head_dedupelabels.go | 40 +++++++++++++++++++++++++++++++++++++++ tsdb/head_other.go | 7 +++++++ 3 files changed, 50 insertions(+) diff --git a/tsdb/db.go b/tsdb/db.go index b2175d4758..090d6fcf0c 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -1407,6 +1407,9 @@ func (db *DB) compactHead(head *RangeHead) error { if err = db.head.truncateMemory(head.BlockMaxTime()); err != nil { return fmt.Errorf("head memory truncate: %w", err) } + + db.head.RebuildSymbolTable(db.logger) + return nil } diff --git a/tsdb/head_dedupelabels.go b/tsdb/head_dedupelabels.go index 203f92e6a8..aaab7c25be 100644 --- a/tsdb/head_dedupelabels.go +++ b/tsdb/head_dedupelabels.go @@ -16,6 +16,9 @@ package tsdb import ( + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus/prometheus/model/labels" ) @@ -25,3 +28,40 @@ func (s *memSeries) labels() labels.Labels { defer s.Unlock() return s.lset } + +// RebuildSymbolTable goes through all the series in h, build a SymbolTable with all names and values, +// replace each series' Labels with one using that SymbolTable. +func (h *Head) RebuildSymbolTable(logger log.Logger) *labels.SymbolTable { + level.Info(logger).Log("msg", "RebuildSymbolTable starting") + st := labels.NewSymbolTable() + builder := labels.NewScratchBuilderWithSymbolTable(st, 0) + rebuildLabels := func(lbls labels.Labels) labels.Labels { + builder.Reset() + lbls.Range(func(l labels.Label) { + builder.Add(l.Name, l.Value) + }) + return builder.Labels() + } + + for i := 0; i < h.series.size; i++ { + h.series.locks[i].Lock() + + for _, s := range h.series.hashes[i].unique { + s.Lock() + s.lset = rebuildLabels(s.lset) + s.Unlock() + } + + for _, all := range h.series.hashes[i].conflicts { + for _, s := range all { + s.Lock() + s.lset = rebuildLabels(s.lset) + s.Unlock() + } + } + + h.series.locks[i].Unlock() + } + level.Info(logger).Log("msg", "RebuildSymbolTable finished", "size", st.Len()) + return st +} diff --git a/tsdb/head_other.go b/tsdb/head_other.go index 9306913d8f..eb1b93a3e5 100644 --- a/tsdb/head_other.go +++ b/tsdb/head_other.go @@ -16,6 +16,8 @@ package tsdb import ( + "github.com/go-kit/log" + "github.com/prometheus/prometheus/model/labels" ) @@ -23,3 +25,8 @@ import ( func (s *memSeries) labels() labels.Labels { return s.lset } + +// No-op when not using dedupelabels. +func (h *Head) RebuildSymbolTable(logger log.Logger) *labels.SymbolTable { + return nil +} From 4d7532f60b1731f0c3a1aa62f50f4b964288e6c1 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Sat, 11 May 2024 11:00:42 +0100 Subject: [PATCH 26/29] tsdb: reset symbol table for exemplars periodically To avoid keeping the memory alive forever. Signed-off-by: Bryan Boreham --- tsdb/head_dedupelabels.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tsdb/head_dedupelabels.go b/tsdb/head_dedupelabels.go index aaab7c25be..a16d907261 100644 --- a/tsdb/head_dedupelabels.go +++ b/tsdb/head_dedupelabels.go @@ -62,6 +62,34 @@ func (h *Head) RebuildSymbolTable(logger log.Logger) *labels.SymbolTable { h.series.locks[i].Unlock() } + type withReset interface{ ResetSymbolTable(*labels.SymbolTable) } + if e, ok := h.exemplars.(withReset); ok { + e.ResetSymbolTable(st) + } level.Info(logger).Log("msg", "RebuildSymbolTable finished", "size", st.Len()) return st } + +func (ce *CircularExemplarStorage) ResetSymbolTable(st *labels.SymbolTable) { + builder := labels.NewScratchBuilderWithSymbolTable(st, 0) + rebuildLabels := func(lbls labels.Labels) labels.Labels { + builder.Reset() + lbls.Range(func(l labels.Label) { + builder.Add(l.Name, l.Value) + }) + return builder.Labels() + } + + ce.lock.RLock() + defer ce.lock.RUnlock() + + for _, v := range ce.index { + v.seriesLabels = rebuildLabels(v.seriesLabels) + } + for i := range ce.exemplars { + if ce.exemplars[i].ref == nil { + continue + } + ce.exemplars[i].exemplar.Labels = rebuildLabels(ce.exemplars[i].exemplar.Labels) + } +} From 480fefd0899e7af0835d0a49fefe91396bc37a29 Mon Sep 17 00:00:00 2001 From: zenador Date: Sat, 6 Jul 2024 17:05:00 +0800 Subject: [PATCH 27/29] Split warnings and info annotations in API response (#14327) * split warnings and info annotations in API response Signed-off-by: Jeanette Tan * update according to code review Signed-off-by: Jeanette Tan * minimal UI change: show infos in different colour Signed-off-by: Jeanette Tan * Update web/ui/react-app/src/pages/graph/Panel.tsx Co-authored-by: Julius Volz Signed-off-by: zenador --------- Signed-off-by: Jeanette Tan Signed-off-by: zenador Co-authored-by: Julius Volz --- storage/fanout_test.go | 6 ++- util/annotations/annotations.go | 50 +++++++++++++------ web/api/v1/api.go | 5 +- .../src/client/prometheus.ts | 1 + web/ui/react-app/src/pages/graph/Panel.tsx | 8 +++ 5 files changed, 51 insertions(+), 19 deletions(-) diff --git a/storage/fanout_test.go b/storage/fanout_test.go index 913e2fe24e..712f0400f7 100644 --- a/storage/fanout_test.go +++ b/storage/fanout_test.go @@ -181,7 +181,8 @@ func TestFanoutErrors(t *testing.T) { require.NotEmpty(t, ss.Warnings(), "warnings expected") w := ss.Warnings() require.Error(t, w.AsErrors()[0]) - require.Equal(t, tc.warning.Error(), w.AsStrings("", 0)[0]) + warn, _ := w.AsStrings("", 0, 0) + require.Equal(t, tc.warning.Error(), warn[0]) } }) t.Run("chunks", func(t *testing.T) { @@ -207,7 +208,8 @@ func TestFanoutErrors(t *testing.T) { require.NotEmpty(t, ss.Warnings(), "warnings expected") w := ss.Warnings() require.Error(t, w.AsErrors()[0]) - require.Equal(t, tc.warning.Error(), w.AsStrings("", 0)[0]) + warn, _ := w.AsStrings("", 0, 0) + require.Equal(t, tc.warning.Error(), warn[0]) } }) } diff --git a/util/annotations/annotations.go b/util/annotations/annotations.go index 40a20e4b92..bc5d76db43 100644 --- a/util/annotations/annotations.go +++ b/util/annotations/annotations.go @@ -71,31 +71,49 @@ func (a Annotations) AsErrors() []error { return arr } -// AsStrings is a convenience function to return the annotations map as a slice -// of strings. The query string is used to get the line number and character offset -// positioning info of the elements which trigger an annotation. We limit the number -// of annotations returned here with maxAnnos (0 for no limit). -func (a Annotations) AsStrings(query string, maxAnnos int) []string { - arr := make([]string, 0, len(a)) +// AsStrings is a convenience function to return the annotations map as 2 slices +// of strings, separated into warnings and infos. The query string is used to get the +// line number and character offset positioning info of the elements which trigger an +// annotation. We limit the number of warnings and infos returned here with maxWarnings +// and maxInfos respectively (0 for no limit). +func (a Annotations) AsStrings(query string, maxWarnings, maxInfos int) (warnings, infos []string) { + warnings = make([]string, 0, maxWarnings+1) + infos = make([]string, 0, maxInfos+1) + warnSkipped := 0 + infoSkipped := 0 for _, err := range a { - if maxAnnos > 0 && len(arr) >= maxAnnos { - break - } var anErr annoErr if errors.As(err, &anErr) { anErr.Query = query err = anErr } - arr = append(arr, err.Error()) + switch { + case errors.Is(err, PromQLInfo): + if maxInfos == 0 || len(infos) < maxInfos { + infos = append(infos, err.Error()) + } else { + infoSkipped++ + } + default: + if maxWarnings == 0 || len(warnings) < maxWarnings { + warnings = append(warnings, err.Error()) + } else { + warnSkipped++ + } + } } - if maxAnnos > 0 && len(a) > maxAnnos { - arr = append(arr, fmt.Sprintf("%d more annotations omitted", len(a)-maxAnnos)) + if warnSkipped > 0 { + warnings = append(warnings, fmt.Sprintf("%d more warning annotations omitted", warnSkipped)) } - return arr + if infoSkipped > 0 { + infos = append(infos, fmt.Sprintf("%d more info annotations omitted", infoSkipped)) + } + return } -func (a Annotations) CountWarningsAndInfo() (int, int) { - var countWarnings, countInfo int +// CountWarningsAndInfo counts and returns the number of warnings and infos in the +// annotations wrapper. +func (a Annotations) CountWarningsAndInfo() (countWarnings, countInfo int) { for _, err := range a { if errors.Is(err, PromQLWarning) { countWarnings++ @@ -104,7 +122,7 @@ func (a Annotations) CountWarningsAndInfo() (int, int) { countInfo++ } } - return countWarnings, countInfo + return } //nolint:revive // error-naming. diff --git a/web/api/v1/api.go b/web/api/v1/api.go index c93892f008..7e98dac454 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -159,6 +159,7 @@ type Response struct { ErrorType errorType `json:"errorType,omitempty"` Error string `json:"error,omitempty"` Warnings []string `json:"warnings,omitempty"` + Infos []string `json:"infos,omitempty"` } type apiFuncResult struct { @@ -1747,11 +1748,13 @@ func (api *API) cleanTombstones(*http.Request) apiFuncResult { // can be empty if the position information isn't needed. func (api *API) respond(w http.ResponseWriter, req *http.Request, data interface{}, warnings annotations.Annotations, query string) { statusMessage := statusSuccess + warn, info := warnings.AsStrings(query, 10, 10) resp := &Response{ Status: statusMessage, Data: data, - Warnings: warnings.AsStrings(query, 10), + Warnings: warn, + Infos: info, } codec, err := api.negotiateCodec(req, resp) diff --git a/web/ui/module/codemirror-promql/src/client/prometheus.ts b/web/ui/module/codemirror-promql/src/client/prometheus.ts index 873cbb0d22..71d12d3ac4 100644 --- a/web/ui/module/codemirror-promql/src/client/prometheus.ts +++ b/web/ui/module/codemirror-promql/src/client/prometheus.ts @@ -66,6 +66,7 @@ interface APIResponse { data?: T; error?: string; warnings?: string[]; + infos?: string[]; } // These are status codes where the Prometheus API still returns a valid JSON body, diff --git a/web/ui/react-app/src/pages/graph/Panel.tsx b/web/ui/react-app/src/pages/graph/Panel.tsx index f60812a503..1b2956fd78 100644 --- a/web/ui/react-app/src/pages/graph/Panel.tsx +++ b/web/ui/react-app/src/pages/graph/Panel.tsx @@ -37,6 +37,7 @@ interface PanelState { lastQueryParams: QueryParams | null; loading: boolean; warnings: string[] | null; + infos: string[] | null; error: string | null; stats: QueryStats | null; exprInputValue: string; @@ -87,6 +88,7 @@ class Panel extends Component { lastQueryParams: null, loading: false, warnings: null, + infos: null, error: null, stats: null, exprInputValue: props.options.expr, @@ -204,6 +206,7 @@ class Panel extends Component { data: query.data, exemplars: exemplars?.data, warnings: query.warnings, + infos: query.infos, lastQueryParams: { startTime, endTime, @@ -307,6 +310,11 @@ class Panel extends Component { {warning && {warning}} ))} + {this.state.infos?.map((info, index) => ( + + {info && {info}} + + ))}