From bfec57bd2e11c12077bc82e9ec144843e0a890af Mon Sep 17 00:00:00 2001 From: Marco Pracucci Date: Thu, 25 Jan 2024 10:40:57 +0100 Subject: [PATCH 001/127] Further optimise FastRegexMatcher Signed-off-by: Marco Pracucci --- model/labels/matcher.go | 27 + model/labels/matcher_test.go | 94 ++- model/labels/regexp.go | 894 ++++++++++++++++++++++++++-- model/labels/regexp_test.go | 1055 +++++++++++++++++++++++++++++++--- 4 files changed, 1960 insertions(+), 110 deletions(-) diff --git a/model/labels/matcher.go b/model/labels/matcher.go index f299c40f64..1282f80d63 100644 --- a/model/labels/matcher.go +++ b/model/labels/matcher.go @@ -118,3 +118,30 @@ func (m *Matcher) GetRegexString() string { } return m.re.GetRegexString() } + +// SetMatches returns a set of equality matchers for the current regex matchers if possible. +// For examples the regexp `a(b|f)` will returns "ab" and "af". +// Returns nil if we can't replace the regexp by only equality matchers. +func (m *Matcher) SetMatches() []string { + if m.re == nil { + return nil + } + return m.re.SetMatches() +} + +// Prefix returns the required prefix of the value to match, if possible. +// It will be empty if it's an equality matcher or if the prefix can't be determined. +func (m *Matcher) Prefix() string { + if m.re == nil { + return "" + } + return m.re.prefix +} + +// IsRegexOptimized returns whether regex is optimized. +func (m *Matcher) IsRegexOptimized() bool { + if m.re == nil { + return false + } + return m.re.IsOptimized() +} diff --git a/model/labels/matcher_test.go b/model/labels/matcher_test.go index d26e9329f2..c23deafe61 100644 --- a/model/labels/matcher_test.go +++ b/model/labels/matcher_test.go @@ -14,13 +14,14 @@ package labels import ( + "fmt" "testing" "github.com/stretchr/testify/require" ) func mustNewMatcher(t *testing.T, mType MatchType, value string) *Matcher { - m, err := NewMatcher(mType, "", value) + m, err := NewMatcher(mType, "test_label_name", value) require.NoError(t, err) return m } @@ -81,6 +82,21 @@ func TestMatcher(t *testing.T) { value: "foo-bar", match: false, }, + { + matcher: mustNewMatcher(t, MatchRegexp, "$*bar"), + value: "foo-bar", + match: false, + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "bar^+"), + value: "foo-bar", + match: false, + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "$+bar"), + value: "foo-bar", + match: false, + }, } for _, test := range tests { @@ -118,6 +134,82 @@ func TestInverse(t *testing.T) { } } +func TestPrefix(t *testing.T) { + for i, tc := range []struct { + matcher *Matcher + prefix string + }{ + { + matcher: mustNewMatcher(t, MatchEqual, "abc"), + prefix: "", + }, + { + matcher: mustNewMatcher(t, MatchNotEqual, "abc"), + prefix: "", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "abc.+"), + prefix: "abc", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "abcd|abc.+"), + prefix: "abc", + }, + { + matcher: mustNewMatcher(t, MatchNotRegexp, "abcd|abc.+"), + prefix: "abc", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "abc(def|ghj)|ab|a."), + prefix: "a", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "foo.+bar|foo.*baz"), + prefix: "foo", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "abc|.*"), + prefix: "", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "abc|def"), + prefix: "", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, ".+def"), + prefix: "", + }, + } { + t.Run(fmt.Sprintf("%d: %s", i, tc.matcher), func(t *testing.T) { + require.Equal(t, tc.prefix, tc.matcher.Prefix()) + }) + } +} + +func TestIsRegexOptimized(t *testing.T) { + for i, tc := range []struct { + matcher *Matcher + isRegexOptimized bool + }{ + { + matcher: mustNewMatcher(t, MatchEqual, "abc"), + isRegexOptimized: false, + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "."), + isRegexOptimized: false, + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "abc.+"), + isRegexOptimized: true, + }, + } { + t.Run(fmt.Sprintf("%d: %s", i, tc.matcher), func(t *testing.T) { + require.Equal(t, tc.isRegexOptimized, tc.matcher.IsRegexOptimized()) + }) + } +} + func BenchmarkMatchType_String(b *testing.B) { for i := 0; i <= b.N; i++ { _ = MatchType(i % int(MatchNotRegexp+1)).String() diff --git a/model/labels/regexp.go b/model/labels/regexp.go index 14319c7f7a..5e470afa3b 100644 --- a/model/labels/regexp.go +++ b/model/labels/regexp.go @@ -18,69 +18,344 @@ import ( "github.com/grafana/regexp" "github.com/grafana/regexp/syntax" + "golang.org/x/exp/slices" +) + +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 + // to match values instead of iterating over a list. This value has + // been computed running BenchmarkOptimizeEqualStringMatchers. + minEqualMultiStringMatcherMapThreshold = 16 ) type FastRegexMatcher struct { + // Under some conditions, re is nil because the expression is never parsed. + // We store the original string to be able to return it in GetRegexString(). + reString string re *regexp.Regexp - prefix string - suffix string - contains string - // shortcut for literals - literal bool - value string + setMatches []string + stringMatcher StringMatcher + prefix string + suffix string + contains string + + // matchString is the "compiled" function to run by MatchString(). + matchString func(string) bool } func NewFastRegexMatcher(v string) (*FastRegexMatcher, error) { - if isLiteral(v) { - return &FastRegexMatcher{literal: true, value: v}, nil - } - re, err := regexp.Compile("^(?:" + v + ")$") - if err != nil { - return nil, err - } - - parsed, err := syntax.Parse(v, syntax.Perl) - if err != nil { - return nil, err - } - m := &FastRegexMatcher{ - re: re, + reString: v, } - if parsed.Op == syntax.OpConcat { - m.prefix, m.suffix, m.contains = optimizeConcatRegex(parsed) + m.stringMatcher, m.setMatches = optimizeAlternatingLiterals(v) + if m.stringMatcher != nil { + // If we already have a string matcher, we don't need to parse the regex + // or compile the matchString function. This also avoids the behavior in + // compileMatchStringFunction where it prefers to use setMatches when + // available, even if the string matcher is faster. + m.matchString = m.stringMatcher.Matches + } else { + parsed, err := syntax.Parse(v, syntax.Perl) + if err != nil { + return nil, err + } + // Simplify the syntax tree to run faster. + parsed = parsed.Simplify() + m.re, err = regexp.Compile("^(?:" + parsed.String() + ")$") + if err != nil { + return nil, err + } + if parsed.Op == syntax.OpConcat { + m.prefix, m.suffix, m.contains = optimizeConcatRegex(parsed) + } + if matches, caseSensitive := findSetMatches(parsed); caseSensitive { + m.setMatches = matches + } + m.stringMatcher = stringMatcherFromRegexp(parsed) + m.matchString = m.compileMatchStringFunction() } return m, nil } +// compileMatchStringFunction returns the function to run by MatchString(). +func (m *FastRegexMatcher) compileMatchStringFunction() func(string) bool { + // If the only optimization available is the string matcher, then we can just run it. + if len(m.setMatches) == 0 && m.prefix == "" && m.suffix == "" && m.contains == "" && m.stringMatcher != nil { + return m.stringMatcher.Matches + } + + return func(s string) bool { + if len(m.setMatches) != 0 { + for _, match := range m.setMatches { + if match == s { + return true + } + } + return false + } + if m.prefix != "" && !strings.HasPrefix(s, m.prefix) { + return false + } + if m.suffix != "" && !strings.HasSuffix(s, m.suffix) { + return false + } + if m.contains != "" && !strings.Contains(s, m.contains) { + return false + } + if m.stringMatcher != nil { + return m.stringMatcher.Matches(s) + } + return m.re.MatchString(s) + } +} + +// IsOptimized returns true if any fast-path optimization is applied to the +// regex matcher. +func (m *FastRegexMatcher) IsOptimized() bool { + return len(m.setMatches) > 0 || m.stringMatcher != nil || m.prefix != "" || m.suffix != "" || m.contains != "" +} + +// findSetMatches extract equality matches from a regexp. +// Returns nil if we can't replace the regexp by only equality matchers or the regexp contains +// a mix of case sensitive and case insensitive matchers. +func findSetMatches(re *syntax.Regexp) (matches []string, caseSensitive bool) { + clearBeginEndText(re) + + return findSetMatchesInternal(re, "") +} + +func findSetMatchesInternal(re *syntax.Regexp, base string) (matches []string, caseSensitive bool) { + switch re.Op { + case syntax.OpBeginText: + // Correctly handling the begin text operator inside a regex is tricky, + // so in this case we fallback to the regex engine. + return nil, false + case syntax.OpEndText: + // Correctly handling the end text operator inside a regex is tricky, + // so in this case we fallback to the regex engine. + return nil, false + case syntax.OpLiteral: + return []string{base + string(re.Rune)}, isCaseSensitive(re) + case syntax.OpEmptyMatch: + if base != "" { + return []string{base}, isCaseSensitive(re) + } + case syntax.OpAlternate: + return findSetMatchesFromAlternate(re, base) + case syntax.OpCapture: + clearCapture(re) + return findSetMatchesInternal(re, base) + case syntax.OpConcat: + return findSetMatchesFromConcat(re, base) + case syntax.OpCharClass: + if len(re.Rune)%2 != 0 { + return nil, false + } + var matches []string + var totalSet int + for i := 0; i+1 < len(re.Rune); i += 2 { + totalSet += int(re.Rune[i+1]-re.Rune[i]) + 1 + } + // limits the total characters that can be used to create matches. + // In some case like negation [^0-9] a lot of possibilities exists and that + // can create thousands of possible matches at which points we're better off using regexp. + if totalSet > maxSetMatches { + return nil, false + } + for i := 0; i+1 < len(re.Rune); i += 2 { + lo, hi := re.Rune[i], re.Rune[i+1] + for c := lo; c <= hi; c++ { + matches = append(matches, base+string(c)) + } + } + return matches, isCaseSensitive(re) + default: + return nil, false + } + return nil, false +} + +func findSetMatchesFromConcat(re *syntax.Regexp, base string) (matches []string, matchesCaseSensitive bool) { + if len(re.Sub) == 0 { + return nil, false + } + clearCapture(re.Sub...) + + matches = []string{base} + + for i := 0; i < len(re.Sub); i++ { + var newMatches []string + for j, b := range matches { + m, caseSensitive := findSetMatchesInternal(re.Sub[i], b) + if m == nil { + return nil, false + } + if tooManyMatches(newMatches, m...) { + return nil, false + } + + // All matches must have the same case sensitivity. If it's the first set of matches + // returned, we store its sensitivity as the expected case, and then we'll check all + // other ones. + if i == 0 && j == 0 { + matchesCaseSensitive = caseSensitive + } + if matchesCaseSensitive != caseSensitive { + return nil, false + } + + newMatches = append(newMatches, m...) + } + matches = newMatches + } + + return matches, matchesCaseSensitive +} + +func findSetMatchesFromAlternate(re *syntax.Regexp, base string) (matches []string, matchesCaseSensitive bool) { + for i, sub := range re.Sub { + found, caseSensitive := findSetMatchesInternal(sub, base) + if found == nil { + return nil, false + } + if tooManyMatches(matches, found...) { + return nil, false + } + + // All matches must have the same case sensitivity. If it's the first set of matches + // returned, we store its sensitivity as the expected case, and then we'll check all + // other ones. + if i == 0 { + matchesCaseSensitive = caseSensitive + } + if matchesCaseSensitive != caseSensitive { + return nil, false + } + + matches = append(matches, found...) + } + + return matches, matchesCaseSensitive +} + +// clearCapture removes capture operation as they are not used for matching. +func clearCapture(regs ...*syntax.Regexp) { + for _, r := range regs { + // Iterate on the regexp because capture groups could be nested. + for r.Op == syntax.OpCapture { + *r = *r.Sub[0] + } + } +} + +// clearBeginEndText removes the begin and end text from the regexp. Prometheus regexp are anchored to the beginning and end of the string. +func clearBeginEndText(re *syntax.Regexp) { + // Do not clear begin/end text from an alternate operator because it could + // change the actual regexp properties. + if re.Op == syntax.OpAlternate { + return + } + + if len(re.Sub) == 0 { + return + } + if len(re.Sub) == 1 { + if re.Sub[0].Op == syntax.OpBeginText || re.Sub[0].Op == syntax.OpEndText { + // We need to remove this element. Since it's the only one, we convert into a matcher of an empty string. + // OpEmptyMatch is regexp's nop operator. + re.Op = syntax.OpEmptyMatch + re.Sub = nil + return + } + } + if re.Sub[0].Op == syntax.OpBeginText { + re.Sub = re.Sub[1:] + } + if re.Sub[len(re.Sub)-1].Op == syntax.OpEndText { + re.Sub = re.Sub[:len(re.Sub)-1] + } +} + +// isCaseInsensitive tells if a regexp is case insensitive. +// The flag should be check at each level of the syntax tree. +func isCaseInsensitive(reg *syntax.Regexp) bool { + return (reg.Flags & syntax.FoldCase) != 0 +} + +// isCaseSensitive tells if a regexp is case sensitive. +// The flag should be check at each level of the syntax tree. +func isCaseSensitive(reg *syntax.Regexp) bool { + return !isCaseInsensitive(reg) +} + +// tooManyMatches guards against creating too many set matches. +func tooManyMatches(matches []string, added ...string) bool { + return len(matches)+len(added) > maxSetMatches +} + func (m *FastRegexMatcher) MatchString(s string) bool { - if m.literal { - return s == m.value - } - if m.prefix != "" && !strings.HasPrefix(s, m.prefix) { - return false - } - if m.suffix != "" && !strings.HasSuffix(s, m.suffix) { - return false - } - if m.contains != "" && !strings.Contains(s, m.contains) { - return false - } - return m.re.MatchString(s) + return m.matchString(s) +} + +func (m *FastRegexMatcher) SetMatches() []string { + // IMPORTANT: always return a copy, otherwise if the caller manipulate this slice it will + // also get manipulated in the cached FastRegexMatcher instance. + return slices.Clone(m.setMatches) } func (m *FastRegexMatcher) GetRegexString() string { - if m.literal { - return m.value - } - return m.re.String() + return m.reString } -func isLiteral(re string) bool { - return regexp.QuoteMeta(re) == re +// optimizeAlternatingLiterals optimizes a regex of the form +// +// `literal1|literal2|literal3|...` +// +// this function returns an optimized StringMatcher or nil if the regex +// cannot be optimized in this way, and a list of setMatches up to maxSetMatches. +func optimizeAlternatingLiterals(s string) (StringMatcher, []string) { + if len(s) == 0 { + return emptyStringMatcher{}, nil + } + + estimatedAlternates := strings.Count(s, "|") + 1 + + // If there are no alternates, check if the string is a literal + if estimatedAlternates == 1 { + if regexp.QuoteMeta(s) == s { + return &equalStringMatcher{s: s, caseSensitive: true}, []string{s} + } + return nil, nil + } + + multiMatcher := newEqualMultiStringMatcher(true, estimatedAlternates) + + for end := strings.IndexByte(s, '|'); end > -1; end = strings.IndexByte(s, '|') { + // Split the string into the next literal and the remainder + subMatch := s[:end] + s = s[end+1:] + + // break if any of the submatches are not literals + if regexp.QuoteMeta(subMatch) != subMatch { + return nil, nil + } + + multiMatcher.add(subMatch) + } + + // break if the remainder is not a literal + if regexp.QuoteMeta(s) != s { + return nil, nil + } + multiMatcher.add(s) + + return multiMatcher, multiMatcher.setMatches() } // optimizeConcatRegex returns literal prefix/suffix text that can be safely @@ -123,3 +398,540 @@ func optimizeConcatRegex(r *syntax.Regexp) (prefix, suffix, contains string) { return } + +// StringMatcher is a matcher that matches a string in place of a regular expression. +type StringMatcher interface { + Matches(s string) bool +} + +// stringMatcherFromRegexp attempts to replace a common regexp with a string matcher. +// It returns nil if the regexp is not supported. +func stringMatcherFromRegexp(re *syntax.Regexp) StringMatcher { + clearBeginEndText(re) + + m := stringMatcherFromRegexpInternal(re) + m = optimizeEqualStringMatchers(m, minEqualMultiStringMatcherMapThreshold) + + return m +} + +func stringMatcherFromRegexpInternal(re *syntax.Regexp) StringMatcher { + clearCapture(re) + + switch re.Op { + case syntax.OpBeginText: + // Correctly handling the begin text operator inside a regex is tricky, + // so in this case we fallback to the regex engine. + return nil + case syntax.OpEndText: + // Correctly handling the end text operator inside a regex is tricky, + // so in this case we fallback to the regex engine. + return nil + case syntax.OpPlus: + if re.Sub[0].Op != syntax.OpAnyChar && re.Sub[0].Op != syntax.OpAnyCharNotNL { + return nil + } + return &anyNonEmptyStringMatcher{ + matchNL: re.Sub[0].Op == syntax.OpAnyChar, + } + case syntax.OpStar: + if re.Sub[0].Op != syntax.OpAnyChar && re.Sub[0].Op != syntax.OpAnyCharNotNL { + return nil + } + + // If the newline is valid, than this matcher literally match any string (even empty). + if re.Sub[0].Op == syntax.OpAnyChar { + return trueMatcher{} + } + + // Any string is fine (including an empty one), as far as it doesn't contain any newline. + return anyStringWithoutNewlineMatcher{} + case syntax.OpQuest: + // Only optimize for ".?". + if len(re.Sub) != 1 || (re.Sub[0].Op != syntax.OpAnyChar && re.Sub[0].Op != syntax.OpAnyCharNotNL) { + return nil + } + + return &zeroOrOneCharacterStringMatcher{ + matchNL: re.Sub[0].Op == syntax.OpAnyChar, + } + case syntax.OpEmptyMatch: + return emptyStringMatcher{} + + case syntax.OpLiteral: + return &equalStringMatcher{ + s: string(re.Rune), + caseSensitive: !isCaseInsensitive(re), + } + case syntax.OpAlternate: + or := make([]StringMatcher, 0, len(re.Sub)) + for _, sub := range re.Sub { + m := stringMatcherFromRegexpInternal(sub) + if m == nil { + return nil + } + or = append(or, m) + } + return orStringMatcher(or) + case syntax.OpConcat: + clearCapture(re.Sub...) + + if len(re.Sub) == 0 { + return emptyStringMatcher{} + } + if len(re.Sub) == 1 { + return stringMatcherFromRegexpInternal(re.Sub[0]) + } + + var left, right StringMatcher + + // Let's try to find if there's a first and last any matchers. + if re.Sub[0].Op == syntax.OpPlus || re.Sub[0].Op == syntax.OpStar || re.Sub[0].Op == syntax.OpQuest { + left = stringMatcherFromRegexpInternal(re.Sub[0]) + if left == nil { + return nil + } + re.Sub = re.Sub[1:] + } + if re.Sub[len(re.Sub)-1].Op == syntax.OpPlus || re.Sub[len(re.Sub)-1].Op == syntax.OpStar || re.Sub[len(re.Sub)-1].Op == syntax.OpQuest { + right = stringMatcherFromRegexpInternal(re.Sub[len(re.Sub)-1]) + if right == nil { + return nil + } + re.Sub = re.Sub[:len(re.Sub)-1] + } + + matches, matchesCaseSensitive := findSetMatchesInternal(re, "") + + if len(matches) == 0 && len(re.Sub) == 2 { + // We have not find fixed set matches. We look for other known cases that + // we can optimize. + switch { + // Prefix is literal. + case right == nil && re.Sub[0].Op == syntax.OpLiteral: + right = stringMatcherFromRegexpInternal(re.Sub[1]) + if right != nil { + matches = []string{string(re.Sub[0].Rune)} + matchesCaseSensitive = !isCaseInsensitive(re.Sub[0]) + } + + // Suffix is literal. + case left == nil && re.Sub[1].Op == syntax.OpLiteral: + left = stringMatcherFromRegexpInternal(re.Sub[0]) + if left != nil { + matches = []string{string(re.Sub[1].Rune)} + matchesCaseSensitive = !isCaseInsensitive(re.Sub[1]) + } + } + } + + // Ensure we've found some literals to match (optionally with a left and/or right matcher). + // If not, then this optimization doesn't trigger. + if len(matches) == 0 { + return nil + } + + // Use the right (and best) matcher based on what we've found. + switch { + // No left and right matchers (only fixed set matches). + case left == nil && right == nil: + // if there's no any matchers on both side it's a concat of literals + or := make([]StringMatcher, 0, len(matches)) + for _, match := range matches { + or = append(or, &equalStringMatcher{ + s: match, + caseSensitive: matchesCaseSensitive, + }) + } + return orStringMatcher(or) + + // Right matcher with 1 fixed set match. + case left == nil && len(matches) == 1: + return &literalPrefixStringMatcher{ + prefix: matches[0], + prefixCaseSensitive: matchesCaseSensitive, + right: right, + } + + // Left matcher with 1 fixed set match. + case right == nil && len(matches) == 1: + return &literalSuffixStringMatcher{ + left: left, + suffix: matches[0], + suffixCaseSensitive: matchesCaseSensitive, + } + + // We found literals in the middle. We can trigger the fast path only if + // the matches are case sensitive because containsStringMatcher doesn't + // support case insensitive. + case matchesCaseSensitive: + return &containsStringMatcher{ + substrings: matches, + left: left, + right: right, + } + } + } + return nil +} + +// containsStringMatcher matches a string if it contains any of the substrings. +// If left and right are not nil, it's a contains operation where left and right must match. +// If left is nil, it's a hasPrefix operation and right must match. +// Finally, if right is nil it's a hasSuffix operation and left must match. +type containsStringMatcher struct { + // The matcher that must match the left side. Can be nil. + left StringMatcher + + // At least one of these strings must match in the "middle", between left and right matchers. + substrings []string + + // The matcher that must match the right side. Can be nil. + right StringMatcher +} + +func (m *containsStringMatcher) Matches(s string) bool { + for _, substr := range m.substrings { + switch { + case m.right != nil && m.left != nil: + searchStartPos := 0 + + for { + pos := strings.Index(s[searchStartPos:], substr) + if pos < 0 { + break + } + + // Since we started searching from searchStartPos, we have to add that offset + // to get the actual position of the substring inside the text. + pos += searchStartPos + + // If both the left and right matchers match, then we can stop searching because + // we've found a match. + if m.left.Matches(s[:pos]) && m.right.Matches(s[pos+len(substr):]) { + return true + } + + // Continue searching for another occurrence of the substring inside the text. + searchStartPos = pos + 1 + } + case m.left != nil: + // If we have to check for characters on the left then we need to match a suffix. + if strings.HasSuffix(s, substr) && m.left.Matches(s[:len(s)-len(substr)]) { + return true + } + case m.right != nil: + if strings.HasPrefix(s, substr) && m.right.Matches(s[len(substr):]) { + return true + } + } + } + return false +} + +// literalPrefixStringMatcher matches a string with the given literal prefix and right side matcher. +type literalPrefixStringMatcher struct { + prefix string + prefixCaseSensitive bool + + // 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) { + return false + } + if !m.prefixCaseSensitive && !hasPrefixCaseInsensitive(s, m.prefix) { + return false + } + + // Ensure the right side matches. + return m.right.Matches(s[len(m.prefix):]) +} + +// literalSuffixStringMatcher matches a string with the given literal suffix and left side matcher. +type literalSuffixStringMatcher struct { + // The matcher that must match the left side. Can be nil. + left StringMatcher + + suffix string + suffixCaseSensitive bool +} + +func (m *literalSuffixStringMatcher) Matches(s string) bool { + // Ensure the suffix matches. + if m.suffixCaseSensitive && !strings.HasSuffix(s, m.suffix) { + return false + } + if !m.suffixCaseSensitive && !hasSuffixCaseInsensitive(s, m.suffix) { + return false + } + + // Ensure the left side matches. + return m.left.Matches(s[:len(s)-len(m.suffix)]) +} + +// emptyStringMatcher matches an empty string. +type emptyStringMatcher struct{} + +func (m emptyStringMatcher) Matches(s string) bool { + return len(s) == 0 +} + +// orStringMatcher matches any of the sub-matchers. +type orStringMatcher []StringMatcher + +func (m orStringMatcher) Matches(s string) bool { + for _, matcher := range m { + if matcher.Matches(s) { + return true + } + } + return false +} + +// equalStringMatcher matches a string exactly and support case insensitive. +type equalStringMatcher struct { + s string + caseSensitive bool +} + +func (m *equalStringMatcher) Matches(s string) bool { + if m.caseSensitive { + return m.s == s + } + return strings.EqualFold(m.s, s) +} + +type multiStringMatcherBuilder interface { + StringMatcher + add(s string) + setMatches() []string +} + +func newEqualMultiStringMatcher(caseSensitive bool, estimatedSize int) multiStringMatcherBuilder { + // If the estimated size is low enough, it's faster to use a slice instead of a map. + if estimatedSize < minEqualMultiStringMatcherMapThreshold { + return &equalMultiStringSliceMatcher{caseSensitive: caseSensitive, values: make([]string, 0, estimatedSize)} + } + + return &equalMultiStringMapMatcher{ + values: make(map[string]struct{}, estimatedSize), + caseSensitive: caseSensitive, + } +} + +// equalMultiStringSliceMatcher matches a string exactly against a slice of valid values. +type equalMultiStringSliceMatcher struct { + values []string + + caseSensitive bool +} + +func (m *equalMultiStringSliceMatcher) add(s string) { + m.values = append(m.values, s) +} + +func (m *equalMultiStringSliceMatcher) setMatches() []string { + return m.values +} + +func (m *equalMultiStringSliceMatcher) Matches(s string) bool { + if m.caseSensitive { + for _, v := range m.values { + if s == v { + return true + } + } + } else { + for _, v := range m.values { + if strings.EqualFold(s, v) { + return true + } + } + } + return false +} + +// equalMultiStringMapMatcher matches a string exactly against a map of valid values. +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{} + + caseSensitive bool +} + +func (m *equalMultiStringMapMatcher) add(s string) { + if !m.caseSensitive { + s = strings.ToLower(s) + } + + m.values[s] = struct{}{} +} + +func (m *equalMultiStringMapMatcher) setMatches() []string { + if len(m.values) >= maxSetMatches { + return nil + } + + matches := make([]string, 0, len(m.values)) + for s := range m.values { + matches = append(matches, s) + } + return matches +} + +func (m *equalMultiStringMapMatcher) Matches(s string) bool { + if !m.caseSensitive { + s = strings.ToLower(s) + } + + _, ok := m.values[s] + return ok +} + +// anyStringWithoutNewlineMatcher is a stringMatcher which matches any string +// (including an empty one) as far as it doesn't contain any newline character. +type anyStringWithoutNewlineMatcher struct{} + +func (m anyStringWithoutNewlineMatcher) Matches(s string) bool { + // We need to make sure it doesn't contain a newline. Since the newline is + // an ASCII character, we can use strings.IndexByte(). + return strings.IndexByte(s, '\n') == -1 +} + +// anyNonEmptyStringMatcher is a stringMatcher which matches any non-empty string. +type anyNonEmptyStringMatcher struct { + matchNL bool +} + +func (m *anyNonEmptyStringMatcher) Matches(s string) bool { + if m.matchNL { + // It's OK if the string contains a newline so we just need to make + // sure it's non-empty. + return len(s) > 0 + } + + // We need to make sure it non-empty and doesn't contain a newline. + // Since the newline is an ASCII character, we can use strings.IndexByte(). + return len(s) > 0 && strings.IndexByte(s, '\n') == -1 +} + +// zeroOrOneCharacterStringMatcher is a StringMatcher which matches zero or one occurrence +// of any character. The newline character is matches only if matchNL is set to true. +type zeroOrOneCharacterStringMatcher struct { + matchNL bool +} + +func (m *zeroOrOneCharacterStringMatcher) Matches(s string) bool { + // Zero or one. + if len(s) > 1 { + return false + } + + // No need to check for the newline if the string is empty or matching a newline is OK. + if m.matchNL || len(s) == 0 { + return true + } + + return s[0] != '\n' +} + +// trueMatcher is a stringMatcher which matches any string (always returns true). +type trueMatcher struct{} + +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 +// of iterating over the list of strings. +func optimizeEqualStringMatchers(input StringMatcher, threshold int) StringMatcher { + var ( + caseSensitive bool + caseSensitiveSet bool + numValues 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 { + // Ensure we don't have mixed case sensitivity. + if caseSensitiveSet && caseSensitive != matcher.caseSensitive { + return false + } else if !caseSensitiveSet { + caseSensitive = matcher.caseSensitive + caseSensitiveSet = true + } + + numValues++ + return true + } + + if !findEqualStringMatchers(input, analyseCallback) { + return input + } + + // If the number of values found is less than the threshold, then we should skip the optimization. + if numValues < 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) + + // Ignore the return value because we already iterated over the input StringMatcher + // and it was all good. + findEqualStringMatchers(input, func(matcher *equalStringMatcher) bool { + multiMatcher.add(matcher.s) + 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 { + orInput, ok := input.(orStringMatcher) + if !ok { + return false + } + + for _, m := range orInput { + switch casted := m.(type) { + case orStringMatcher: + if !findEqualStringMatchers(m, callback) { + return false + } + + case *equalStringMatcher: + if !callback(casted) { + return false + } + + default: + // It's not an equal string matcher, so we have to stop searching + // cause this optimization can't be applied. + return false + } + } + + return true +} + +func hasPrefixCaseInsensitive(s, prefix string) bool { + return len(s) >= len(prefix) && strings.EqualFold(s[0:len(prefix)], prefix) +} + +func hasSuffixCaseInsensitive(s, suffix string) bool { + return len(s) >= len(suffix) && strings.EqualFold(s[len(s)-len(suffix):], suffix) +} diff --git a/model/labels/regexp_test.go b/model/labels/regexp_test.go index 3188f7cefc..fc21459ed0 100644 --- a/model/labels/regexp_test.go +++ b/model/labels/regexp_test.go @@ -14,49 +14,100 @@ package labels import ( + "fmt" + "math/rand" "strings" "testing" + "time" + "github.com/grafana/regexp" "github.com/grafana/regexp/syntax" "github.com/stretchr/testify/require" ) -func TestNewFastRegexMatcher(t *testing.T) { - cases := []struct { - regex string - value string - expected bool - }{ - {regex: "(foo|bar)", value: "foo", expected: true}, - {regex: "(foo|bar)", value: "foo bar", expected: false}, - {regex: "(foo|bar)", value: "bar", expected: true}, - {regex: "foo.*", value: "foo bar", expected: true}, - {regex: "foo.*", value: "bar foo", expected: false}, - {regex: ".*foo", value: "foo bar", expected: false}, - {regex: ".*foo", value: "bar foo", expected: true}, - {regex: ".*foo", value: "foo", expected: true}, - {regex: "^.*foo$", value: "foo", expected: true}, - {regex: "^.+foo$", value: "foo", expected: false}, - {regex: "^.+foo$", value: "bfoo", expected: true}, - {regex: ".*", value: "\n", expected: false}, - {regex: ".*", value: "\nfoo", expected: false}, - {regex: ".*foo", value: "\nfoo", expected: false}, - {regex: "foo.*", value: "foo\n", expected: false}, - {regex: "foo\n.*", value: "foo\n", expected: true}, - {regex: ".*foo.*", value: "foo", expected: true}, - {regex: ".*foo.*", value: "foo bar", expected: true}, - {regex: ".*foo.*", value: "hello foo world", expected: true}, - {regex: ".*foo.*", value: "hello foo\n world", expected: false}, - {regex: ".*foo\n.*", value: "hello foo\n world", expected: true}, - {regex: ".*", value: "foo", expected: true}, - {regex: "", value: "foo", expected: false}, - {regex: "", value: "", expected: true}, +var ( + asciiRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_") + regexes = []string{ + "", + "foo", + "^foo", + "(foo|bar)", + "foo.*", + ".*foo", + "^.*foo$", + "^.+foo$", + ".*", + ".+", + "foo.+", + ".+foo", + "foo\n.+", + "foo\n.*", + ".*foo.*", + ".+foo.+", + "(?s:.*)", + "(?s:.+)", + "(?s:^.*foo$)", + "(?i:foo)", + "(?i:(foo|bar))", + "(?i:(foo1|foo2|bar))", + "^(?i:foo|oo)|(bar)$", + "(?i:(foo1|foo2|aaa|bbb|ccc|ddd|eee|fff|ggg|hhh|iii|lll|mmm|nnn|ooo|ppp|qqq|rrr|sss|ttt|uuu|vvv|www|xxx|yyy|zzz))", + "((.*)(bar|b|buzz)(.+)|foo)$", + "^$", + "(prometheus|api_prom)_api_v1_.+", + "10\\.0\\.(1|2)\\.+", + "10\\.0\\.(1|2).+", + "((fo(bar))|.+foo)", + // A long case sensitive alternation. + "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", + // An extremely long case sensitive alternation. This is a special + // case because the values share common prefixes rather than being + // entirely random. This is common in the real world. For example, the + // values of a label like kubernetes pod will often include the + // deployment name as a prefix. + "jyyfj00j0061|jyyfj00j0062|jyyfj94j0093|jyyfj99j0093|jyyfm01j0021|jyyfm02j0021|jyefj00j0192|jyefj00j0193|jyefj00j0194|jyefj00j0195|jyefj00j0196|jyefj00j0197|jyefj00j0290|jyefj00j0291|jyefj00j0292|jyefj00j0293|jyefj00j0294|jyefj00j0295|jyefj00j0296|jyefj00j0297|jyefj89j0394|jyefj90j0394|jyefj91j0394|jyefj95j0347|jyefj96j0322|jyefj96j0347|jyefj97j0322|jyefj97j0347|jyefj98j0322|jyefj98j0347|jyefj99j0320|jyefj99j0322|jyefj99j0323|jyefj99j0335|jyefj99j0336|jyefj99j0344|jyefj99j0347|jyefj99j0349|jyefj99j0351|jyeff00j0117|lyyfm01j0025|lyyfm01j0028|lyyfm01j0041|lyyfm01j0133|lyyfm01j0701|lyyfm02j0025|lyyfm02j0028|lyyfm02j0041|lyyfm02j0133|lyyfm02j0701|lyyfm03j0701|lyefj00j0775|lyefj00j0776|lyefj00j0777|lyefj00j0778|lyefj00j0779|lyefj00j0780|lyefj00j0781|lyefj00j0782|lyefj50j3807|lyefj50j3852|lyefj51j3807|lyefj51j3852|lyefj52j3807|lyefj52j3852|lyefj53j3807|lyefj53j3852|lyefj54j3807|lyefj54j3852|lyefj54j3886|lyefj55j3807|lyefj55j3852|lyefj55j3886|lyefj56j3807|lyefj56j3852|lyefj56j3886|lyefj57j3807|lyefj57j3852|lyefj57j3886|lyefj58j3807|lyefj58j3852|lyefj58j3886|lyefj59j3807|lyefj59j3852|lyefj59j3886|lyefj60j3807|lyefj60j3852|lyefj60j3886|lyefj61j3807|lyefj61j3852|lyefj61j3886|lyefj62j3807|lyefj62j3852|lyefj62j3886|lyefj63j3807|lyefj63j3852|lyefj63j3886|lyefj64j3807|lyefj64j3852|lyefj64j3886|lyefj65j3807|lyefj65j3852|lyefj65j3886|lyefj66j3807|lyefj66j3852|lyefj66j3886|lyefj67j3807|lyefj67j3852|lyefj67j3886|lyefj68j3807|lyefj68j3852|lyefj68j3886|lyefj69j3807|lyefj69j3846|lyefj69j3852|lyefj69j3886|lyefj70j3807|lyefj70j3846|lyefj70j3852|lyefj70j3886|lyefj71j3807|lyefj71j3846|lyefj71j3852|lyefj71j3886|lyefj72j3807|lyefj72j3846|lyefj72j3852|lyefj72j3886|lyefj73j3807|lyefj73j3846|lyefj73j3852|lyefj73j3886|lyefj74j3807|lyefj74j3846|lyefj74j3852|lyefj74j3886|lyefj75j3807|lyefj75j3808|lyefj75j3846|lyefj75j3852|lyefj75j3886|lyefj76j3732|lyefj76j3807|lyefj76j3808|lyefj76j3846|lyefj76j3852|lyefj76j3886|lyefj77j3732|lyefj77j3807|lyefj77j3808|lyefj77j3846|lyefj77j3852|lyefj77j3886|lyefj78j3278|lyefj78j3732|lyefj78j3807|lyefj78j3808|lyefj78j3846|lyefj78j3852|lyefj78j3886|lyefj79j3732|lyefj79j3807|lyefj79j3808|lyefj79j3846|lyefj79j3852|lyefj79j3886|lyefj80j3732|lyefj80j3807|lyefj80j3808|lyefj80j3846|lyefj80j3852|lyefj80j3886|lyefj81j3732|lyefj81j3807|lyefj81j3808|lyefj81j3846|lyefj81j3852|lyefj81j3886|lyefj82j3732|lyefj82j3807|lyefj82j3808|lyefj82j3846|lyefj82j3852|lyefj82j3886|lyefj83j3732|lyefj83j3807|lyefj83j3808|lyefj83j3846|lyefj83j3852|lyefj83j3886|lyefj84j3732|lyefj84j3807|lyefj84j3808|lyefj84j3846|lyefj84j3852|lyefj84j3886|lyefj85j3732|lyefj85j3807|lyefj85j3808|lyefj85j3846|lyefj85j3852|lyefj85j3886|lyefj86j3278|lyefj86j3732|lyefj86j3807|lyefj86j3808|lyefj86j3846|lyefj86j3852|lyefj86j3886|lyefj87j3278|lyefj87j3732|lyefj87j3807|lyefj87j3808|lyefj87j3846|lyefj87j3852|lyefj87j3886|lyefj88j3732|lyefj88j3807|lyefj88j3808|lyefj88j3846|lyefj88j3852|lyefj88j3886|lyefj89j3732|lyefj89j3807|lyefj89j3808|lyefj89j3846|lyefj89j3852|lyefj89j3886|lyefj90j3732|lyefj90j3807|lyefj90j3808|lyefj90j3846|lyefj90j3852|lyefj90j3886|lyefj91j3732|lyefj91j3807|lyefj91j3808|lyefj91j3846|lyefj91j3852|lyefj91j3886|lyefj92j3732|lyefj92j3807|lyefj92j3808|lyefj92j3846|lyefj92j3852|lyefj92j3886|lyefj93j3732|lyefj93j3807|lyefj93j3808|lyefj93j3846|lyefj93j3852|lyefj93j3885|lyefj93j3886|lyefj94j3525|lyefj94j3732|lyefj94j3807|lyefj94j3808|lyefj94j3846|lyefj94j3852|lyefj94j3885|lyefj94j3886|lyefj95j3525|lyefj95j3732|lyefj95j3807|lyefj95j3808|lyefj95j3846|lyefj95j3852|lyefj95j3886|lyefj96j3732|lyefj96j3803|lyefj96j3807|lyefj96j3808|lyefj96j3846|lyefj96j3852|lyefj96j3886|lyefj97j3333|lyefj97j3732|lyefj97j3792|lyefj97j3803|lyefj97j3807|lyefj97j3808|lyefj97j3838|lyefj97j3843|lyefj97j3846|lyefj97j3852|lyefj97j3886|lyefj98j3083|lyefj98j3333|lyefj98j3732|lyefj98j3807|lyefj98j3808|lyefj98j3838|lyefj98j3843|lyefj98j3846|lyefj98j3852|lyefj98j3873|lyefj98j3877|lyefj98j3882|lyefj98j3886|lyefj99j2984|lyefj99j3083|lyefj99j3333|lyefj99j3732|lyefj99j3807|lyefj99j3808|lyefj99j3846|lyefj99j3849|lyefj99j3852|lyefj99j3873|lyefj99j3877|lyefj99j3882|lyefj99j3884|lyefj99j3886|lyeff00j0106|lyeff00j0107|lyeff00j0108|lyeff00j0129|lyeff00j0130|lyeff00j0131|lyeff00j0132|lyeff00j0133|lyeff00j0134|lyeff00j0444|lyeff00j0445|lyeff91j0473|lyeff92j0473|lyeff92j3877|lyeff93j3877|lyeff94j0501|lyeff94j3525|lyeff94j3877|lyeff95j0501|lyeff95j3525|lyeff95j3877|lyeff96j0503|lyeff96j3877|lyeff97j3877|lyeff98j3333|lyeff98j3877|lyeff99j2984|lyeff99j3333|lyeff99j3877|mfyr9149ej|mfyr9149ek|mfyr9156ej|mfyr9156ek|mfyr9157ej|mfyr9157ek|mfyr9159ej|mfyr9159ek|mfyr9203ej|mfyr9204ej|mfyr9205ej|mfyr9206ej|mfyr9207ej|mfyr9207ek|mfyr9217ej|mfyr9217ek|mfyr9222ej|mfyr9222ek|mfyu0185ej|mfye9187ej|mfye9187ek|mfye9188ej|mfye9188ek|mfye9189ej|mfye9189ek|mfyf0185ej|oyefj87j0007|oyefj88j0007|oyefj89j0007|oyefj90j0007|oyefj91j0007|oyefj95j0001|oyefj96j0001|oyefj98j0004|oyefj99j0004|oyeff91j0004|oyeff92j0004|oyeff93j0004|oyeff94j0004|oyeff95j0004|oyeff96j0004|rklvyaxmany|ryefj93j0001|ryefj94j0001|tyyfj00a0001|tyyfj84j0005|tyyfj85j0005|tyyfj86j0005|tyyfj87j0005|tyyfj88j0005|tyyfj89j0005|tyyfj90j0005|tyyfj91j0005|tyyfj92j0005|tyyfj93j0005|tyyfj94j0005|tyyfj95j0005|tyyfj96j0005|tyyfj97j0005|tyyfj98j0005|tyyfj99j0005|tyefj50j0015|tyefj50j0017|tyefj50j0019|tyefj50j0020|tyefj50j0021|tyefj51j0015|tyefj51j0017|tyefj51j0019|tyefj51j0020|tyefj51j0021|tyefj52j0015|tyefj52j0017|tyefj52j0019|tyefj52j0020|tyefj52j0021|tyefj53j0015|tyefj53j0017|tyefj53j0019|tyefj53j0020|tyefj53j0021|tyefj54j0015|tyefj54j0017|tyefj54j0019|tyefj54j0020|tyefj54j0021|tyefj55j0015|tyefj55j0017|tyefj55j0019|tyefj55j0020|tyefj55j0021|tyefj56j0015|tyefj56j0017|tyefj56j0019|tyefj56j0020|tyefj56j0021|tyefj57j0015|tyefj57j0017|tyefj57j0019|tyefj57j0020|tyefj57j0021|tyefj58j0015|tyefj58j0017|tyefj58j0019|tyefj58j0020|tyefj58j0021|tyefj59j0015|tyefj59j0017|tyefj59j0019|tyefj59j0020|tyefj59j0021|tyefj60j0015|tyefj60j0017|tyefj60j0019|tyefj60j0020|tyefj60j0021|tyefj61j0015|tyefj61j0017|tyefj61j0019|tyefj61j0020|tyefj61j0021|tyefj62j0015|tyefj62j0017|tyefj62j0019|tyefj62j0020|tyefj62j0021|tyefj63j0015|tyefj63j0017|tyefj63j0019|tyefj63j0020|tyefj63j0021|tyefj64j0015|tyefj64j0017|tyefj64j0019|tyefj64j0020|tyefj64j0021|tyefj65j0015|tyefj65j0017|tyefj65j0019|tyefj65j0020|tyefj65j0021|tyefj66j0015|tyefj66j0017|tyefj66j0019|tyefj66j0020|tyefj66j0021|tyefj67j0015|tyefj67j0017|tyefj67j0019|tyefj67j0020|tyefj67j0021|tyefj68j0015|tyefj68j0017|tyefj68j0019|tyefj68j0020|tyefj68j0021|tyefj69j0015|tyefj69j0017|tyefj69j0019|tyefj69j0020|tyefj69j0021|tyefj70j0015|tyefj70j0017|tyefj70j0019|tyefj70j0020|tyefj70j0021|tyefj71j0015|tyefj71j0017|tyefj71j0019|tyefj71j0020|tyefj71j0021|tyefj72j0015|tyefj72j0017|tyefj72j0019|tyefj72j0020|tyefj72j0021|tyefj72j0022|tyefj73j0015|tyefj73j0017|tyefj73j0019|tyefj73j0020|tyefj73j0021|tyefj73j0022|tyefj74j0015|tyefj74j0017|tyefj74j0019|tyefj74j0020|tyefj74j0021|tyefj74j0022|tyefj75j0015|tyefj75j0017|tyefj75j0019|tyefj75j0020|tyefj75j0021|tyefj75j0022|tyefj76j0015|tyefj76j0017|tyefj76j0019|tyefj76j0020|tyefj76j0021|tyefj76j0022|tyefj76j0119|tyefj77j0015|tyefj77j0017|tyefj77j0019|tyefj77j0020|tyefj77j0021|tyefj77j0022|tyefj77j0119|tyefj78j0015|tyefj78j0017|tyefj78j0019|tyefj78j0020|tyefj78j0021|tyefj78j0022|tyefj78j0119|tyefj79j0015|tyefj79j0017|tyefj79j0019|tyefj79j0020|tyefj79j0021|tyefj79j0022|tyefj79j0119|tyefj80j0015|tyefj80j0017|tyefj80j0019|tyefj80j0020|tyefj80j0021|tyefj80j0022|tyefj80j0114|tyefj80j0119|tyefj81j0015|tyefj81j0017|tyefj81j0019|tyefj81j0020|tyefj81j0021|tyefj81j0022|tyefj81j0114|tyefj81j0119|tyefj82j0015|tyefj82j0017|tyefj82j0019|tyefj82j0020|tyefj82j0021|tyefj82j0022|tyefj82j0119|tyefj83j0015|tyefj83j0017|tyefj83j0019|tyefj83j0020|tyefj83j0021|tyefj83j0022|tyefj83j0119|tyefj84j0014|tyefj84j0015|tyefj84j0017|tyefj84j0019|tyefj84j0020|tyefj84j0021|tyefj84j0022|tyefj84j0119|tyefj85j0014|tyefj85j0015|tyefj85j0017|tyefj85j0019|tyefj85j0020|tyefj85j0021|tyefj85j0022|tyefj85j0119|tyefj86j0014|tyefj86j0015|tyefj86j0017|tyefj86j0019|tyefj86j0020|tyefj86j0021|tyefj86j0022|tyefj87j0014|tyefj87j0015|tyefj87j0017|tyefj87j0019|tyefj87j0020|tyefj87j0021|tyefj87j0022|tyefj88j0014|tyefj88j0015|tyefj88j0017|tyefj88j0019|tyefj88j0020|tyefj88j0021|tyefj88j0022|tyefj88j0100|tyefj88j0115|tyefj89j0003|tyefj89j0014|tyefj89j0015|tyefj89j0017|tyefj89j0019|tyefj89j0020|tyefj89j0021|tyefj89j0022|tyefj89j0100|tyefj89j0115|tyefj90j0014|tyefj90j0015|tyefj90j0016|tyefj90j0017|tyefj90j0018|tyefj90j0019|tyefj90j0020|tyefj90j0021|tyefj90j0022|tyefj90j0100|tyefj90j0111|tyefj90j0115|tyefj91j0014|tyefj91j0015|tyefj91j0016|tyefj91j0017|tyefj91j0018|tyefj91j0019|tyefj91j0020|tyefj91j0021|tyefj91j0022|tyefj91j0100|tyefj91j0111|tyefj91j0115|tyefj92j0014|tyefj92j0015|tyefj92j0016|tyefj92j0017|tyefj92j0018|tyefj92j0019|tyefj92j0020|tyefj92j0021|tyefj92j0022|tyefj92j0100|tyefj92j0105|tyefj92j0115|tyefj92j0121|tyefj93j0004|tyefj93j0014|tyefj93j0015|tyefj93j0017|tyefj93j0018|tyefj93j0019|tyefj93j0020|tyefj93j0021|tyefj93j0022|tyefj93j0100|tyefj93j0105|tyefj93j0115|tyefj93j0121|tyefj94j0002|tyefj94j0004|tyefj94j0008|tyefj94j0014|tyefj94j0015|tyefj94j0017|tyefj94j0019|tyefj94j0020|tyefj94j0021|tyefj94j0022|tyefj94j0084|tyefj94j0088|tyefj94j0100|tyefj94j0106|tyefj94j0116|tyefj94j0121|tyefj94j0123|tyefj95j0002|tyefj95j0004|tyefj95j0008|tyefj95j0014|tyefj95j0015|tyefj95j0017|tyefj95j0019|tyefj95j0020|tyefj95j0021|tyefj95j0022|tyefj95j0084|tyefj95j0088|tyefj95j0100|tyefj95j0101|tyefj95j0106|tyefj95j0112|tyefj95j0116|tyefj95j0121|tyefj95j0123|tyefj96j0014|tyefj96j0015|tyefj96j0017|tyefj96j0019|tyefj96j0020|tyefj96j0021|tyefj96j0022|tyefj96j0082|tyefj96j0084|tyefj96j0100|tyefj96j0101|tyefj96j0112|tyefj96j0117|tyefj96j0121|tyefj96j0124|tyefj97j0014|tyefj97j0015|tyefj97j0017|tyefj97j0019|tyefj97j0020|tyefj97j0021|tyefj97j0022|tyefj97j0081|tyefj97j0087|tyefj97j0098|tyefj97j0100|tyefj97j0107|tyefj97j0109|tyefj97j0113|tyefj97j0117|tyefj97j0118|tyefj97j0121|tyefj98j0003|tyefj98j0006|tyefj98j0014|tyefj98j0015|tyefj98j0017|tyefj98j0019|tyefj98j0020|tyefj98j0021|tyefj98j0022|tyefj98j0083|tyefj98j0085|tyefj98j0086|tyefj98j0100|tyefj98j0104|tyefj98j0118|tyefj98j0121|tyefj99j0003|tyefj99j0006|tyefj99j0007|tyefj99j0014|tyefj99j0015|tyefj99j0017|tyefj99j0019|tyefj99j0020|tyefj99j0021|tyefj99j0022|tyefj99j0023|tyefj99j0100|tyefj99j0108|tyefj99j0110|tyefj99j0121|tyefj99j0125|tyeff94j0002|tyeff94j0008|tyeff94j0010|tyeff94j0011|tyeff94j0035|tyeff95j0002|tyeff95j0006|tyeff95j0008|tyeff95j0010|tyeff95j0011|tyeff95j0035|tyeff96j0003|tyeff96j0006|tyeff96j0009|tyeff96j0010|tyeff97j0004|tyeff97j0009|tyeff97j0116|tyeff98j0007|tyeff99j0007|tyeff99j0125|uyyfj00j0484|uyyfj00j0485|uyyfj00j0486|uyyfj00j0487|uyyfj00j0488|uyyfj00j0489|uyyfj00j0490|uyyfj00j0491|uyyfj00j0492|uyyfj00j0493|uyyfj00j0494|uyyfj00j0495|uyyfj00j0496|uyyfj00j0497|uyyfj00j0498|uyyfj00j0499|uyyfj00j0500|uyyfj00j0501|uyyfj00j0502|uyyfj00j0503|uyyfj00j0504|uyyfj00j0505|uyyfj00j0506|uyyfj00j0507|uyyfj00j0508|uyyfj00j0509|uyyfj00j0510|uyyfj00j0511|uyyfj00j0512|uyyfj00j0513|uyyfj00j0514|uyyfj00j0515|uyyfj00j0516|uyyfj00j0517|uyyfj00j0518|uyyfj00j0519|uyyfj00j0520|uyyfj00j0521|uyyfj00j0522|uyyfj00j0523|uyyfj00j0524|uyyfj00j0525|uyyfj00j0526|uyyfj00j0527|uyyfj00j0528|uyyfj00j0529|uyyfj00j0530|uyyfj00j0531|uyyfj00j0532|uyyfj00j0533|uyyfj00j0534|uyyfj00j0535|uyyfj00j0536|uyyfj00j0537|uyyfj00j0538|uyyfj00j0539|uyyfj00j0540|uyyfj00j0541|uyyfj00j0542|uyyfj00j0543|uyyfj00j0544|uyyfj00j0545|uyyfj00j0546|uyyfj00j0547|uyyfj00j0548|uyyfj00j0549|uyyfj00j0550|uyyfj00j0551|uyyfj00j0553|uyyfj00j0554|uyyfj00j0555|uyyfj00j0556|uyyfj00j0557|uyyfj00j0558|uyyfj00j0559|uyyfj00j0560|uyyfj00j0561|uyyfj00j0562|uyyfj00j0563|uyyfj00j0564|uyyfj00j0565|uyyfj00j0566|uyyfj00j0614|uyyfj00j0615|uyyfj00j0616|uyyfj00j0617|uyyfj00j0618|uyyfj00j0619|uyyfj00j0620|uyyfj00j0621|uyyfj00j0622|uyyfj00j0623|uyyfj00j0624|uyyfj00j0625|uyyfj00j0626|uyyfj00j0627|uyyfj00j0628|uyyfj00j0629|uyyfj00j0630|uyyfj00j0631|uyyfj00j0632|uyyfj00j0633|uyyfj00j0634|uyyfj00j0635|uyyfj00j0636|uyyfj00j0637|uyyfj00j0638|uyyfj00j0639|uyyfj00j0640|uyyfj00j0641|uyyfj00j0642|uyyfj00j0643|uyyfj00j0644|uyyfj00j0645|uyyfj00j0646|uyyfj00j0647|uyyfj00j0648|uyyfj00j0649|uyyfj00j0650|uyyfj00j0651|uyyfj00j0652|uyyfj00j0653|uyyfj00j0654|uyyfj00j0655|uyyfj00j0656|uyyfj00j0657|uyyfj00j0658|uyyfj00j0659|uyyfj00j0660|uyyfj00j0661|uyyfj00j0662|uyyfj00j0663|uyyfj00j0664|uyyfj00j0665|uyyfj00j0666|uyyfj00j0667|uyyfj00j0668|uyyfj00j0669|uyyfj00j0670|uyyfj00j0671|uyyfj00j0672|uyyfj00j0673|uyyfj00j0674|uyyfj00j0675|uyyfj00j0676|uyyfj00j0677|uyyfj00j0678|uyyfj00j0679|uyyfj00j0680|uyyfj00j0681|uyyfj00j0682|uyyfj00j0683|uyyfj00j0684|uyyfj00j0685|uyyfj00j0686|uyyfj00j0687|uyyfj00j0688|uyyfj00j0689|uyyfj00j0690|uyyfj00j0691|uyyfj00j0692|uyyfj00j0693|uyyfj00j0694|uyyfj00j0695|uyyfj00j0696|uyyfj00j0697|uyyfj00j0698|uyyfj00j0699|uyyfj00j0700|uyyfj00j0701|uyyfj00j0702|uyyfj00j0703|uyyfj00j0704|uyyfj00j0705|uyyfj00j0706|uyyfj00j0707|uyyfj00j0708|uyyfj00j0709|uyyfj00j0710|uyyfj00j0711|uyyfj00j0712|uyyfj00j0713|uyyfj00j0714|uyyfj00j0715|uyyfj00j0716|uyyfj00j0717|uyyfj00j0718|uyyfj00j0719|uyyfj00j0720|uyyfj00j0721|uyyfj00j0722|uyyfj00j0723|uyyfj00j0724|uyyfj00j0725|uyyfj00j0726|uyyfj00j0727|uyyfj00j0728|uyyfj00j0729|uyyfj00j0730|uyyfj00j0731|uyyfj00j0732|uyyfj00j0733|uyyfj00j0734|uyyfj00j0735|uyyfj00j0736|uyyfj00j0737|uyyfj00j0738|uyyfj00j0739|uyyfj00j0740|uyyfj00j0741|uyyfj00j0742|uyyfj00j0743|uyyfj00j0744|uyyfj00j0745|uyyfj00j0746|uyyfj00j0747|uyyfj00j0748|uyyfj00j0749|uyyfj00j0750|uyyfj00j0751|uyyfj00j0752|uyyfj00j0753|uyyfj00j0754|uyyfj00j0755|uyyfj00j0756|uyyfj00j0757|uyyfj00j0758|uyyfj00j0759|uyyfj00j0760|uyyfj00j0761|uyyfj00j0762|uyyfj00j0763|uyyfj00j0764|uyyfj00j0765|uyyfj00j0766|uyyfj00j0767|uyyfj00j0768|uyyfj00j0769|uyyfj00j0770|uyyfj00j0771|uyyfj00j0772|uyyfj00j0773|uyyfj00j0774|uyyfj00j0775|uyyfj00j0776|uyyfj00j0777|uyyfj00j0778|uyyfj00j0779|uyyfj00j0780|uyyfj00j0781|uyyfj00j0782|uyyff00j0011|uyyff00j0031|uyyff00j0032|uyyff00j0033|uyyff00j0034|uyyff99j0012|uyefj00j0071|uyefj00j0455|uyefj00j0456|uyefj00j0582|uyefj00j0583|uyefj00j0584|uyefj00j0585|uyefj00j0586|uyefj00j0590|uyeff00j0188|xyrly-f-jyy-y01|xyrly-f-jyy-y02|xyrly-f-jyy-y03|xyrly-f-jyy-y04|xyrly-f-jyy-y05|xyrly-f-jyy-y06|xyrly-f-jyy-y07|xyrly-f-jyy-y08|xyrly-f-jyy-y09|xyrly-f-jyy-y10|xyrly-f-jyy-y11|xyrly-f-jyy-y12|xyrly-f-jyy-y13|xyrly-f-jyy-y14|xyrly-f-jyy-y15|xyrly-f-jyy-y16|xyrly-f-url-y01|xyrly-f-url-y02|yyefj97j0005|ybyfcy4000|ybyfcy4001|ayefj99j0035|by-b-y-bzu-l01|by-b-y-bzu-l02|by-b-e-079|by-b-e-080|by-b-e-082|by-b-e-083|byefj72j0002|byefj73j0002|byefj74j0002|byefj75j0002|byefj76j0002|byefj77j0002|byefj78j0002|byefj79j0002|byefj91j0007|byefj92j0007|byefj98j0003|byefj99j0003|byefj99j0005|byefj99j0006|byeff88j0002|byeff89j0002|byeff90j0002|byeff91j0002|byeff92j0002|byeff93j0002|byeff96j0003|byeff97j0003|byeff98j0003|byeff99j0003|fymfj98j0001|fymfj99j0001|fyyaj98k0297|fyyaj99k0297|fyyfj00j0109|fyyfj00j0110|fyyfj00j0122|fyyfj00j0123|fyyfj00j0201|fyyfj00j0202|fyyfj00j0207|fyyfj00j0208|fyyfj00j0227|fyyfj00j0228|fyyfj00j0229|fyyfj00j0230|fyyfj00j0231|fyyfj00j0232|fyyfj00j0233|fyyfj00j0234|fyyfj00j0235|fyyfj00j0236|fyyfj00j0237|fyyfj00j0238|fyyfj00j0239|fyyfj00j0240|fyyfj00j0241|fyyfj00j0242|fyyfj00j0243|fyyfj00j0244|fyyfj00j0245|fyyfj00j0246|fyyfj00j0247|fyyfj00j0248|fyyfj00j0249|fyyfj00j0250|fyyfj00j0251|fyyfj00j0252|fyyfj00j0253|fyyfj00j0254|fyyfj00j0255|fyyfj00j0256|fyyfj00j0257|fyyfj00j0258|fyyfj00j0259|fyyfj00j0260|fyyfj00j0261|fyyfj00j0262|fyyfj00j0263|fyyfj00j0264|fyyfj00j0265|fyyfj00j0266|fyyfj00j0267|fyyfj00j0268|fyyfj00j0290|fyyfj00j0291|fyyfj00j0292|fyyfj00j0293|fyyfj00j0294|fyyfj00j0295|fyyfj00j0296|fyyfj00j0297|fyyfj00j0298|fyyfj00j0299|fyyfj00j0300|fyyfj00j0301|fyyfj00j0302|fyyfj00j0303|fyyfj00j0304|fyyfj00j0305|fyyfj00j0306|fyyfj00j0307|fyyfj00j0308|fyyfj00j0309|fyyfj00j0310|fyyfj00j0311|fyyfj00j0312|fyyfj00j0313|fyyfj00j0314|fyyfj00j0315|fyyfj00j0316|fyyfj00j0317|fyyfj00j0318|fyyfj00j0319|fyyfj00j0320|fyyfj00j0321|fyyfj00j0322|fyyfj00j0323|fyyfj00j0324|fyyfj00j0325|fyyfj00j0326|fyyfj00j0327|fyyfj00j0328|fyyfj00j0329|fyyfj00j0330|fyyfj00j0331|fyyfj00j0332|fyyfj00j0333|fyyfj00j0334|fyyfj00j0335|fyyfj00j0340|fyyfj00j0341|fyyfj00j0342|fyyfj00j0343|fyyfj00j0344|fyyfj00j0345|fyyfj00j0346|fyyfj00j0347|fyyfj00j0348|fyyfj00j0349|fyyfj00j0367|fyyfj00j0368|fyyfj00j0369|fyyfj00j0370|fyyfj00j0371|fyyfj00j0372|fyyfj00j0373|fyyfj00j0374|fyyfj00j0375|fyyfj00j0376|fyyfj00j0377|fyyfj00j0378|fyyfj00j0379|fyyfj00j0380|fyyfj00j0381|fyyfj00j0382|fyyfj00j0383|fyyfj00j0384|fyyfj00j0385|fyyfj00j0386|fyyfj00j0387|fyyfj00j0388|fyyfj00j0415|fyyfj00j0416|fyyfj00j0417|fyyfj00j0418|fyyfj00j0419|fyyfj00j0420|fyyfj00j0421|fyyfj00j0422|fyyfj00j0423|fyyfj00j0424|fyyfj00j0425|fyyfj00j0426|fyyfj00j0427|fyyfj00j0428|fyyfj00j0429|fyyfj00j0430|fyyfj00j0431|fyyfj00j0432|fyyfj00j0433|fyyfj00j0434|fyyfj00j0435|fyyfj00j0436|fyyfj00j0437|fyyfj00j0438|fyyfj00j0439|fyyfj00j0440|fyyfj00j0441|fyyfj00j0446|fyyfj00j0447|fyyfj00j0448|fyyfj00j0449|fyyfj00j0451|fyyfj00j0452|fyyfj00j0453|fyyfj00j0454|fyyfj00j0455|fyyfj00j0456|fyyfj00j0457|fyyfj00j0459|fyyfj00j0460|fyyfj00j0461|fyyfj00j0462|fyyfj00j0463|fyyfj00j0464|fyyfj00j0465|fyyfj00j0466|fyyfj00j0467|fyyfj00j0468|fyyfj00j0469|fyyfj00j0470|fyyfj00j0471|fyyfj00j0474|fyyfj00j0475|fyyfj00j0476|fyyfj00j0477|fyyfj00j0478|fyyfj00j0479|fyyfj00j0480|fyyfj00j0481|fyyfj00j0482|fyyfj00j0483|fyyfj00j0484|fyyfj00j0485|fyyfj00j0486|fyyfj00j0487|fyyfj00j0488|fyyfj00j0489|fyyfj00j0490|fyyfj00j0491|fyyfj00j0492|fyyfj00j0493|fyyfj00j0494|fyyfj00j0495|fyyfj00j0496|fyyfj00j0497|fyyfj00j0498|fyyfj00j0499|fyyfj00j0500|fyyfj00j0501|fyyfj00j0502|fyyfj00j0503|fyyfj00j0504|fyyfj00j0505|fyyfj00j0506|fyyfj00j0507|fyyfj00j0508|fyyfj00j0509|fyyfj00j0510|fyyfj00j0511|fyyfj00j0512|fyyfj00j0513|fyyfj00j0514|fyyfj00j0515|fyyfj00j0516|fyyfj00j0517|fyyfj00j0518|fyyfj00j0521|fyyfj00j0522|fyyfj00j0523|fyyfj00j0524|fyyfj00j0526|fyyfj00j0527|fyyfj00j0528|fyyfj00j0529|fyyfj00j0530|fyyfj00j0531|fyyfj00j0532|fyyfj00j0533|fyyfj00j0534|fyyfj00j0535|fyyfj00j0536|fyyfj00j0537|fyyfj00j0538|fyyfj00j0539|fyyfj00j0540|fyyfj00j0541|fyyfj00j0542|fyyfj00j0543|fyyfj00j0544|fyyfj00j0545|fyyfj00j0546|fyyfj00j0564|fyyfj00j0565|fyyfj00j0566|fyyfj00j0567|fyyfj00j0568|fyyfj00j0569|fyyfj00j0570|fyyfj00j0571|fyyfj00j0572|fyyfj00j0574|fyyfj00j0575|fyyfj00j0576|fyyfj00j0577|fyyfj00j0578|fyyfj00j0579|fyyfj00j0580|fyyfj01j0473|fyyfj02j0473|fyyfj36j0289|fyyfj37j0209|fyyfj37j0289|fyyfj38j0209|fyyfj38j0289|fyyfj39j0209|fyyfj39j0289|fyyfj40j0209|fyyfj40j0289|fyyfj41j0209|fyyfj41j0289|fyyfj42j0209|fyyfj42j0289|fyyfj43j0209|fyyfj43j0289|fyyfj44j0209|fyyfj44j0289|fyyfj45j0104|fyyfj45j0209|fyyfj45j0289|fyyfj46j0104|fyyfj46j0209|fyyfj46j0289|fyyfj47j0104|fyyfj47j0209|fyyfj47j0289|fyyfj48j0104|fyyfj48j0209|fyyfj48j0289|fyyfj49j0104|fyyfj49j0209|fyyfj49j0289|fyyfj50j0104|fyyfj50j0209|fyyfj50j0289|fyyfj50j0500|fyyfj51j0104|fyyfj51j0209|fyyfj51j0289|fyyfj51j0500|fyyfj52j0104|fyyfj52j0209|fyyfj52j0289|fyyfj52j0500|fyyfj53j0104|fyyfj53j0209|fyyfj53j0289|fyyfj53j0500|fyyfj54j0104|fyyfj54j0209|fyyfj54j0289|fyyfj54j0500|fyyfj55j0104|fyyfj55j0209|fyyfj55j0289|fyyfj55j0500|fyyfj56j0104|fyyfj56j0209|fyyfj56j0289|fyyfj56j0500|fyyfj57j0104|fyyfj57j0209|fyyfj57j0289|fyyfj57j0500|fyyfj58j0104|fyyfj58j0209|fyyfj58j0289|fyyfj58j0500|fyyfj59j0104|fyyfj59j0209|fyyfj59j0289|fyyfj59j0500|fyyfj60j0104|fyyfj60j0209|fyyfj60j0289|fyyfj60j0500|fyyfj61j0104|fyyfj61j0209|fyyfj61j0289|fyyfj61j0500|fyyfj62j0104|fyyfj62j0209|fyyfj62j0289|fyyfj62j0500|fyyfj63j0104|fyyfj63j0209|fyyfj63j0289|fyyfj63j0500|fyyfj64j0104|fyyfj64j0107|fyyfj64j0209|fyyfj64j0289|fyyfj64j0500|fyyfj64j0573|fyyfj65j0104|fyyfj65j0107|fyyfj65j0209|fyyfj65j0289|fyyfj65j0500|fyyfj65j0573|fyyfj66j0104|fyyfj66j0107|fyyfj66j0209|fyyfj66j0289|fyyfj66j0500|fyyfj66j0573|fyyfj67j0104|fyyfj67j0107|fyyfj67j0209|fyyfj67j0289|fyyfj67j0500|fyyfj67j0573|fyyfj68j0104|fyyfj68j0107|fyyfj68j0209|fyyfj68j0289|fyyfj68j0500|fyyfj68j0573|fyyfj69j0104|fyyfj69j0107|fyyfj69j0209|fyyfj69j0289|fyyfj69j0500|fyyfj69j0573|fyyfj70j0104|fyyfj70j0107|fyyfj70j0209|fyyfj70j0289|fyyfj70j0472|fyyfj70j0500|fyyfj70j0573|fyyfj71j0104|fyyfj71j0107|fyyfj71j0209|fyyfj71j0289|fyyfj71j0472|fyyfj71j0500|fyyfj71j0573|fyyfj72j0104|fyyfj72j0107|fyyfj72j0209|fyyfj72j0289|fyyfj72j0472|fyyfj72j0500|fyyfj72j0573|fyyfj73j0104|fyyfj73j0107|fyyfj73j0209|fyyfj73j0289|fyyfj73j0472|fyyfj73j0500|fyyfj73j0573|fyyfj74j0104|fyyfj74j0107|fyyfj74j0209|fyyfj74j0289|fyyfj74j0472|fyyfj74j0500|fyyfj74j0573|fyyfj75j0104|fyyfj75j0107|fyyfj75j0108|fyyfj75j0209|fyyfj75j0289|fyyfj75j0472|fyyfj75j0500|fyyfj75j0573|fyyfj76j0104|fyyfj76j0107|fyyfj76j0108|fyyfj76j0209|fyyfj76j0289|fyyfj76j0472|fyyfj76j0500|fyyfj76j0573|fyyfj77j0104|fyyfj77j0107|fyyfj77j0108|fyyfj77j0209|fyyfj77j0289|fyyfj77j0472|fyyfj77j0500|fyyfj77j0573|fyyfj78j0104|fyyfj78j0107|fyyfj78j0108|fyyfj78j0209|fyyfj78j0289|fyyfj78j0472|fyyfj78j0500|fyyfj78j0573|fyyfj79j0104|fyyfj79j0107|fyyfj79j0108|fyyfj79j0209|fyyfj79j0289|fyyfj79j0339|fyyfj79j0472|fyyfj79j0500|fyyfj79j0573|fyyfj80j0104|fyyfj80j0107|fyyfj80j0108|fyyfj80j0209|fyyfj80j0289|fyyfj80j0339|fyyfj80j0352|fyyfj80j0472|fyyfj80j0500|fyyfj80j0573|fyyfj81j0104|fyyfj81j0107|fyyfj81j0108|fyyfj81j0209|fyyfj81j0289|fyyfj81j0339|fyyfj81j0352|fyyfj81j0472|fyyfj81j0500|fyyfj81j0573|fyyfj82j0104|fyyfj82j0107|fyyfj82j0108|fyyfj82j0209|fyyfj82j0289|fyyfj82j0339|fyyfj82j0352|fyyfj82j0472|fyyfj82j0500|fyyfj82j0573|fyyfj83j0104|fyyfj83j0107|fyyfj83j0108|fyyfj83j0209|fyyfj83j0289|fyyfj83j0339|fyyfj83j0352|fyyfj83j0472|fyyfj83j0500|fyyfj83j0573|fyyfj84j0104|fyyfj84j0107|fyyfj84j0108|fyyfj84j0209|fyyfj84j0289|fyyfj84j0339|fyyfj84j0352|fyyfj84j0472|fyyfj84j0500|fyyfj84j0573|fyyfj85j0104|fyyfj85j0107|fyyfj85j0108|fyyfj85j0209|fyyfj85j0289|fyyfj85j0301|fyyfj85j0339|fyyfj85j0352|fyyfj85j0472|fyyfj85j0500|fyyfj85j0573|fyyfj86j0104|fyyfj86j0107|fyyfj86j0108|fyyfj86j0209|fyyfj86j0289|fyyfj86j0301|fyyfj86j0339|fyyfj86j0352|fyyfj86j0472|fyyfj86j0500|fyyfj86j0573|fyyfj87j0067|fyyfj87j0104|fyyfj87j0107|fyyfj87j0108|fyyfj87j0209|fyyfj87j0289|fyyfj87j0301|fyyfj87j0339|fyyfj87j0352|fyyfj87j0472|fyyfj87j0500|fyyfj87j0573|fyyfj88j0067|fyyfj88j0104|fyyfj88j0107|fyyfj88j0108|fyyfj88j0209|fyyfj88j0289|fyyfj88j0301|fyyfj88j0339|fyyfj88j0352|fyyfj88j0472|fyyfj88j0500|fyyfj88j0573|fyyfj89j0067|fyyfj89j0104|fyyfj89j0107|fyyfj89j0108|fyyfj89j0209|fyyfj89j0289|fyyfj89j0301|fyyfj89j0339|fyyfj89j0352|fyyfj89j0358|fyyfj89j0472|fyyfj89j0500|fyyfj89j0573|fyyfj90j0067|fyyfj90j0104|fyyfj90j0107|fyyfj90j0108|fyyfj90j0209|fyyfj90j0289|fyyfj90j0301|fyyfj90j0321|fyyfj90j0339|fyyfj90j0352|fyyfj90j0358|fyyfj90j0452|fyyfj90j0472|fyyfj90j0500|fyyfj90j0573|fyyfj91j0067|fyyfj91j0104|fyyfj91j0107|fyyfj91j0108|fyyfj91j0209|fyyfj91j0289|fyyfj91j0301|fyyfj91j0321|fyyfj91j0339|fyyfj91j0352|fyyfj91j0358|fyyfj91j0452|fyyfj91j0472|fyyfj91j0500|fyyfj91j0573|fyyfj92j0067|fyyfj92j0104|fyyfj92j0107|fyyfj92j0108|fyyfj92j0209|fyyfj92j0289|fyyfj92j0301|fyyfj92j0321|fyyfj92j0339|fyyfj92j0352|fyyfj92j0358|fyyfj92j0452|fyyfj92j0472|fyyfj92j0500|fyyfj92j0573|fyyfj93j0067|fyyfj93j0099|fyyfj93j0104|fyyfj93j0107|fyyfj93j0108|fyyfj93j0209|fyyfj93j0289|fyyfj93j0301|fyyfj93j0321|fyyfj93j0352|fyyfj93j0358|fyyfj93j0452|fyyfj93j0472|fyyfj93j0500|fyyfj93j0573|fyyfj94j0067|fyyfj94j0099|fyyfj94j0104|fyyfj94j0107|fyyfj94j0108|fyyfj94j0209|fyyfj94j0211|fyyfj94j0289|fyyfj94j0301|fyyfj94j0321|fyyfj94j0352|fyyfj94j0358|fyyfj94j0359|fyyfj94j0452|fyyfj94j0472|fyyfj94j0500|fyyfj94j0573|fyyfj95j0067|fyyfj95j0099|fyyfj95j0104|fyyfj95j0107|fyyfj95j0108|fyyfj95j0209|fyyfj95j0211|fyyfj95j0289|fyyfj95j0298|fyyfj95j0301|fyyfj95j0321|fyyfj95j0339|fyyfj95j0352|fyyfj95j0358|fyyfj95j0359|fyyfj95j0414|fyyfj95j0452|fyyfj95j0472|fyyfj95j0500|fyyfj95j0573|fyyfj96j0067|fyyfj96j0099|fyyfj96j0104|fyyfj96j0107|fyyfj96j0108|fyyfj96j0209|fyyfj96j0211|fyyfj96j0289|fyyfj96j0298|fyyfj96j0301|fyyfj96j0321|fyyfj96j0339|fyyfj96j0352|fyyfj96j0358|fyyfj96j0359|fyyfj96j0414|fyyfj96j0452|fyyfj96j0472|fyyfj96j0500|fyyfj96j0573|fyyfj97j0067|fyyfj97j0099|fyyfj97j0100|fyyfj97j0104|fyyfj97j0107|fyyfj97j0108|fyyfj97j0209|fyyfj97j0211|fyyfj97j0289|fyyfj97j0298|fyyfj97j0301|fyyfj97j0321|fyyfj97j0339|fyyfj97j0352|fyyfj97j0358|fyyfj97j0359|fyyfj97j0414|fyyfj97j0445|fyyfj97j0452|fyyfj97j0472|fyyfj97j0500|fyyfj97j0573|fyyfj98j0067|fyyfj98j0099|fyyfj98j0100|fyyfj98j0104|fyyfj98j0107|fyyfj98j0108|fyyfj98j0178|fyyfj98j0209|fyyfj98j0211|fyyfj98j0289|fyyfj98j0298|fyyfj98j0301|fyyfj98j0303|fyyfj98j0321|fyyfj98j0339|fyyfj98j0352|fyyfj98j0358|fyyfj98j0359|fyyfj98j0413|fyyfj98j0414|fyyfj98j0445|fyyfj98j0452|fyyfj98j0472|fyyfj98j0500|fyyfj98j0573|fyyfj99j0067|fyyfj99j0099|fyyfj99j0100|fyyfj99j0104|fyyfj99j0107|fyyfj99j0108|fyyfj99j0131|fyyfj99j0209|fyyfj99j0211|fyyfj99j0285|fyyfj99j0289|fyyfj99j0298|fyyfj99j0301|fyyfj99j0303|fyyfj99j0321|fyyfj99j0339|fyyfj99j0352|fyyfj99j0358|fyyfj99j0359|fyyfj99j0413|fyyfj99j0414|fyyfj99j0445|fyyfj99j0452|fyyfj99j0472|fyyfj99j0500|fyyfj99j0573|fyyfm01j0064|fyyfm01j0070|fyyfm01j0071|fyyfm01j0088|fyyfm01j0091|fyyfm01j0108|fyyfm01j0111|fyyfm01j0112|fyyfm01j0114|fyyfm01j0115|fyyfm01j0133|fyyfm01j0140|fyyfm01j0141|fyyfm01j0142|fyyfm01j0143|fyyfm01j0148|fyyfm01j0149|fyyfm01j0152|fyyfm01j0153|fyyfm01j0155|fyyfm01j0159|fyyfm01j0160|fyyfm01j0163|fyyfm01j0165|fyyfm01j0168|fyyfm01j0169|fyyfm01j0221|fyyfm01j0223|fyyfm01j0268|fyyfm01j0271|fyyfm01j0285|fyyfm01j0299|fyyfm01j0320|fyyfm01j0321|fyyfm01j0360|fyyfm01j0369|fyyfm01j0400|fyyfm01j0401|fyyfm01j0411|fyyfm01j0572|fyyfm01j0765|fyyfm02j0064|fyyfm02j0069|fyyfm02j0070|fyyfm02j0071|fyyfm02j0088|fyyfm02j0091|fyyfm02j0108|fyyfm02j0111|fyyfm02j0112|fyyfm02j0114|fyyfm02j0115|fyyfm02j0133|fyyfm02j0140|fyyfm02j0141|fyyfm02j0142|fyyfm02j0143|fyyfm02j0148|fyyfm02j0149|fyyfm02j0152|fyyfm02j0153|fyyfm02j0155|fyyfm02j0159|fyyfm02j0160|fyyfm02j0163|fyyfm02j0165|fyyfm02j0168|fyyfm02j0169|fyyfm02j0221|fyyfm02j0223|fyyfm02j0268|fyyfm02j0271|fyyfm02j0285|fyyfm02j0299|fyyfm02j0320|fyyfm02j0321|fyyfm02j0360|fyyfm02j0369|fyyfm02j0400|fyyfm02j0572|fyyfm02j0765|fyyfm03j0064|fyyfm03j0070|fyyfm03j0091|fyyfm03j0108|fyyfm03j0111|fyyfm03j0115|fyyfm03j0160|fyyfm03j0165|fyyfm03j0299|fyyfm03j0400|fyyfm03j0572|fyyfm04j0111|fyyfm51j0064|fyyfm51j0369|fyyfm52j0064|fyyfm52j0369|fyyfr88j0003|fyyfr89j0003|fyyff98j0071|fyyff98j0303|fyyff99j0029|fyyff99j0303|fyefj00j0112|fyefj00j0545|fyefj00j0546|fyefj00j0633|fyefj00j0634|fyefj00j0635|fyefj00j0636|fyefj00j0637|fyefj00j0649|fyefj00j0651|fyefj00j0652|fyefj00j0656|fyefj00j0657|fyefj00j0658|fyefj00j0659|fyefj00j0660|fyefj00j0685|fyefj00j0686|fyefj00j0688|fyefj00j0701|fyefj00j0702|fyefj00j0703|fyefj00j0715|fyefj00j0720|fyefj00j0721|fyefj00j0722|fyefj00j0724|fyefj00j0725|fyefj00j0726|fyefj00j0731|fyefj00j0751|fyefj00j0752|fyefj00j0756|fyefj00j0757|fyefj00j0758|fyefj00j0759|fyefj00j0761|fyefj00j0762|fyefj00j0763|fyefj00j0764|fyefj00j0768|fyefj00j0769|fyefj00j0785|fyefj00j0786|fyefj00j0789|fyefj00j0790|fyefj00j0793|fyefj00j0794|fyefj00j0803|fyefj00j0811|fyefj00j0821|fyefj00j0822|fyefj00j0823|fyefj00j0824|fyefj00j0825|fyefj00j0826|fyefj00j0827|fyefj00j0828|fyefj00j0829|fyefj00j0831|fyefj00j0832|fyefj00j0833|fyefj00j0838|fyefj00j0839|fyefj00j0840|fyefj00j0854|fyefj00j0855|fyefj00j0856|fyefj00j0859|fyefj00j0860|fyefj00j0861|fyefj00j0869|fyefj00j0870|fyefj00j0879|fyefj00j0887|fyefj00j0888|fyefj00j0889|fyefj00j0900|fyefj00j0901|fyefj00j0903|fyefj00j0904|fyefj00j0905|fyefj00j0959|fyefj00j0960|fyefj00j0961|fyefj00j1004|fyefj00j1005|fyefj00j1012|fyefj00j1013|fyefj00j1014|fyefj00j1015|fyefj00j1016|fyefj00j1017|fyefj00j1018|fyefj00j1019|fyefj00j1020|fyefj00j1021|fyefj00j1218|fyefj00j1219|fyefj00j1220|fyefj00j1221|fyefj00j1222|fyefj00j1811|fyefj00j1854|fyefj00j1855|fyefj00j1856|fyefj01j0707|fyefj02j0707|fyefj03j0707|fyefj66j0001|fyefj67j0001|fyefj68j0001|fyefj68j1064|fyefj69j0001|fyefj69j1064|fyefj70j0001|fyefj70j0859|fyefj70j1064|fyefj71j0001|fyefj71j1064|fyefj72j0001|fyefj72j1064|fyefj73j0001|fyefj73j1064|fyefj74j0001|fyefj74j1064|fyefj75j0001|fyefj75j1064|fyefj75j1092|fyefj76j0001|fyefj76j1064|fyefj76j1092|fyefj77j0001|fyefj77j1064|fyefj77j1092|fyefj78j0001|fyefj78j1064|fyefj78j1092|fyefj79j0001|fyefj79j1064|fyefj79j1092|fyefj80j0001|fyefj80j0859|fyefj80j1064|fyefj80j1077|fyefj80j1092|fyefj81j0001|fyefj81j1064|fyefj81j1077|fyefj81j1092|fyefj82j0001|fyefj82j1064|fyefj82j1092|fyefj83j0001|fyefj83j1064|fyefj83j1092|fyefj84j0001|fyefj84j1064|fyefj84j1092|fyefj85j0001|fyefj85j0356|fyefj85j1064|fyefj85j1092|fyefj86j0001|fyefj86j0356|fyefj86j1064|fyefj87j0001|fyefj87j0356|fyefj87j1064|fyefj88j0001|fyefj88j0356|fyefj88j1064|fyefj89j0001|fyefj89j0356|fyefj89j1064|fyefj89j1067|fyefj90j0001|fyefj90j0758|fyefj90j1021|fyefj90j1064|fyefj90j1067|fyefj91j0001|fyefj91j0758|fyefj91j0791|fyefj91j1021|fyefj91j1064|fyefj91j1067|fyefj91j1077|fyefj92j0001|fyefj92j0359|fyefj92j0678|fyefj92j0758|fyefj92j0791|fyefj92j0867|fyefj92j1021|fyefj92j1064|fyefj92j1077|fyefj93j0001|fyefj93j0359|fyefj93j0678|fyefj93j0758|fyefj93j0791|fyefj93j0867|fyefj93j1010|fyefj93j1021|fyefj93j1049|fyefj93j1064|fyefj93j1077|fyefj94j0001|fyefj94j0678|fyefj94j0758|fyefj94j0791|fyefj94j0867|fyefj94j1010|fyefj94j1021|fyefj94j1049|fyefj94j1064|fyefj94j1070|fyefj94j1077|fyefj94j1085|fyefj95j0001|fyefj95j0678|fyefj95j0758|fyefj95j0791|fyefj95j0867|fyefj95j0965|fyefj95j0966|fyefj95j1010|fyefj95j1011|fyefj95j1021|fyefj95j1055|fyefj95j1064|fyefj95j1069|fyefj95j1077|fyefj95j1085|fyefj95j1089|fyefj96j0001|fyefj96j0106|fyefj96j0671|fyefj96j0678|fyefj96j0758|fyefj96j0791|fyefj96j0814|fyefj96j0836|fyefj96j0867|fyefj96j0931|fyefj96j0965|fyefj96j0966|fyefj96j0976|fyefj96j1010|fyefj96j1021|fyefj96j1051|fyefj96j1055|fyefj96j1064|fyefj96j1068|fyefj96j1070|fyefj96j1077|fyefj96j1079|fyefj96j1081|fyefj96j1086|fyefj96j1088|fyefj96j1091|fyefj96j1093|fyefj96j1094|fyefj97j0001|fyefj97j0106|fyefj97j0584|fyefj97j0586|fyefj97j0671|fyefj97j0678|fyefj97j0758|fyefj97j0791|fyefj97j0814|fyefj97j0825|fyefj97j0836|fyefj97j0863|fyefj97j0865|fyefj97j0867|fyefj97j0914|fyefj97j0931|fyefj97j0952|fyefj97j0965|fyefj97j0966|fyefj97j0969|fyefj97j0971|fyefj97j0972|fyefj97j0976|fyefj97j0985|fyefj97j1010|fyefj97j1021|fyefj97j1051|fyefj97j1052|fyefj97j1055|fyefj97j1058|fyefj97j1059|fyefj97j1064|fyefj97j1068|fyefj97j1077|fyefj97j1079|fyefj97j1081|fyefj97j1086|fyefj97j1088|fyefj97j1095|fyefj98j0001|fyefj98j0243|fyefj98j0326|fyefj98j0329|fyefj98j0343|fyefj98j0344|fyefj98j0380|fyefj98j0472|fyefj98j0584|fyefj98j0586|fyefj98j0604|fyefj98j0671|fyefj98j0673|fyefj98j0676|fyefj98j0677|fyefj98j0678|fyefj98j0694|fyefj98j0758|fyefj98j0814|fyefj98j0825|fyefj98j0836|fyefj98j0863|fyefj98j0865|fyefj98j0867|fyefj98j0896|fyefj98j0898|fyefj98j0901|fyefj98j0906|fyefj98j0910|fyefj98j0913|fyefj98j0914|fyefj98j0922|fyefj98j0931|fyefj98j0934|fyefj98j0936|fyefj98j0951|fyefj98j0952|fyefj98j0963|fyefj98j0965|fyefj98j0966|fyefj98j0969|fyefj98j0971|fyefj98j0972|fyefj98j0974|fyefj98j0975|fyefj98j0976|fyefj98j0977|fyefj98j0978|fyefj98j0985|fyefj98j0992|fyefj98j1008|fyefj98j1009|fyefj98j1010|fyefj98j1011|fyefj98j1012|fyefj98j1019|fyefj98j1021|fyefj98j1028|fyefj98j1034|fyefj98j1039|fyefj98j1046|fyefj98j1047|fyefj98j1048|fyefj98j1054|fyefj98j1055|fyefj98j1064|fyefj98j1068|fyefj98j1077|fyefj98j1079|fyefj98j1080|fyefj98j1081|fyefj98j1082|fyefj98j1084|fyefj98j1087|fyefj98j1088|fyefj98j1090|fyefj99j0010|fyefj99j0188|fyefj99j0243|fyefj99j0268|fyefj99j0280|fyefj99j0301|fyefj99j0329|fyefj99j0343|fyefj99j0344|fyefj99j0380|fyefj99j0552|fyefj99j0573|fyefj99j0584|fyefj99j0586|fyefj99j0604|fyefj99j0671|fyefj99j0673|fyefj99j0676|fyefj99j0677|fyefj99j0678|fyefj99j0694|fyefj99j0722|fyefj99j0757|fyefj99j0758|fyefj99j0771|fyefj99j0772|fyefj99j0804|fyefj99j0806|fyefj99j0809|fyefj99j0814|fyefj99j0825|fyefj99j0836|fyefj99j0862|fyefj99j0863|fyefj99j0865|fyefj99j0866|fyefj99j0867|fyefj99j0875|fyefj99j0896|fyefj99j0898|fyefj99j0901|fyefj99j0906|fyefj99j0907|fyefj99j0908|fyefj99j0910|fyefj99j0912|fyefj99j0913|fyefj99j0914|fyefj99j0921|fyefj99j0922|fyefj99j0923|fyefj99j0931|fyefj99j0934|fyefj99j0936|fyefj99j0937|fyefj99j0949|fyefj99j0951|fyefj99j0952|fyefj99j0962|fyefj99j0963|fyefj99j0965|fyefj99j0966|fyefj99j0969|fyefj99j0971|fyefj99j0972|fyefj99j0974|fyefj99j0975|fyefj99j0976|fyefj99j0977|fyefj99j0978|fyefj99j0982|fyefj99j0985|fyefj99j0986|fyefj99j0988|fyefj99j0991|fyefj99j0992|fyefj99j0995|fyefj99j0997|fyefj99j0999|fyefj99j1003|fyefj99j1006|fyefj99j1008|fyefj99j1009|fyefj99j1010|fyefj99j1011|fyefj99j1016|fyefj99j1019|fyefj99j1020|fyefj99j1021|fyefj99j1024|fyefj99j1026|fyefj99j1028|fyefj99j1031|fyefj99j1033|fyefj99j1034|fyefj99j1036|fyefj99j1039|fyefj99j1042|fyefj99j1045|fyefj99j1046|fyefj99j1048|fyefj99j1053|fyefj99j1054|fyefj99j1055|fyefj99j1061|fyefj99j1062|fyefj99j1063|fyefj99j1064|fyefj99j1068|fyefj99j1072|fyefj99j1076|fyefj99j1077|fyefj99j1079|fyefj99j1080|fyefj99j1081|fyefj99j1083|fyefj99j1084|fyefj99j1087|fyefj99j1088|fyefm00j0113|fyefm01j0057|fyefm01j0088|fyefm01j0091|fyefm01j0101|fyefm01j0104|fyefm01j0107|fyefm01j0112|fyefm01j0379|fyefm02j0057|fyefm02j0101|fyefm02j0104|fyefm02j0107|fyefm02j0112|fyefm02j0379|fyefm98j0066|fyefm99j0066|fyefm99j0090|fyefm99j0093|fyefm99j0110|fyefm99j0165|fyefm99j0208|fyefm99j0209|fyefm99j0295|fyefm99j0401|fyefm99j0402|fyefm99j0907|fyefm99j1054|fyefn98j0015|fyefn98j0024|fyefn98j0030|fyefn99j0015|fyefn99j0024|fyefn99j0030|fyefr94j0559|fyefr95j0559|fyefr96j0559|fyefr97j0559|fyefr98j0559|fyefr99j0012|fyefr99j0559|fyefb01305|fyeff00j0170|fyeff00j0224|fyeff00j0227|fyeff00j0228|fyeff00j0229|fyeff00j0280|fyeff00j0281|fyeff00j0282|fyeff00j0283|fyeff00j0288|fyeff00j0289|fyeff00j0331|fyeff00j0332|fyeff00j0333|fyeff00j0334|fyeff00j0335|fyeff00j0336|fyeff00j0337|fyeff00j0338|fyeff00j0346|fyeff00j0347|fyeff00j0348|fyeff00j0349|fyeff00j0350|fyeff00j0351|fyeff00j0357|fyeff00j0358|fyeff00j0371|fyeff00j0372|fyeff00j0396|fyeff00j0397|fyeff00j0424|fyeff00j0425|fyeff01j0416|fyeff02j0416|fyeff78j0418|fyeff79j0418|fyeff79j1051|fyeff80j1051|fyeff81j1051|fyeff82j1051|fyeff83j1051|fyeff84j1051|fyeff85j1051|fyeff86j1051|fyeff87j1051|fyeff88j0422|fyeff89j0422|fyeff90j0422|fyeff90j0434|fyeff90j0440|fyeff91j0422|fyeff91j0434|fyeff91j0440|fyeff92j0440|fyeff93j0440|fyeff93j1045|fyeff93j1067|fyeff94j0392|fyeff94j0440|fyeff94j0443|fyeff94j1045|fyeff94j1067|fyeff95j0219|fyeff95j0392|fyeff95j0439|fyeff95j0440|fyeff95j0443|fyeff96j0053|fyeff96j0219|fyeff96j0392|fyeff96j0429|fyeff96j0434|fyeff96j0950|fyeff96j1019|fyeff96j1028|fyeff97j0053|fyeff97j0178|fyeff97j0191|fyeff97j0219|fyeff97j0221|fyeff97j0258|fyeff97j0324|fyeff97j0355|fyeff97j0370|fyeff97j0377|fyeff97j0392|fyeff97j0429|fyeff97j0434|fyeff97j0950|fyeff97j1019|fyeff98j0053|fyeff98j0065|fyeff98j0101|fyeff98j0144|fyeff98j0156|fyeff98j0178|fyeff98j0191|fyeff98j0193|fyeff98j0196|fyeff98j0197|fyeff98j0209|fyeff98j0210|fyeff98j0211|fyeff98j0214|fyeff98j0215|fyeff98j0218|fyeff98j0219|fyeff98j0221|fyeff98j0258|fyeff98j0260|fyeff98j0279|fyeff98j0284|fyeff98j0295|fyeff98j0296|fyeff98j0298|fyeff98j0324|fyeff98j0355|fyeff98j0370|fyeff98j0376|fyeff98j0379|fyeff98j0381|fyeff98j0392|fyeff98j0401|fyeff98j0404|fyeff98j0405|fyeff98j0407|fyeff98j0411|fyeff98j0418|fyeff98j0421|fyeff98j0423|fyeff98j0433|fyeff98j0436|fyeff98j0673|fyeff98j0896|fyeff98j0950|fyeff98j0985|fyeff98j1012|fyeff99j0053|fyeff99j0065|fyeff99j0152|fyeff99j0156|fyeff99j0159|fyeff99j0178|fyeff99j0191|fyeff99j0193|fyeff99j0196|fyeff99j0197|fyeff99j0209|fyeff99j0210|fyeff99j0211|fyeff99j0214|fyeff99j0215|fyeff99j0218|fyeff99j0219|fyeff99j0220|fyeff99j0221|fyeff99j0260|fyeff99j0279|fyeff99j0284|fyeff99j0291|fyeff99j0295|fyeff99j0296|fyeff99j0297|fyeff99j0298|fyeff99j0324|fyeff99j0339|fyeff99j0355|fyeff99j0370|fyeff99j0376|fyeff99j0379|fyeff99j0381|fyeff99j0392|fyeff99j0401|fyeff99j0404|fyeff99j0405|fyeff99j0407|fyeff99j0410|fyeff99j0411|fyeff99j0413|fyeff99j0414|fyeff99j0415|fyeff99j0418|fyeff99j0421|fyeff99j0423|fyeff99j0436|fyeff99j0673|fyeff99j0896|fyeff99j0950|fyeff99j0962|fyeff99j0985|fyeff99j1010|fyeff99j1012|fyeff99j1028|fyeff99j1090|fyeff99j1370|fayfm01j0148|fayfm01j0149|fayfm01j0155|fayfm02j0148|fayfm02j0149|fayfm02j0155|faefj00j0594|faefj00j0595|faefj00j0596|faefj00j0597|faefj01j0707|faefj02j0707|faefj03j0707|faefj90j1023|faefj91j1023|faefj92j1023|faefj94j1056|faefj95j1023|faefj95j1056|faefj96j1056|faefj98j1038|faefj99j1078|fdeff99j9001|fdeff99j9002|gyefj99j0005", + // 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))", + // 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 ".*". + "(?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))", + // Quest ".?". + "fo.?", + "foo.?", + "f.?o", + ".*foo.?", + ".?foo.+", + "foo.?|bar", } + values = []string{ + "foo", " foo bar", "bar", "buzz\nbar", "bar foo", "bfoo", "\n", "\nfoo", "foo\n", "hello foo world", "hello foo\n world", "", + "FOO", "Foo", "OO", "Oo", "\nfoo\n", strings.Repeat("f", 20), "prometheus", "prometheus_api_v1", "prometheus_api_v1_foo", + "10.0.1.20", "10.0.2.10", "10.0.3.30", "10.0.4.40", + "foofoo0", "foofoo", - for _, c := range cases { - m, err := NewFastRegexMatcher(c.regex) - require.NoError(t, err) - require.Equal(t, c.expected, m.MatchString(c.value)) + // Values matching / not matching the test regexps on long alternations. + "zQPbMkNO", "zQPbMkNo", "jyyfj00j0061", "jyyfj00j006", "jyyfj00j00612", "NNSPdvMi", "NNSPdvMiXXX", "NNSPdvMixxx", "nnSPdvMi", "nnSPdvMiXXX", + } +) + +func TestFastRegexMatcher_MatchString(t *testing.T) { + // Run the test both against a set of predefined values and a set of random ones. + testValues := append([]string{}, values...) + testValues = append(testValues, generateRandomValues()...) + + for _, r := range regexes { + r := r + for _, v := range testValues { + v := v + t.Run(r+` on "`+v+`"`, func(t *testing.T) { + t.Parallel() + m, err := NewFastRegexMatcher(r) + require.NoError(t, err) + re := regexp.MustCompile("^(?:" + r + ")$") + require.Equal(t, re.MatchString(v), m.MatchString(v)) + }) + } } } @@ -85,6 +136,9 @@ func TestOptimizeConcatRegex(t *testing.T) { {regex: "(?i).*(?-i:abc)def", prefix: "", suffix: "", contains: "abc"}, {regex: ".*(?msU:abc).*", prefix: "", suffix: "", contains: "abc"}, {regex: "[aA]bc.*", prefix: "", suffix: "", contains: "bc"}, + {regex: "^5..$", prefix: "5", suffix: "", contains: ""}, + {regex: "^release.*", prefix: "release", suffix: "", contains: ""}, + {regex: "^env-[0-9]+laio[1]?[^0-9].*", prefix: "env-", suffix: "", contains: "laio"}, } for _, c := range cases { @@ -98,41 +152,906 @@ func TestOptimizeConcatRegex(t *testing.T) { } } -func BenchmarkFastRegexMatcher(b *testing.B) { - var ( - x = strings.Repeat("x", 50) - y = "foo" + x - z = x + "foo" - ) - regexes := []string{ - "foo", - "^foo", - "(foo|bar)", - "foo.*", - ".*foo", - "^.*foo$", - "^.+foo$", - ".*", - ".+", - "foo.+", - ".+foo", - ".*foo.*", - "(?i:foo)", - "(prometheus|api_prom)_api_v1_.+", - "((fo(bar))|.+foo)", - } - for _, r := range regexes { - r := r - b.Run(r, func(b *testing.B) { - m, err := NewFastRegexMatcher(r) - require.NoError(b, err) - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = m.MatchString(x) - _ = m.MatchString(y) - _ = m.MatchString(z) +// Refer to https://github.com/prometheus/prometheus/issues/2651. +func TestFindSetMatches(t *testing.T) { + for _, c := range []struct { + pattern string + expMatches []string + expCaseSensitive bool + }{ + // Single value, coming from a `bar=~"foo"` selector. + {"foo", []string{"foo"}, true}, + {"^foo", []string{"foo"}, true}, + {"^foo$", []string{"foo"}, true}, + // Simple sets alternates. + {"foo|bar|zz", []string{"foo", "bar", "zz"}, true}, + // Simple sets alternate and concat (bar|baz is parsed as "ba[rz]"). + {"foo|bar|baz", []string{"foo", "bar", "baz"}, true}, + // Simple sets alternate and concat and capture + {"foo|bar|baz|(zz)", []string{"foo", "bar", "baz", "zz"}, true}, + // Simple sets alternate and concat and alternates with empty matches + // parsed as b(ar|(?:)|uzz) where b(?:) means literal b. + {"bar|b|buzz", []string{"bar", "b", "buzz"}, true}, + // Skip nested capture groups. + {"^((bar|b|buzz))$", []string{"bar", "b", "buzz"}, true}, + // Skip outer anchors (it's enforced anyway at the root). + {"^(bar|b|buzz)$", []string{"bar", "b", "buzz"}, true}, + {"^(?:prod|production)$", []string{"prod", "production"}, true}, + // Do not optimize regexp with inner anchors. + {"(bar|b|b^uz$z)", nil, false}, + // Do not optimize regexp with empty string matcher. + {"^$|Running", nil, false}, + // Simple sets containing escaped characters. + {"fo\\.o|bar\\?|\\^baz", []string{"fo.o", "bar?", "^baz"}, true}, + // using charclass + {"[abc]d", []string{"ad", "bd", "cd"}, true}, + // high low charset different => A(B[CD]|EF)|BC[XY] + {"ABC|ABD|AEF|BCX|BCY", []string{"ABC", "ABD", "AEF", "BCX", "BCY"}, true}, + // triple concat + {"api_(v1|prom)_push", []string{"api_v1_push", "api_prom_push"}, true}, + // triple concat with multiple alternates + {"(api|rpc)_(v1|prom)_push", []string{"api_v1_push", "api_prom_push", "rpc_v1_push", "rpc_prom_push"}, true}, + {"(api|rpc)_(v1|prom)_(push|query)", []string{"api_v1_push", "api_v1_query", "api_prom_push", "api_prom_query", "rpc_v1_push", "rpc_v1_query", "rpc_prom_push", "rpc_prom_query"}, true}, + // class starting with "-" + {"[-1-2][a-c]", []string{"-a", "-b", "-c", "1a", "1b", "1c", "2a", "2b", "2c"}, true}, + {"[1^3]", []string{"1", "3", "^"}, true}, + // OpPlus with concat + {"(.+)/(foo|bar)", nil, false}, + // Simple sets containing special characters without escaping. + {"fo.o|bar?|^baz", nil, false}, + // case sensitive wrapper. + {"(?i)foo", []string{"FOO"}, false}, + // case sensitive wrapper on alternate. + {"(?i)foo|bar|baz", []string{"FOO", "BAR", "BAZ", "BAr", "BAz"}, false}, + // mixed case sensitivity. + {"(api|rpc)_(v1|prom)_((?i)push|query)", nil, false}, + // mixed case sensitivity concatenation only without capture group. + {"api_v1_(?i)push", nil, false}, + // mixed case sensitivity alternation only without capture group. + {"api|(?i)rpc", nil, false}, + // case sensitive after unsetting insensitivity. + {"rpc|(?i)(?-i)api", []string{"rpc", "api"}, true}, + // case sensitive after unsetting insensitivity in all alternation options. + {"(?i)((?-i)api|(?-i)rpc)", []string{"api", "rpc"}, true}, + // mixed case sensitivity after unsetting insensitivity. + {"(?i)rpc|(?-i)api", nil, false}, + // too high charset combination + {"(api|rpc)_[^0-9]", nil, false}, + // too many combinations + {"[a-z][a-z]", nil, false}, + } { + c := c + t.Run(c.pattern, func(t *testing.T) { + t.Parallel() + parsed, err := syntax.Parse(c.pattern, syntax.Perl) + require.NoError(t, err) + matches, actualCaseSensitive := findSetMatches(parsed) + require.Equal(t, c.expMatches, matches) + require.Equal(t, c.expCaseSensitive, actualCaseSensitive) + + if c.expCaseSensitive { + // When the regexp is case sensitive, we want to ensure that the + // set matches are maintained in the final matcher. + r, err := NewFastRegexMatcher(c.pattern) + require.NoError(t, err) + require.Equal(t, c.expMatches, r.SetMatches()) } }) - + } +} + +func TestFastRegexMatcher_SetMatches_ShouldReturnACopy(t *testing.T) { + m, err := NewFastRegexMatcher("a|b") + require.NoError(t, err) + require.Equal(t, []string{"a", "b"}, m.SetMatches()) + + // Manipulate the returned slice. + matches := m.SetMatches() + matches[0] = "xxx" + matches[1] = "yyy" + + // Ensure that if we call SetMatches() again we get the original one. + require.Equal(t, []string{"a", "b"}, m.SetMatches()) +} + +func BenchmarkFastRegexMatcher(b *testing.B) { + texts := generateRandomValues() + + for _, r := range regexes { + b.Run(getTestNameFromRegexp(r), func(b *testing.B) { + m, err := NewFastRegexMatcher(r) + require.NoError(b, err) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, text := range texts { + _ = m.MatchString(text) + } + } + }) + } +} + +func TestStringMatcherFromRegexp(t *testing.T) { + for _, c := range []struct { + pattern string + exp StringMatcher + }{ + {".*", anyStringWithoutNewlineMatcher{}}, + {".*?", anyStringWithoutNewlineMatcher{}}, + {"(?s:.*)", trueMatcher{}}, + {"(.*)", anyStringWithoutNewlineMatcher{}}, + {"^.*$", anyStringWithoutNewlineMatcher{}}, + {".+", &anyNonEmptyStringMatcher{matchNL: false}}, + {"(?s:.+)", &anyNonEmptyStringMatcher{matchNL: true}}, + {"^.+$", &anyNonEmptyStringMatcher{matchNL: false}}, + {"(.+)", &anyNonEmptyStringMatcher{matchNL: false}}, + {"", emptyStringMatcher{}}, + {"^$", emptyStringMatcher{}}, + {"^foo$", &equalStringMatcher{s: "foo", caseSensitive: true}}, + {"^(?i:foo)$", &equalStringMatcher{s: "FOO", caseSensitive: false}}, + {"^((?i:foo)|(bar))$", orStringMatcher([]StringMatcher{&equalStringMatcher{s: "FOO", caseSensitive: false}, &equalStringMatcher{s: "bar", caseSensitive: true}})}, + {`(?i:((foo|bar)))`, orStringMatcher([]StringMatcher{&equalStringMatcher{s: "FOO", caseSensitive: false}, &equalStringMatcher{s: "BAR", caseSensitive: false}})}, + {`(?i:((foo1|foo2|bar)))`, orStringMatcher([]StringMatcher{orStringMatcher([]StringMatcher{&equalStringMatcher{s: "FOO1", caseSensitive: false}, &equalStringMatcher{s: "FOO2", caseSensitive: false}}), &equalStringMatcher{s: "BAR", caseSensitive: false}})}, + {"^((?i:foo|oo)|(bar))$", orStringMatcher([]StringMatcher{&equalStringMatcher{s: "FOO", caseSensitive: false}, &equalStringMatcher{s: "OO", caseSensitive: false}, &equalStringMatcher{s: "bar", caseSensitive: true}})}, + {"(?i:(foo1|foo2|bar))", orStringMatcher([]StringMatcher{orStringMatcher([]StringMatcher{&equalStringMatcher{s: "FOO1", caseSensitive: false}, &equalStringMatcher{s: "FOO2", caseSensitive: false}}), &equalStringMatcher{s: "BAR", caseSensitive: false}})}, + {".*foo.*", &containsStringMatcher{substrings: []string{"foo"}, left: anyStringWithoutNewlineMatcher{}, right: anyStringWithoutNewlineMatcher{}}}, + {"(.*)foo.*", &containsStringMatcher{substrings: []string{"foo"}, left: anyStringWithoutNewlineMatcher{}, right: anyStringWithoutNewlineMatcher{}}}, + {"(.*)foo(.*)", &containsStringMatcher{substrings: []string{"foo"}, left: anyStringWithoutNewlineMatcher{}, right: anyStringWithoutNewlineMatcher{}}}, + {"(.+)foo(.*)", &containsStringMatcher{substrings: []string{"foo"}, left: &anyNonEmptyStringMatcher{matchNL: false}, right: anyStringWithoutNewlineMatcher{}}}, + {"^.+foo.+", &containsStringMatcher{substrings: []string{"foo"}, left: &anyNonEmptyStringMatcher{matchNL: false}, right: &anyNonEmptyStringMatcher{matchNL: false}}}, + {"^(.*)(foo)(.*)$", &containsStringMatcher{substrings: []string{"foo"}, left: anyStringWithoutNewlineMatcher{}, right: anyStringWithoutNewlineMatcher{}}}, + {"^(.*)(foo|foobar)(.*)$", &containsStringMatcher{substrings: []string{"foo", "foobar"}, left: anyStringWithoutNewlineMatcher{}, right: anyStringWithoutNewlineMatcher{}}}, + {"^(.*)(foo|foobar)(.+)$", &containsStringMatcher{substrings: []string{"foo", "foobar"}, left: anyStringWithoutNewlineMatcher{}, right: &anyNonEmptyStringMatcher{matchNL: false}}}, + {"^(.*)(bar|b|buzz)(.+)$", &containsStringMatcher{substrings: []string{"bar", "b", "buzz"}, left: anyStringWithoutNewlineMatcher{}, right: &anyNonEmptyStringMatcher{matchNL: false}}}, + {"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{}}}, + {"(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}}})}, + {"(.+)/(gateway|cortex-gw|cortex-gw-internal)", &containsStringMatcher{substrings: []string{"/gateway", "/cortex-gw", "/cortex-gw-internal"}, left: &anyNonEmptyStringMatcher{matchNL: false}, right: nil}}, + // we don't support case insensitive matching for contains. + // This is because there's no strings.IndexOfFold function. + // We can revisit later if this is really popular by using strings.ToUpper. + {"^(.*)((?i)foo|foobar)(.*)$", nil}, + {"(api|rpc)_(v1|prom)_((?i)push|query)", nil}, + {"[a-z][a-z]", nil}, + {"[1^3]", nil}, + {".*foo.*bar.*", nil}, + {`\d*`, nil}, + {".", nil}, + {"/|/bar.*", &literalPrefixStringMatcher{prefix: "/", prefixCaseSensitive: true, right: orStringMatcher{emptyStringMatcher{}, &literalPrefixStringMatcher{prefix: "bar", prefixCaseSensitive: true, 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{}}}}}, + // 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{}}}}}, + // Concatenated variable length selectors are not supported. + {"foo.*.*", nil}, + {"foo.+.+", nil}, + {".*.*foo", nil}, + {".+.+foo", nil}, + {"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}}}, + {"f.?o", nil}, + } { + c := c + t.Run(c.pattern, func(t *testing.T) { + t.Parallel() + parsed, err := syntax.Parse(c.pattern, syntax.Perl) + require.NoError(t, err) + matches := stringMatcherFromRegexp(parsed) + require.Equal(t, c.exp, matches) + }) + } +} + +func TestStringMatcherFromRegexp_LiteralPrefix(t *testing.T) { + for _, c := range []struct { + pattern string + expectedLiteralPrefixMatchers int + expectedMatches []string + expectedNotMatches []string + }{ + // Case sensitive. + { + pattern: "(xyz-016a-ixb-dp.*|xyz-016a-ixb-op.*)", + expectedLiteralPrefixMatchers: 3, + expectedMatches: []string{"xyz-016a-ixb-dp", "xyz-016a-ixb-dpXXX", "xyz-016a-ixb-op", "xyz-016a-ixb-opXXX"}, + expectedNotMatches: []string{"XYZ-016a-ixb-dp", "xyz-016a-ixb-d", "XYZ-016a-ixb-op", "xyz-016a-ixb-o", "xyz", "dp", "xyz-016a-ixb-dp\n"}, + }, + + // Case insensitive. + { + pattern: "(?i)(xyz-016a-ixb-dp.*|xyz-016a-ixb-op.*)", + expectedLiteralPrefixMatchers: 3, + expectedMatches: []string{"xyz-016a-ixb-dp", "XYZ-016a-ixb-dpXXX", "xyz-016a-ixb-op", "XYZ-016a-ixb-opXXX"}, + expectedNotMatches: []string{"xyz-016a-ixb-d", "xyz", "dp", "xyz-016a-ixb-dp\n"}, + }, + + // Nested literal prefixes, case sensitive. + { + pattern: "(xyz-(aaa-(111.*)|bbb-(222.*)))|(xyz-(aaa-(333.*)|bbb-(444.*)))", + expectedLiteralPrefixMatchers: 10, + expectedMatches: []string{"xyz-aaa-111", "xyz-aaa-111XXX", "xyz-aaa-333", "xyz-aaa-333XXX", "xyz-bbb-222", "xyz-bbb-222XXX", "xyz-bbb-444", "xyz-bbb-444XXX"}, + expectedNotMatches: []string{"XYZ-aaa-111", "xyz-aaa-11", "xyz-aaa-222", "xyz-bbb-111"}, + }, + + // Nested literal prefixes, case insensitive. + { + pattern: "(?i)(xyz-(aaa-(111.*)|bbb-(222.*)))|(xyz-(aaa-(333.*)|bbb-(444.*)))", + expectedLiteralPrefixMatchers: 10, + expectedMatches: []string{"xyz-aaa-111", "XYZ-aaa-111XXX", "xyz-aaa-333", "xyz-AAA-333XXX", "xyz-bbb-222", "xyz-BBB-222XXX", "XYZ-bbb-444", "xyz-bbb-444XXX"}, + expectedNotMatches: []string{"xyz-aaa-11", "xyz-aaa-222", "xyz-bbb-111"}, + }, + + // Mixed case sensitivity. + { + pattern: "(xyz-((?i)(aaa.*|bbb.*)))", + expectedLiteralPrefixMatchers: 3, + expectedMatches: []string{"xyz-aaa", "xyz-AAA", "xyz-aaaXXX", "xyz-AAAXXX", "xyz-bbb", "xyz-BBBXXX"}, + expectedNotMatches: []string{"XYZ-aaa", "xyz-aa", "yz-aaa", "aaa"}, + }, + } { + t.Run(c.pattern, func(t *testing.T) { + parsed, err := syntax.Parse(c.pattern, syntax.Perl) + require.NoError(t, err) + + matcher := stringMatcherFromRegexp(parsed) + require.NotNil(t, matcher) + + re := regexp.MustCompile("^" + c.pattern + "$") + + // Pre-condition check: ensure it contains literalPrefixStringMatcher. + numPrefixMatchers := 0 + visitStringMatcher(matcher, func(matcher StringMatcher) { + if _, ok := matcher.(*literalPrefixStringMatcher); ok { + numPrefixMatchers++ + } + }) + + require.Equal(t, c.expectedLiteralPrefixMatchers, numPrefixMatchers) + + for _, value := range c.expectedMatches { + require.Truef(t, matcher.Matches(value), "Value: %s", value) + + // Ensure the golang regexp engine would return the same. + require.Truef(t, re.MatchString(value), "Value: %s", value) + } + + for _, value := range c.expectedNotMatches { + require.Falsef(t, matcher.Matches(value), "Value: %s", value) + + // Ensure the golang regexp engine would return the same. + require.Falsef(t, re.MatchString(value), "Value: %s", value) + } + }) + } +} + +func TestStringMatcherFromRegexp_LiteralSuffix(t *testing.T) { + for _, c := range []struct { + pattern string + expectedLiteralSuffixMatchers int + expectedMatches []string + expectedNotMatches []string + }{ + // Case sensitive. + { + pattern: "(.*xyz-016a-ixb-dp|.*xyz-016a-ixb-op)", + expectedLiteralSuffixMatchers: 2, + expectedMatches: []string{"xyz-016a-ixb-dp", "XXXxyz-016a-ixb-dp", "xyz-016a-ixb-op", "XXXxyz-016a-ixb-op"}, + expectedNotMatches: []string{"XYZ-016a-ixb-dp", "yz-016a-ixb-dp", "XYZ-016a-ixb-op", "xyz-016a-ixb-o", "xyz", "dp", "\nxyz-016a-ixb-dp"}, + }, + + // Case insensitive. + { + pattern: "(?i)(.*xyz-016a-ixb-dp|.*xyz-016a-ixb-op)", + expectedLiteralSuffixMatchers: 2, + expectedMatches: []string{"xyz-016a-ixb-dp", "XYZ-016a-ixb-dp", "XXXxyz-016a-ixb-dp", "XyZ-016a-ixb-op", "XXXxyz-016a-ixb-op"}, + expectedNotMatches: []string{"yz-016a-ixb-dp", "xyz-016a-ixb-o", "xyz", "dp", "\nxyz-016a-ixb-dp"}, + }, + + // Nested literal suffixes, case sensitive. + { + pattern: "(.*aaa|.*bbb(.*ccc|.*ddd))", + expectedLiteralSuffixMatchers: 3, + expectedMatches: []string{"aaa", "XXXaaa", "bbbccc", "XXXbbbccc", "XXXbbbXXXccc", "bbbddd", "bbbddd", "XXXbbbddd", "XXXbbbXXXddd", "bbbXXXccc", "aaabbbccc", "aaabbbddd"}, + expectedNotMatches: []string{"AAA", "aa", "Xaa", "BBBCCC", "bb", "Xbb", "bbccc", "bbbcc", "bbbdd"}, + }, + + // Mixed case sensitivity. + { + pattern: "(.*aaa|.*bbb((?i)(.*ccc|.*ddd)))", + expectedLiteralSuffixMatchers: 3, + expectedMatches: []string{"aaa", "XXXaaa", "bbbccc", "bbbCCC", "bbbXXXCCC", "bbbddd", "bbbDDD", "bbbXXXddd", "bbbXXXDDD"}, + expectedNotMatches: []string{"AAA", "XXXAAA", "BBBccc", "BBBCCC", "aaaBBB"}, + }, + } { + t.Run(c.pattern, func(t *testing.T) { + parsed, err := syntax.Parse(c.pattern, syntax.Perl) + require.NoError(t, err) + + matcher := stringMatcherFromRegexp(parsed) + require.NotNil(t, matcher) + + re := regexp.MustCompile("^" + c.pattern + "$") + + // Pre-condition check: ensure it contains literalSuffixStringMatcher. + numSuffixMatchers := 0 + visitStringMatcher(matcher, func(matcher StringMatcher) { + if _, ok := matcher.(*literalSuffixStringMatcher); ok { + numSuffixMatchers++ + } + }) + + require.Equal(t, c.expectedLiteralSuffixMatchers, numSuffixMatchers) + + for _, value := range c.expectedMatches { + require.Truef(t, matcher.Matches(value), "Value: %s", value) + + // Ensure the golang regexp engine would return the same. + require.Truef(t, re.MatchString(value), "Value: %s", value) + } + + for _, value := range c.expectedNotMatches { + require.Falsef(t, matcher.Matches(value), "Value: %s", value) + + // Ensure the golang regexp engine would return the same. + require.Falsef(t, re.MatchString(value), "Value: %s", value) + } + }) + } +} + +func TestStringMatcherFromRegexp_Quest(t *testing.T) { + for _, c := range []struct { + pattern string + expectedZeroOrOneMatchers int + expectedMatches []string + expectedNotMatches []string + }{ + // Not match newline. + { + pattern: "test.?", + expectedZeroOrOneMatchers: 1, + expectedMatches: []string{"test", "test!"}, + expectedNotMatches: []string{"test\n", "tes", "test!!"}, + }, + { + pattern: ".?test", + expectedZeroOrOneMatchers: 1, + expectedMatches: []string{"test", "!test"}, + expectedNotMatches: []string{"\ntest", "tes", "test!"}, + }, + { + pattern: "(aaa.?|bbb.?)", + expectedZeroOrOneMatchers: 2, + expectedMatches: []string{"aaa", "aaaX", "bbb", "bbbX"}, + expectedNotMatches: []string{"aa", "aaaXX", "aaa\n", "bb", "bbbXX", "bbb\n"}, + }, + { + pattern: ".*aaa.?", + expectedZeroOrOneMatchers: 1, + expectedMatches: []string{"aaa", "Xaaa", "aaaX", "XXXaaa", "XXXaaaX"}, + expectedNotMatches: []string{"aa", "aaaXX", "XXXaaaXXX", "XXXaaa\n"}, + }, + + // Match newline. + { + pattern: "(?s)test.?", + expectedZeroOrOneMatchers: 1, + expectedMatches: []string{"test", "test!", "test\n"}, + expectedNotMatches: []string{"tes", "test!!", "test\n\n"}, + }, + + // Mixed flags (a part matches newline another doesn't). + { + pattern: "(aaa.?|((?s).?bbb.+))", + expectedZeroOrOneMatchers: 2, + expectedMatches: []string{"aaa", "aaaX", "bbbX", "XbbbX", "bbbXXX", "\nbbbX"}, + expectedNotMatches: []string{"aa", "aaa\n", "Xbbb", "\nbbb"}, + }, + } { + t.Run(c.pattern, func(t *testing.T) { + parsed, err := syntax.Parse(c.pattern, syntax.Perl) + require.NoError(t, err) + + matcher := stringMatcherFromRegexp(parsed) + require.NotNil(t, matcher) + + re := regexp.MustCompile("^" + c.pattern + "$") + + // Pre-condition check: ensure it contains zeroOrOneCharacterStringMatcher. + numZeroOrOneMatchers := 0 + visitStringMatcher(matcher, func(matcher StringMatcher) { + if _, ok := matcher.(*zeroOrOneCharacterStringMatcher); ok { + numZeroOrOneMatchers++ + } + }) + + require.Equal(t, c.expectedZeroOrOneMatchers, numZeroOrOneMatchers) + + for _, value := range c.expectedMatches { + require.Truef(t, matcher.Matches(value), "Value: %s", value) + + // Ensure the golang regexp engine would return the same. + require.Truef(t, re.MatchString(value), "Value: %s", value) + } + + for _, value := range c.expectedNotMatches { + require.Falsef(t, matcher.Matches(value), "Value: %s", value) + + // Ensure the golang regexp engine would return the same. + require.Falsef(t, re.MatchString(value), "Value: %s", value) + } + }) + } +} + +func randString(randGenerator *rand.Rand, length int) string { + b := make([]rune, length) + for i := range b { + b[i] = asciiRunes[randGenerator.Intn(len(asciiRunes))] + } + return string(b) +} + +func randStrings(randGenerator *rand.Rand, many, length int) []string { + out := make([]string, 0, many) + for i := 0; i < many; i++ { + out = append(out, randString(randGenerator, length)) + } + return out +} + +func TestOptimizeEqualStringMatchers(t *testing.T) { + tests := map[string]struct { + input StringMatcher + expectedValues []string + expectedCaseSensitive bool + }{ + "should skip optimization on orStringMatcher with containsStringMatcher": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: true}, + &containsStringMatcher{substrings: []string{"a", "b", "c"}}, + }, + expectedValues: nil, + }, + "should run optimization on orStringMatcher with equalStringMatcher and same case sensitivity": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: true}, + &equalStringMatcher{s: "bar", caseSensitive: true}, + &equalStringMatcher{s: "baz", caseSensitive: true}, + }, + expectedValues: []string{"FOO", "bar", "baz"}, + expectedCaseSensitive: true, + }, + "should skip optimization on orStringMatcher with equalStringMatcher but different case sensitivity": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: true}, + &equalStringMatcher{s: "bar", caseSensitive: false}, + &equalStringMatcher{s: "baz", caseSensitive: true}, + }, + expectedValues: nil, + }, + "should run optimization on orStringMatcher with nested orStringMatcher and equalStringMatcher, and same case sensitivity": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: true}, + orStringMatcher{ + &equalStringMatcher{s: "bar", caseSensitive: true}, + &equalStringMatcher{s: "xxx", caseSensitive: true}, + }, + &equalStringMatcher{s: "baz", caseSensitive: true}, + }, + expectedValues: []string{"FOO", "bar", "xxx", "baz"}, + expectedCaseSensitive: true, + }, + "should skip optimization on orStringMatcher with nested orStringMatcher and equalStringMatcher, but different case sensitivity": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: true}, + orStringMatcher{ + // Case sensitivity is different within items at the same level. + &equalStringMatcher{s: "bar", caseSensitive: true}, + &equalStringMatcher{s: "xxx", caseSensitive: false}, + }, + &equalStringMatcher{s: "baz", caseSensitive: true}, + }, + expectedValues: nil, + }, + "should skip optimization on orStringMatcher with nested orStringMatcher and equalStringMatcher, but different case sensitivity in the nested one": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: true}, + // Case sensitivity is different between the parent and child. + orStringMatcher{ + &equalStringMatcher{s: "bar", caseSensitive: false}, + &equalStringMatcher{s: "xxx", caseSensitive: false}, + }, + &equalStringMatcher{s: "baz", caseSensitive: true}, + }, + expectedValues: nil, + }, + "should return unchanged values on few case insensitive matchers": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: false}, + orStringMatcher{ + &equalStringMatcher{s: "bAr", caseSensitive: false}, + }, + &equalStringMatcher{s: "baZ", caseSensitive: false}, + }, + expectedValues: []string{"FOO", "bAr", "baZ"}, + expectedCaseSensitive: false, + }, + } + + for testName, testData := range tests { + t.Run(testName, func(t *testing.T) { + actualMatcher := optimizeEqualStringMatchers(testData.input, 0) + + if testData.expectedValues == nil { + require.IsType(t, testData.input, actualMatcher) + } else { + require.IsType(t, &equalMultiStringSliceMatcher{}, actualMatcher) + require.Equal(t, testData.expectedValues, actualMatcher.(*equalMultiStringSliceMatcher).values) + require.Equal(t, testData.expectedCaseSensitive, actualMatcher.(*equalMultiStringSliceMatcher).caseSensitive) + } + }) + } +} + +func TestNewEqualMultiStringMatcher(t *testing.T) { + tests := map[string]struct { + values []string + caseSensitive bool + expectedValuesMap map[string]struct{} + expectedValuesList []string + }{ + "few case sensitive values": { + values: []string{"a", "B"}, + caseSensitive: true, + expectedValuesList: []string{"a", "B"}, + }, + "few case insensitive values": { + values: []string{"a", "B"}, + caseSensitive: false, + expectedValuesList: []string{"a", "B"}, + }, + "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": {}}, + }, + "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": {}}, + }, + } + + for testName, testData := range tests { + t.Run(testName, func(t *testing.T) { + matcher := newEqualMultiStringMatcher(testData.caseSensitive, len(testData.values)) + for _, v := range testData.values { + matcher.add(v) + } + if testData.expectedValuesMap != nil { + require.IsType(t, &equalMultiStringMapMatcher{}, matcher) + require.Equal(t, testData.expectedValuesMap, matcher.(*equalMultiStringMapMatcher).values) + require.Equal(t, testData.caseSensitive, matcher.(*equalMultiStringMapMatcher).caseSensitive) + } + if testData.expectedValuesList != nil { + require.IsType(t, &equalMultiStringSliceMatcher{}, matcher) + require.Equal(t, testData.expectedValuesList, matcher.(*equalMultiStringSliceMatcher).values) + require.Equal(t, testData.caseSensitive, matcher.(*equalMultiStringSliceMatcher).caseSensitive) + } + }) + } +} + +func TestEqualMultiStringMatcher_Matches(t *testing.T) { + tests := map[string]struct { + values []string + caseSensitive bool + expectedMatches []string + expectedNotMatches []string + }{ + "few case sensitive values": { + values: []string{"a", "B"}, + caseSensitive: true, + expectedMatches: []string{"a", "B"}, + expectedNotMatches: []string{"A", "b"}, + }, + "few case insensitive values": { + values: []string{"a", "B"}, + caseSensitive: false, + expectedMatches: []string{"a", "A", "b", "B"}, + expectedNotMatches: []string{"c", "C"}, + }, + "many case sensitive values": { + values: []string{"a", "B", "c", "D", "e", "F", "g", "H", "i", "L", "m", "N", "o", "P", "q", "r"}, + caseSensitive: true, + expectedMatches: []string{"a", "B"}, + expectedNotMatches: []string{"A", "b"}, + }, + "many case insensitive values": { + values: []string{"a", "B", "c", "D", "e", "F", "g", "H", "i", "L", "m", "N", "o", "P", "q", "r"}, + caseSensitive: false, + expectedMatches: []string{"a", "A", "b", "B"}, + expectedNotMatches: []string{"x", "X"}, + }, + } + + for testName, testData := range tests { + t.Run(testName, func(t *testing.T) { + matcher := newEqualMultiStringMatcher(testData.caseSensitive, len(testData.values)) + for _, v := range testData.values { + matcher.add(v) + } + + for _, v := range testData.expectedMatches { + require.True(t, matcher.Matches(v), "value: %s", v) + } + for _, v := range testData.expectedNotMatches { + require.False(t, matcher.Matches(v), "value: %s", v) + } + }) + } +} + +func TestFindEqualStringMatchers(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 { + matches = append(matches, match{matcher.s, matcher.caseSensitive}) + return true + }) + return + } + + t.Run("empty matcher", func(t *testing.T) { + actualMatches, actualOk := findEqualStringMatchersAndCollectMatches(emptyStringMatcher{}) + require.False(t, actualOk) + require.Empty(t, actualMatches) + }) + + t.Run("concat of literal matchers (case sensitive)", func(t *testing.T) { + actualMatches, actualOk := findEqualStringMatchersAndCollectMatches( + orStringMatcher{ + &equalStringMatcher{s: "test-1", caseSensitive: true}, + &equalStringMatcher{s: "test-2", caseSensitive: true}, + }, + ) + + require.True(t, actualOk) + require.Equal(t, []match{{"test-1", true}, {"test-2", true}}, actualMatches) + }) + + t.Run("concat of literal matchers (case insensitive)", func(t *testing.T) { + actualMatches, actualOk := findEqualStringMatchersAndCollectMatches( + orStringMatcher{ + &equalStringMatcher{s: "test-1", caseSensitive: false}, + &equalStringMatcher{s: "test-2", caseSensitive: false}, + }, + ) + + require.True(t, actualOk) + require.Equal(t, []match{{"test-1", false}, {"test-2", false}}, actualMatches) + }) + + t.Run("concat of literal matchers (mixed case)", func(t *testing.T) { + actualMatches, actualOk := findEqualStringMatchersAndCollectMatches( + orStringMatcher{ + &equalStringMatcher{s: "test-1", caseSensitive: false}, + &equalStringMatcher{s: "test-2", caseSensitive: true}, + }, + ) + + require.True(t, actualOk) + require.Equal(t, []match{{"test-1", false}, {"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) { + randGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) + + // Generate variable lengths random texts to match against. + texts := append([]string{}, randStrings(randGenerator, 10, 10)...) + texts = append(texts, randStrings(randGenerator, 5, 30)...) + texts = append(texts, randStrings(randGenerator, 1, 100)...) + + 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) + } + } + }) + + b.Run("with optimizeEqualStringMatchers()", func(b *testing.B) { + for n := 0; n < b.N; n++ { + for _, t := range texts { + optimized.Matches(t) + } + } + }) + }) + } + } +} + +func TestZeroOrOneCharacterStringMatcher(t *testing.T) { + matcher := &zeroOrOneCharacterStringMatcher{matchNL: true} + require.True(t, matcher.Matches("")) + require.True(t, matcher.Matches("x")) + require.True(t, matcher.Matches("\n")) + require.False(t, matcher.Matches("xx")) + require.False(t, matcher.Matches("\n\n")) + + matcher = &zeroOrOneCharacterStringMatcher{matchNL: false} + require.True(t, matcher.Matches("")) + require.True(t, matcher.Matches("x")) + require.False(t, matcher.Matches("\n")) + require.False(t, matcher.Matches("xx")) + require.False(t, matcher.Matches("\n\n")) +} + +func TestLiteralPrefixStringMatcher(t *testing.T) { + m := &literalPrefixStringMatcher{prefix: "mar", prefixCaseSensitive: true, 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}} + require.True(t, m.Matches("marco")) + require.True(t, m.Matches("marCO")) + require.False(t, m.Matches("MARco")) + require.False(t, m.Matches("mar")) + require.False(t, m.Matches("marcopracucci")) +} + +func TestLiteralSuffixStringMatcher(t *testing.T) { + m := &literalSuffixStringMatcher{left: &emptyStringMatcher{}, suffix: "co", suffixCaseSensitive: true} + require.True(t, m.Matches("co")) + require.False(t, m.Matches("marco")) + require.False(t, m.Matches("coo")) + require.False(t, m.Matches("Co")) + + m = &literalSuffixStringMatcher{left: &emptyStringMatcher{}, suffix: "co", suffixCaseSensitive: false} + require.True(t, m.Matches("co")) + require.False(t, m.Matches("marco")) + require.False(t, m.Matches("coo")) + require.True(t, m.Matches("Co")) + + m = &literalSuffixStringMatcher{left: &equalStringMatcher{s: "mar", caseSensitive: false}, suffix: "co", suffixCaseSensitive: true} + require.True(t, m.Matches("marco")) + require.True(t, m.Matches("MARco")) + require.False(t, m.Matches("marCO")) + require.False(t, m.Matches("mar")) + require.False(t, m.Matches("marcopracucci")) + + m = &literalSuffixStringMatcher{left: &equalStringMatcher{s: "mar", caseSensitive: false}, suffix: "co", suffixCaseSensitive: false} + require.True(t, m.Matches("marco")) + require.True(t, m.Matches("MARco")) + require.True(t, m.Matches("marCO")) + require.False(t, m.Matches("mar")) + require.False(t, m.Matches("marcopracucci")) +} + +func TestHasPrefixCaseInsensitive(t *testing.T) { + require.True(t, hasPrefixCaseInsensitive("marco", "mar")) + require.True(t, hasPrefixCaseInsensitive("mArco", "mar")) + require.True(t, hasPrefixCaseInsensitive("marco", "MaR")) + require.True(t, hasPrefixCaseInsensitive("marco", "marco")) + require.True(t, hasPrefixCaseInsensitive("mArco", "marco")) + + require.False(t, hasPrefixCaseInsensitive("marco", "a")) + require.False(t, hasPrefixCaseInsensitive("marco", "abcdefghi")) +} + +func TestHasSuffixCaseInsensitive(t *testing.T) { + require.True(t, hasSuffixCaseInsensitive("marco", "rco")) + require.True(t, hasSuffixCaseInsensitive("marco", "RcO")) + require.True(t, hasSuffixCaseInsensitive("marco", "marco")) + require.False(t, hasSuffixCaseInsensitive("marco", "a")) + require.False(t, hasSuffixCaseInsensitive("marco", "abcdefghi")) +} + +func getTestNameFromRegexp(re string) string { + if len(re) > 32 { + return re[:32] + } + return re +} + +func generateRandomValues() []string { + // Init the random seed with a constant, so that it doesn't change between runs. + randGenerator := rand.New(rand.NewSource(1)) + + // Generate variable lengths random texts to match against. + texts := append([]string{}, randStrings(randGenerator, 10, 10)...) + texts = append(texts, randStrings(randGenerator, 5, 30)...) + texts = append(texts, randStrings(randGenerator, 1, 100)...) + texts = append(texts, "foo"+randString(randGenerator, 50)) + texts = append(texts, randString(randGenerator, 50)+"foo") + + return texts +} + +func visitStringMatcher(matcher StringMatcher, callback func(matcher StringMatcher)) { + callback(matcher) + + switch casted := matcher.(type) { + case *containsStringMatcher: + if casted.left != nil { + visitStringMatcher(casted.left, callback) + } + if casted.right != nil { + visitStringMatcher(casted.right, callback) + } + + case *literalPrefixStringMatcher: + visitStringMatcher(casted.right, callback) + + case *literalSuffixStringMatcher: + visitStringMatcher(casted.left, callback) + + case orStringMatcher: + for _, entry := range casted { + visitStringMatcher(entry, callback) + } + + // No nested matchers for the folling ones. + case emptyStringMatcher: + case *equalStringMatcher: + case *equalMultiStringSliceMatcher: + case *equalMultiStringMapMatcher: + case anyStringWithoutNewlineMatcher: + case *anyNonEmptyStringMatcher: + case trueMatcher: } } From f639d7794c456c4ec1b563d5bf1576bc2f6d832b Mon Sep 17 00:00:00 2001 From: Marco Pracucci Date: Thu, 25 Jan 2024 14:57:43 +0100 Subject: [PATCH 002/127] Fix TestParseExpressions Signed-off-by: Marco Pracucci --- promql/parser/parse_test.go | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/promql/parser/parse_test.go b/promql/parser/parse_test.go index 6c26445e38..7150891161 100644 --- a/promql/parser/parse_test.go +++ b/promql/parser/parse_test.go @@ -3568,7 +3568,31 @@ func TestParseExpressions(t *testing.T) { if !test.fail { require.NoError(t, err) - require.Equal(t, test.expected, expr, "error on input '%s'", test.input) + expected := test.expected + + // The FastRegexMatcher is not comparable with a deep equal, so only compare its String() version. + if actualVector, ok := expr.(*VectorSelector); ok { + require.IsType(t, &VectorSelector{}, test.expected, "error on input '%s'", test.input) + expectedVector := test.expected.(*VectorSelector) + + require.Len(t, actualVector.LabelMatchers, len(expectedVector.LabelMatchers), "error on input '%s'", test.input) + + for i := 0; i < len(actualVector.LabelMatchers); i++ { + expectedMatcher := expectedVector.LabelMatchers[i].String() + actualMatcher := actualVector.LabelMatchers[i].String() + + require.Equal(t, expectedMatcher, actualMatcher, "unexpected label matcher '%s' on input '%s'", actualMatcher, test.input) + } + + // Make a shallow copy of the expected expr (because the test cases are defined in a global variable) + // and then reset the LabelMatcher to not compared them with the following deep equal. + expectedCopy := *expectedVector + expectedCopy.LabelMatchers = nil + expected = &expectedCopy + actualVector.LabelMatchers = nil + } + + require.Equal(t, expected, expr, "error on input '%s'", test.input) } else { require.Error(t, err) require.Contains(t, err.Error(), test.errMsg, "unexpected error on input '%s', expected '%s', got '%s'", test.input, test.errMsg, err.Error()) From a1a45990a2eae1788d311f24166b2c780ee48b80 Mon Sep 17 00:00:00 2001 From: Marco Pracucci Date: Thu, 25 Jan 2024 14:59:39 +0100 Subject: [PATCH 003/127] Fix TestPostingsForMatcher Signed-off-by: Marco Pracucci --- tsdb/querier_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsdb/querier_test.go b/tsdb/querier_test.go index fcedc54621..ce59c0ee77 100644 --- a/tsdb/querier_test.go +++ b/tsdb/querier_test.go @@ -3304,7 +3304,7 @@ func TestPostingsForMatcher(t *testing.T) { { // Test case for double quoted regex matcher matcher: labels.MustNewMatcher(labels.MatchRegexp, "test", "^(?:a|b)$"), - hasError: true, + hasError: false, }, } From 515890ec5307a4e8448c50589ac3fc442e0e4671 Mon Sep 17 00:00:00 2001 From: Marco Pracucci Date: Fri, 26 Jan 2024 06:26:52 +0100 Subject: [PATCH 004/127] Use Matcher.SetMatches() Signed-off-by: Marco Pracucci --- tsdb/querier.go | 54 ++----------------------------------- tsdb/querier_test.go | 63 -------------------------------------------- 2 files changed, 2 insertions(+), 115 deletions(-) diff --git a/tsdb/querier.go b/tsdb/querier.go index a692c98f1a..4473323414 100644 --- a/tsdb/querier.go +++ b/tsdb/querier.go @@ -18,7 +18,6 @@ import ( "errors" "fmt" "math" - "strings" "unicode/utf8" "github.com/oklog/ulid" @@ -186,55 +185,6 @@ func (q *blockChunkQuerier) Select(ctx context.Context, sortSeries bool, hints * return NewBlockChunkSeriesSet(q.blockID, q.index, q.chunks, q.tombstones, p, mint, maxt, disableTrimming) } -func findSetMatches(pattern string) []string { - // Return empty matches if the wrapper from Prometheus is missing. - if len(pattern) < 6 || pattern[:4] != "^(?:" || pattern[len(pattern)-2:] != ")$" { - return nil - } - escaped := false - sets := []*strings.Builder{{}} - init := 4 - end := len(pattern) - 2 - // If the regex is wrapped in a group we can remove the first and last parentheses - if pattern[init] == '(' && pattern[end-1] == ')' { - init++ - end-- - } - for i := init; i < end; i++ { - if escaped { - switch { - case isRegexMetaCharacter(pattern[i]): - sets[len(sets)-1].WriteByte(pattern[i]) - case pattern[i] == '\\': - sets[len(sets)-1].WriteByte('\\') - default: - return nil - } - escaped = false - } else { - switch { - case isRegexMetaCharacter(pattern[i]): - if pattern[i] == '|' { - sets = append(sets, &strings.Builder{}) - } else { - return nil - } - case pattern[i] == '\\': - escaped = true - default: - sets[len(sets)-1].WriteByte(pattern[i]) - } - } - } - matches := make([]string, 0, len(sets)) - for _, s := range sets { - if s.Len() > 0 { - matches = append(matches, s.String()) - } - } - return matches -} - // PostingsForMatchers assembles a single postings iterator against the index reader // based on the given matchers. The resulting postings are not ordered by series. func PostingsForMatchers(ctx context.Context, ix IndexReader, ms ...*labels.Matcher) (index.Postings, error) { @@ -376,7 +326,7 @@ func postingsForMatcher(ctx context.Context, ix IndexReader, m *labels.Matcher) // Fast-path for set matching. if m.Type == labels.MatchRegexp { - setMatches := findSetMatches(m.GetRegexString()) + setMatches := m.SetMatches() if len(setMatches) > 0 { return ix.Postings(ctx, m.Name, setMatches...) } @@ -407,7 +357,7 @@ func inversePostingsForMatcher(ctx context.Context, ix IndexReader, m *labels.Ma // Inverse of a MatchNotRegexp is MatchRegexp (double negation). // Fast-path for set matching. if m.Type == labels.MatchNotRegexp { - setMatches := findSetMatches(m.GetRegexString()) + setMatches := m.SetMatches() if len(setMatches) > 0 { return ix.Postings(ctx, m.Name, setMatches...) } diff --git a/tsdb/querier_test.go b/tsdb/querier_test.go index ce59c0ee77..ed7e4e3404 100644 --- a/tsdb/querier_test.go +++ b/tsdb/querier_test.go @@ -2637,69 +2637,6 @@ func BenchmarkSetMatcher(b *testing.B) { } } -// Refer to https://github.com/prometheus/prometheus/issues/2651. -func TestFindSetMatches(t *testing.T) { - cases := []struct { - pattern string - exp []string - }{ - // Single value, coming from a `bar=~"foo"` selector. - { - pattern: "^(?:foo)$", - exp: []string{ - "foo", - }, - }, - // Simple sets. - { - pattern: "^(?:foo|bar|baz)$", - exp: []string{ - "foo", - "bar", - "baz", - }, - }, - // Simple sets containing escaped characters. - { - pattern: "^(?:fo\\.o|bar\\?|\\^baz)$", - exp: []string{ - "fo.o", - "bar?", - "^baz", - }, - }, - // Simple sets containing special characters without escaping. - { - pattern: "^(?:fo.o|bar?|^baz)$", - exp: nil, - }, - // Missing wrapper. - { - pattern: "foo|bar|baz", - exp: nil, - }, - } - - for _, c := range cases { - matches := findSetMatches(c.pattern) - if len(c.exp) == 0 { - if len(matches) != 0 { - t.Errorf("Evaluating %s, unexpected result %v", c.pattern, matches) - } - } else { - if len(matches) != len(c.exp) { - t.Errorf("Evaluating %s, length of result not equal to exp", c.pattern) - } else { - for i := 0; i < len(c.exp); i++ { - if c.exp[i] != matches[i] { - t.Errorf("Evaluating %s, unexpected result %s", c.pattern, matches[i]) - } - } - } - } - } -} - func TestPostingsForMatchers(t *testing.T) { ctx := context.Background() From ec9cada56e78a3332e64beafd2874229c7503fb2 Mon Sep 17 00:00:00 2001 From: Marco Pracucci Date: Fri, 26 Jan 2024 06:35:02 +0100 Subject: [PATCH 005/127] Remove unused isRegexMetaCharacter() Signed-off-by: Marco Pracucci --- tsdb/querier.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tsdb/querier.go b/tsdb/querier.go index 4473323414..899674ef12 100644 --- a/tsdb/querier.go +++ b/tsdb/querier.go @@ -18,7 +18,6 @@ import ( "errors" "fmt" "math" - "unicode/utf8" "github.com/oklog/ulid" "golang.org/x/exp/slices" @@ -34,20 +33,6 @@ import ( "github.com/prometheus/prometheus/util/annotations" ) -// Bitmap used by func isRegexMetaCharacter to check whether a character needs to be escaped. -var regexMetaCharacterBytes [16]byte - -// isRegexMetaCharacter reports whether byte b needs to be escaped. -func isRegexMetaCharacter(b byte) bool { - return b < utf8.RuneSelf && regexMetaCharacterBytes[b%16]&(1<<(b/16)) != 0 -} - -func init() { - for _, b := range []byte(`.+*?()|[]{}^$`) { - regexMetaCharacterBytes[b%16] |= 1 << (b / 16) - } -} - type blockBaseQuerier struct { blockID ulid.ULID index IndexReader From 1a8ea78207af44d2e26c53a55341687b859340d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Thu, 16 Nov 2023 13:16:47 +0000 Subject: [PATCH 006/127] Fix BenchmarkScrapeLoopAppendOM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenMetrics requires EOF comment at the end of metrics body, but the makeTestMetrics() function doesn't append it. This means this benchmark tests a response with errors but I don't think that was the intention. Signed-off-by: Ɓukasz Mierzwa --- scrape/scrape_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/scrape/scrape_test.go b/scrape/scrape_test.go index bcaeb460e2..e37d091aec 100644 --- a/scrape/scrape_test.go +++ b/scrape/scrape_test.go @@ -1068,6 +1068,7 @@ func makeTestMetrics(n int) []byte { fmt.Fprintf(&sb, "# HELP metric_a help text\n") fmt.Fprintf(&sb, "metric_a{foo=\"%d\",bar=\"%d\"} 1\n", i, i*100) } + fmt.Fprintf(&sb, "# EOF\n") return sb.Bytes() } From 50c81bed86327fa4c954d5cab35142f84bfce532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Thu, 16 Nov 2023 13:22:28 +0000 Subject: [PATCH 007/127] Check for duplicated series on a scrape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Prometheus scrapes a target and it sees the same time series repeated multiple times it currently silently ignores that. This change adds a test for that and fixes the scrape loop so that: - Only first sample for each unique time series is appended - Duplicated samples increment the prometheus_target_scrapes_sample_duplicate_timestamp_total metric This allows one to identify such scrape jobs and targets. Benchmark results: ``` name old time/op new time/op delta ScrapeLoopAppend-8 64.8”s ± 2% 71.1”s ±20% +9.75% (p=0.000 n=10+10) ScrapeLoopAppendOM-8 64.2”s ± 1% 68.5”s ± 7% +6.71% (p=0.000 n=9+10) TargetsFromGroup/1_targets-8 14.2”s ± 1% 14.5”s ± 1% +1.99% (p=0.000 n=10+10) TargetsFromGroup/10_targets-8 149”s ± 1% 152”s ± 1% +2.05% (p=0.000 n=9+10) TargetsFromGroup/100_targets-8 1.49ms ± 4% 1.48ms ± 1% ~ (p=0.796 n=10+10) name old alloc/op new alloc/op delta ScrapeLoopAppend-8 19.9kB ± 1% 17.8kB ± 3% -10.23% (p=0.000 n=8+10) ScrapeLoopAppendOM-8 19.9kB ± 1% 18.3kB ±10% -8.14% (p=0.001 n=9+10) TargetsFromGroup/1_targets-8 2.43kB ± 0% 2.43kB ± 0% -0.15% (p=0.045 n=10+10) TargetsFromGroup/10_targets-8 24.3kB ± 0% 24.3kB ± 0% ~ (p=0.083 n=10+9) TargetsFromGroup/100_targets-8 243kB ± 0% 243kB ± 0% ~ (p=0.720 n=9+10) name old allocs/op new allocs/op delta ScrapeLoopAppend-8 9.00 ± 0% 9.00 ± 0% ~ (all equal) ScrapeLoopAppendOM-8 10.0 ± 0% 10.0 ± 0% ~ (all equal) TargetsFromGroup/1_targets-8 40.0 ± 0% 40.0 ± 0% ~ (all equal) TargetsFromGroup/10_targets-8 400 ± 0% 400 ± 0% ~ (all equal) TargetsFromGroup/100_targets-8 4.00k ± 0% 4.00k ± 0% ~ (all equal) ``` Signed-off-by: Ɓukasz Mierzwa --- scrape/scrape.go | 53 +++++++++++++++++++++++++------------------ scrape/scrape_test.go | 28 +++++++++++++++++++++++ 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/scrape/scrape.go b/scrape/scrape.go index aa2d5538b1..9bff47eebc 100644 --- a/scrape/scrape.go +++ b/scrape/scrape.go @@ -1512,13 +1512,13 @@ func (sl *scrapeLoop) append(app storage.Appender, b []byte, contentType string, loop: for { var ( - et textparse.Entry - sampleAdded, isHistogram bool - met []byte - parsedTimestamp *int64 - val float64 - h *histogram.Histogram - fh *histogram.FloatHistogram + et textparse.Entry + sampleAdded, isHistogram, seriesAlreadyScraped bool + met []byte + parsedTimestamp *int64 + val float64 + h *histogram.Histogram + fh *histogram.FloatHistogram ) if et, err = p.Next(); err != nil { if errors.Is(err, io.EOF) { @@ -1573,6 +1573,7 @@ loop: if ok { ref = ce.ref lset = ce.lset + hash = ce.hash // Update metadata only if it changed in the current iteration. updateMetadata(lset, false) @@ -1609,24 +1610,30 @@ loop: updateMetadata(lset, true) } - if ctMs := p.CreatedTimestamp(); sl.enableCTZeroIngestion && ctMs != nil { - ref, err = app.AppendCTZeroSample(ref, lset, t, *ctMs) - if err != nil && !errors.Is(err, storage.ErrOutOfOrderCT) { // OOO is a common case, ignoring completely for now. - // CT is an experimental feature. For now, we don't need to fail the - // scrape on errors updating the created timestamp, log debug. - level.Debug(sl.l).Log("msg", "Error when appending CT in scrape loop", "series", string(met), "ct", *ctMs, "t", t, "err", err) + _, seriesAlreadyScraped = sl.cache.seriesCur[hash] + if seriesAlreadyScraped { + err = storage.ErrDuplicateSampleForTimestamp + } else { + if ctMs := p.CreatedTimestamp(); sl.enableCTZeroIngestion && ctMs != nil { + ref, err = app.AppendCTZeroSample(ref, lset, t, *ctMs) + if err != nil && !errors.Is(err, storage.ErrOutOfOrderCT) { // OOO is a common case, ignoring completely for now. + // CT is an experimental feature. For now, we don't need to fail the + // scrape on errors updating the created timestamp, log debug. + level.Debug(sl.l).Log("msg", "Error when appending CT in scrape loop", "series", string(met), "ct", *ctMs, "t", t, "err", err) + } + } + + if isHistogram { + if h != nil { + ref, err = app.AppendHistogram(ref, lset, t, h, nil) + } else { + ref, err = app.AppendHistogram(ref, lset, t, nil, fh) + } + } else { + ref, err = app.Append(ref, lset, t, val) } } - if isHistogram { - if h != nil { - ref, err = app.AppendHistogram(ref, lset, t, h, nil) - } else { - ref, err = app.AppendHistogram(ref, lset, t, nil, fh) - } - } else { - ref, err = app.Append(ref, lset, t, val) - } sampleAdded, err = sl.checkAddError(ce, met, parsedTimestamp, err, &sampleLimitErr, &bucketLimitErr, &appErrs) if err != nil { if !errors.Is(err, storage.ErrNotFound) { @@ -1648,6 +1655,8 @@ loop: // Increment added even if there's an error so we correctly report the // number of samples remaining after relabeling. + // We still report duplicated samples here since this number should be the exact number + // of time series exposed on a scrape after relabelling. added++ exemplars = exemplars[:0] // Reset and reuse the exemplar slice. for hasExemplar := p.Exemplar(&e); hasExemplar; hasExemplar = p.Exemplar(&e) { diff --git a/scrape/scrape_test.go b/scrape/scrape_test.go index e37d091aec..4732fbe0d0 100644 --- a/scrape/scrape_test.go +++ b/scrape/scrape_test.go @@ -3600,3 +3600,31 @@ func BenchmarkTargetScraperGzip(b *testing.B) { }) } } + +// When a scrape contains multiple instances for the same time series we should increment +// prometheus_target_scrapes_sample_duplicate_timestamp_total metric. +func TestScrapeLoopSeriesAddedDuplicates(t *testing.T) { + ctx, sl := simpleTestScrapeLoop(t) + + slApp := sl.appender(ctx) + total, added, seriesAdded, err := sl.append(slApp, []byte("test_metric 1\ntest_metric 2\ntest_metric 3\n"), "", time.Time{}) + require.NoError(t, err) + require.NoError(t, slApp.Commit()) + require.Equal(t, 3, total) + require.Equal(t, 3, added) + require.Equal(t, 1, seriesAdded) + + slApp = sl.appender(ctx) + total, added, seriesAdded, err = sl.append(slApp, []byte("test_metric 1\ntest_metric 1\ntest_metric 1\n"), "", time.Time{}) + require.NoError(t, err) + require.NoError(t, slApp.Commit()) + require.Equal(t, 3, total) + require.Equal(t, 3, added) + require.Equal(t, 0, seriesAdded) + + metric := dto.Metric{} + err = sl.metrics.targetScrapeSampleDuplicate.Write(&metric) + require.NoError(t, err) + value := metric.GetCounter().GetValue() + require.Equal(t, 4.0, value) +} From 55dcaab41bedc995460927a2c4f3e977d23c374c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Thu, 16 Nov 2023 14:35:44 +0000 Subject: [PATCH 008/127] Fix TestScrapeLoopDiscardDuplicateLabels test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This test calls Rollback() which is normally called from within append code. Doing so means that staleness tracking data is outdated and need to by cycled manually. Signed-off-by: Ɓukasz Mierzwa --- scrape/scrape_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scrape/scrape_test.go b/scrape/scrape_test.go index 4732fbe0d0..dbe9f0bb29 100644 --- a/scrape/scrape_test.go +++ b/scrape/scrape_test.go @@ -2636,6 +2636,9 @@ func TestScrapeLoopDiscardDuplicateLabels(t *testing.T) { _, _, _, err := sl.append(slApp, []byte("test_metric{le=\"500\"} 1\ntest_metric{le=\"600\",le=\"700\"} 1\n"), "", time.Time{}) require.Error(t, err) require.NoError(t, slApp.Rollback()) + // We need to cycle staleness cache maps after a manual rollback. Otherwise they will have old entries in them, + // which would cause ErrDuplicateSampleForTimestamp errors on the next append. + sl.cache.iterDone(true) q, err := s.Querier(time.Time{}.UnixNano(), 0) require.NoError(t, err) From 21f8b35f5bdef290c9e1254ab2e0b86d8f68237a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 28 Nov 2023 17:42:29 +0000 Subject: [PATCH 009/127] Move staleness tracking out of checkAddError() calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This call bloats checkAddError signature and logic, we can and should call it from the main scrape logic. Signed-off-by: Ɓukasz Mierzwa --- scrape/scrape.go | 13 ++++++++----- scrape/scrape_test.go | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/scrape/scrape.go b/scrape/scrape.go index 9bff47eebc..701aa609f3 100644 --- a/scrape/scrape.go +++ b/scrape/scrape.go @@ -1634,7 +1634,13 @@ loop: } } - sampleAdded, err = sl.checkAddError(ce, met, parsedTimestamp, err, &sampleLimitErr, &bucketLimitErr, &appErrs) + if err == nil { + if (parsedTimestamp == nil || sl.trackTimestampsStaleness) && ce != nil { + sl.cache.trackStaleness(ce.hash, ce.lset) + } + } + + sampleAdded, err = sl.checkAddError(met, err, &sampleLimitErr, &bucketLimitErr, &appErrs) if err != nil { if !errors.Is(err, storage.ErrNotFound) { level.Debug(sl.l).Log("msg", "Unexpected error", "series", string(met), "err", err) @@ -1751,12 +1757,9 @@ loop: // Adds samples to the appender, checking the error, and then returns the # of samples added, // whether the caller should continue to process more samples, and any sample or bucket limit errors. -func (sl *scrapeLoop) checkAddError(ce *cacheEntry, met []byte, tp *int64, err error, sampleLimitErr, bucketLimitErr *error, appErrs *appendErrors) (bool, error) { +func (sl *scrapeLoop) checkAddError(met []byte, err error, sampleLimitErr, bucketLimitErr *error, appErrs *appendErrors) (bool, error) { switch { case err == nil: - if (tp == nil || sl.trackTimestampsStaleness) && ce != nil { - sl.cache.trackStaleness(ce.hash, ce.lset) - } return true, nil case errors.Is(err, storage.ErrNotFound): return false, storage.ErrNotFound diff --git a/scrape/scrape_test.go b/scrape/scrape_test.go index dbe9f0bb29..0dfca538d8 100644 --- a/scrape/scrape_test.go +++ b/scrape/scrape_test.go @@ -2975,7 +2975,7 @@ func TestReuseCacheRace(t *testing.T) { func TestCheckAddError(t *testing.T) { var appErrs appendErrors sl := scrapeLoop{l: log.NewNopLogger(), metrics: newTestScrapeMetrics(t)} - sl.checkAddError(nil, nil, nil, storage.ErrOutOfOrderSample, nil, nil, &appErrs) + sl.checkAddError(nil, storage.ErrOutOfOrderSample, nil, nil, &appErrs) require.Equal(t, 1, appErrs.numOutOfOrder) } From c013a3c1b572fb7a11683c49590822c46d91a917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 27 Feb 2024 12:09:32 +0000 Subject: [PATCH 010/127] Check of duplicated samples directly in scrapeCache.get() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid extra map lookup by hooking check into cache get. ``` goos: linux goarch: amd64 pkg: github.com/prometheus/prometheus/scrape cpu: Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz │ main.txt │ new.txt │ │ sec/op │ sec/op vs base │ ScrapeLoopAppend-8 66.72” ± 0% 66.89” ± 0% ~ (p=0.879 n=50) ScrapeLoopAppendOM-8 66.61” ± 0% 66.89” ± 1% ~ (p=0.115 n=50) geomean 66.66” 66.89” +0.34% │ main.txt │ new.txt │ │ B/op │ B/op vs base │ ScrapeLoopAppend-8 20.17Ki ± 1% 20.12Ki ± 1% ~ (p=0.343 n=50) ScrapeLoopAppendOM-8 20.38Ki ± 10% 17.99Ki ± 2% -11.69% (p=0.017 n=50) geomean 20.27Ki 19.03Ki -6.14% │ main.txt │ new.txt │ │ allocs/op │ allocs/op vs base │ ScrapeLoopAppend-8 11.00 ± 0% 11.00 ± 0% ~ (p=1.000 n=50) Âč ScrapeLoopAppendOM-8 12.00 ± 0% 12.00 ± 0% ~ (p=1.000 n=50) Âč geomean 11.49 11.49 +0.00% Âč all samples are equal ``` Signed-off-by: Ɓukasz Mierzwa --- scrape/scrape.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/scrape/scrape.go b/scrape/scrape.go index 701aa609f3..6be858c681 100644 --- a/scrape/scrape.go +++ b/scrape/scrape.go @@ -954,13 +954,14 @@ func (c *scrapeCache) iterDone(flushCache bool) { } } -func (c *scrapeCache) get(met []byte) (*cacheEntry, bool) { +func (c *scrapeCache) get(met []byte) (*cacheEntry, bool, bool) { e, ok := c.series[string(met)] if !ok { - return nil, false + return nil, false, false } + alreadyScraped := e.lastIter == c.iter e.lastIter = c.iter - return e, true + return e, true, alreadyScraped } func (c *scrapeCache) addRef(met []byte, ref storage.SeriesRef, lset labels.Labels, hash uint64) { @@ -1512,13 +1513,13 @@ func (sl *scrapeLoop) append(app storage.Appender, b []byte, contentType string, loop: for { var ( - et textparse.Entry - sampleAdded, isHistogram, seriesAlreadyScraped bool - met []byte - parsedTimestamp *int64 - val float64 - h *histogram.Histogram - fh *histogram.FloatHistogram + et textparse.Entry + sampleAdded, isHistogram bool + met []byte + parsedTimestamp *int64 + val float64 + h *histogram.Histogram + fh *histogram.FloatHistogram ) if et, err = p.Next(); err != nil { if errors.Is(err, io.EOF) { @@ -1564,7 +1565,7 @@ loop: if sl.cache.getDropped(met) { continue } - ce, ok := sl.cache.get(met) + ce, ok, seriesAlreadyScraped := sl.cache.get(met) var ( ref storage.SeriesRef hash uint64 @@ -1610,7 +1611,6 @@ loop: updateMetadata(lset, true) } - _, seriesAlreadyScraped = sl.cache.seriesCur[hash] if seriesAlreadyScraped { err = storage.ErrDuplicateSampleForTimestamp } else { @@ -1882,7 +1882,7 @@ func (sl *scrapeLoop) reportStale(app storage.Appender, start time.Time) (err er } func (sl *scrapeLoop) addReportSample(app storage.Appender, s []byte, t int64, v float64, b *labels.Builder) error { - ce, ok := sl.cache.get(s) + ce, ok, _ := sl.cache.get(s) var ref storage.SeriesRef var lset labels.Labels if ok { From 0e81ab44a2d5d1f7ca5c6e31618f7fc14e84a5d5 Mon Sep 17 00:00:00 2001 From: machine424 Date: Wed, 7 Feb 2024 12:38:40 +0100 Subject: [PATCH 011/127] discovery(k8s): add a metric to track failed requests, failures will still be logged. Signed-off-by: machine424 --- discovery/kubernetes/kubernetes.go | 51 ++++++++++++++++++------- discovery/kubernetes/kubernetes_test.go | 40 +++++++++++++++++++ discovery/kubernetes/metrics.go | 13 ++++++- 3 files changed, 90 insertions(+), 14 deletions(-) diff --git a/discovery/kubernetes/kubernetes.go b/discovery/kubernetes/kubernetes.go index d6b8115848..94058aa04e 100644 --- a/discovery/kubernetes/kubernetes.go +++ b/discovery/kubernetes/kubernetes.go @@ -485,8 +485,8 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) { eps := NewEndpointSlice( log.With(d.logger, "role", "endpointslice"), informer, - cache.NewSharedInformer(slw, &apiv1.Service{}, resyncDisabled), - cache.NewSharedInformer(plw, &apiv1.Pod{}, resyncDisabled), + d.mustNewSharedInformer(slw, &apiv1.Service{}, resyncDisabled), + d.mustNewSharedInformer(plw, &apiv1.Pod{}, resyncDisabled), nodeInf, d.metrics.eventCount, ) @@ -545,8 +545,8 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) { eps := NewEndpoints( log.With(d.logger, "role", "endpoint"), d.newEndpointsByNodeInformer(elw), - cache.NewSharedInformer(slw, &apiv1.Service{}, resyncDisabled), - cache.NewSharedInformer(plw, &apiv1.Pod{}, resyncDisabled), + d.mustNewSharedInformer(slw, &apiv1.Service{}, resyncDisabled), + d.mustNewSharedInformer(plw, &apiv1.Pod{}, resyncDisabled), nodeInf, d.metrics.eventCount, ) @@ -602,7 +602,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) { } svc := NewService( log.With(d.logger, "role", "service"), - cache.NewSharedInformer(slw, &apiv1.Service{}, resyncDisabled), + d.mustNewSharedInformer(slw, &apiv1.Service{}, resyncDisabled), d.metrics.eventCount, ) d.discoverers = append(d.discoverers, svc) @@ -641,7 +641,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) { return i.Watch(ctx, options) }, } - informer = cache.NewSharedInformer(ilw, &networkv1.Ingress{}, resyncDisabled) + informer = d.mustNewSharedInformer(ilw, &networkv1.Ingress{}, resyncDisabled) } else { i := d.client.NetworkingV1beta1().Ingresses(namespace) ilw := &cache.ListWatch{ @@ -656,7 +656,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) { return i.Watch(ctx, options) }, } - informer = cache.NewSharedInformer(ilw, &v1beta1.Ingress{}, resyncDisabled) + informer = d.mustNewSharedInformer(ilw, &v1beta1.Ingress{}, resyncDisabled) } ingress := NewIngress( log.With(d.logger, "role", "ingress"), @@ -747,7 +747,7 @@ func (d *Discovery) newNodeInformer(ctx context.Context) cache.SharedInformer { return d.client.CoreV1().Nodes().Watch(ctx, options) }, } - return cache.NewSharedInformer(nlw, &apiv1.Node{}, resyncDisabled) + return d.mustNewSharedInformer(nlw, &apiv1.Node{}, resyncDisabled) } func (d *Discovery) newPodsByNodeInformer(plw *cache.ListWatch) cache.SharedIndexInformer { @@ -762,7 +762,7 @@ func (d *Discovery) newPodsByNodeInformer(plw *cache.ListWatch) cache.SharedInde } } - return cache.NewSharedIndexInformer(plw, &apiv1.Pod{}, resyncDisabled, indexers) + return d.mustNewSharedIndexInformer(plw, &apiv1.Pod{}, resyncDisabled, indexers) } func (d *Discovery) newEndpointsByNodeInformer(plw *cache.ListWatch) cache.SharedIndexInformer { @@ -783,7 +783,7 @@ func (d *Discovery) newEndpointsByNodeInformer(plw *cache.ListWatch) cache.Share return pods, nil } if !d.attachMetadata.Node { - return cache.NewSharedIndexInformer(plw, &apiv1.Endpoints{}, resyncDisabled, indexers) + return d.mustNewSharedIndexInformer(plw, &apiv1.Endpoints{}, resyncDisabled, indexers) } indexers[nodeIndex] = func(obj interface{}) ([]string, error) { @@ -809,13 +809,13 @@ func (d *Discovery) newEndpointsByNodeInformer(plw *cache.ListWatch) cache.Share return nodes, nil } - return cache.NewSharedIndexInformer(plw, &apiv1.Endpoints{}, resyncDisabled, indexers) + return d.mustNewSharedIndexInformer(plw, &apiv1.Endpoints{}, resyncDisabled, indexers) } func (d *Discovery) newEndpointSlicesByNodeInformer(plw *cache.ListWatch, object runtime.Object) cache.SharedIndexInformer { indexers := make(map[string]cache.IndexFunc) if !d.attachMetadata.Node { - return cache.NewSharedIndexInformer(plw, object, resyncDisabled, indexers) + return d.mustNewSharedIndexInformer(plw, object, resyncDisabled, indexers) } indexers[nodeIndex] = func(obj interface{}) ([]string, error) { @@ -854,7 +854,32 @@ func (d *Discovery) newEndpointSlicesByNodeInformer(plw *cache.ListWatch, object return nodes, nil } - return cache.NewSharedIndexInformer(plw, object, resyncDisabled, indexers) + return d.mustNewSharedIndexInformer(plw, object, resyncDisabled, indexers) +} + +func (d *Discovery) informerWatchErrorHandler(r *cache.Reflector, err error) { + d.metrics.failuresCount.Inc() + cache.DefaultWatchErrorHandler(r, err) +} + +func (d *Discovery) mustNewSharedInformer(lw cache.ListerWatcher, exampleObject runtime.Object, defaultEventHandlerResyncPeriod time.Duration) cache.SharedInformer { + informer := cache.NewSharedInformer(lw, exampleObject, defaultEventHandlerResyncPeriod) + // Invoking SetWatchErrorHandler should fail only if the informer has been started beforehand. + // Such a scenario would suggest an incorrect use of the API, thus the panic. + if err := informer.SetWatchErrorHandler(d.informerWatchErrorHandler); err != nil { + panic(err) + } + return informer +} + +func (d *Discovery) mustNewSharedIndexInformer(lw cache.ListerWatcher, exampleObject runtime.Object, defaultEventHandlerResyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + informer := cache.NewSharedIndexInformer(lw, exampleObject, defaultEventHandlerResyncPeriod, indexers) + // Invoking SetWatchErrorHandler should fail only if the informer has been started beforehand. + // Such a scenario would suggest an incorrect use of the API, thus the panic. + if err := informer.SetWatchErrorHandler(d.informerWatchErrorHandler); err != nil { + panic(err) + } + return informer } func checkDiscoveryV1Supported(client kubernetes.Interface) (bool, error) { diff --git a/discovery/kubernetes/kubernetes_test.go b/discovery/kubernetes/kubernetes_test.go index e3dd093009..552f8a4453 100644 --- a/discovery/kubernetes/kubernetes_test.go +++ b/discovery/kubernetes/kubernetes_test.go @@ -21,12 +21,16 @@ import ( "time" "github.com/go-kit/log" + prom_testutil "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/version" + "k8s.io/apimachinery/pkg/watch" fakediscovery "k8s.io/client-go/discovery/fake" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" + kubetesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/cache" "github.com/prometheus/client_golang/prometheus" @@ -314,3 +318,39 @@ func TestCheckNetworkingV1Supported(t *testing.T) { }) } } + +func TestFailuresCountMetric(t *testing.T) { + tests := []struct { + role Role + minFailedWatches int + }{ + {RoleNode, 1}, + {RolePod, 1}, + {RoleService, 1}, + {RoleEndpoint, 3}, + {RoleEndpointSlice, 3}, + {RoleIngress, 1}, + } + + for _, tc := range tests { + tc := tc + t.Run(string(tc.role), func(t *testing.T) { + t.Parallel() + + n, c := makeDiscovery(tc.role, NamespaceDiscovery{}) + // The counter is initialized and no failures at the beginning. + require.Equal(t, float64(0), prom_testutil.ToFloat64(n.metrics.failuresCount)) + + // Simulate an error on watch requests. + c.Discovery().(*fakediscovery.FakeDiscovery).PrependWatchReactor("*", func(action kubetesting.Action) (bool, watch.Interface, error) { + return true, nil, apierrors.NewUnauthorized("unauthorized") + }) + + // Start the discovery. + k8sDiscoveryTest{discovery: n}.Run(t) + + // At least the errors of the initial watches should be caught (watches are retried on errors). + require.GreaterOrEqual(t, prom_testutil.ToFloat64(n.metrics.failuresCount), float64(tc.minFailedWatches)) + }) + } +} diff --git a/discovery/kubernetes/metrics.go b/discovery/kubernetes/metrics.go index 7d384fb96a..fe419bc782 100644 --- a/discovery/kubernetes/metrics.go +++ b/discovery/kubernetes/metrics.go @@ -22,7 +22,8 @@ import ( var _ discovery.DiscovererMetrics = (*kubernetesMetrics)(nil) type kubernetesMetrics struct { - eventCount *prometheus.CounterVec + eventCount *prometheus.CounterVec + failuresCount prometheus.Counter metricRegisterer discovery.MetricRegisterer } @@ -37,10 +38,18 @@ func newDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetric }, []string{"role", "event"}, ), + failuresCount: prometheus.NewCounter( + prometheus.CounterOpts{ + Namespace: discovery.KubernetesMetricsNamespace, + Name: "failures_total", + Help: "The number of failed WATCH/LIST requests.", + }, + ), } m.metricRegisterer = discovery.NewMetricRegisterer(reg, []prometheus.Collector{ m.eventCount, + m.failuresCount, }) // Initialize metric vectors. @@ -61,6 +70,8 @@ func newDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetric } } + m.failuresCount.Add(0) + return m } From 7d364c0451957f24715541d58523e4795a8ebec4 Mon Sep 17 00:00:00 2001 From: beorn7 Date: Wed, 14 Feb 2024 19:39:59 +0100 Subject: [PATCH 012/127] promql: remove redundant line Signed-off-by: beorn7 --- promql/engine.go | 1 - 1 file changed, 1 deletion(-) diff --git a/promql/engine.go b/promql/engine.go index 68e0502907..8619b7675a 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1221,7 +1221,6 @@ func (ev *evaluator) rangeEval(prepSeries func(labels.Labels, *EvalSeriesHelper) if ev.currentSamples > ev.maxSamples { ev.error(ErrTooManySamples(env)) } - ev.samplesStats.UpdatePeak(ev.currentSamples) // If this could be an instant query, shortcut so as not to change sort order. if ev.endTimestamp == ev.startTimestamp { From f46dd34982d94e4729d2327ba7fe701cb15474fe Mon Sep 17 00:00:00 2001 From: beorn7 Date: Wed, 28 Feb 2024 16:13:15 +0100 Subject: [PATCH 013/127] promql: Add code comment Signed-off-by: beorn7 --- promql/engine.go | 3 +++ promql/engine_test.go | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/promql/engine.go b/promql/engine.go index 8619b7675a..f4b95c3759 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1196,6 +1196,9 @@ func (ev *evaluator) rangeEval(prepSeries func(labels.Labels, *EvalSeriesHelper) if prepSeries != nil { bufHelpers[i] = append(bufHelpers[i], seriesHelpers[i][si]) } + // Don't add histogram size here because we only + // copy the pointer above, not the whole + // histogram. ev.currentSamples++ if ev.currentSamples > ev.maxSamples { ev.error(ErrTooManySamples(env)) diff --git a/promql/engine_test.go b/promql/engine_test.go index 105108d5b2..e541957f26 100644 --- a/promql/engine_test.go +++ b/promql/engine_test.go @@ -755,6 +755,7 @@ load 10s metricWith3SampleEvery10Seconds{a="1",b="1"} 1+1x100 metricWith3SampleEvery10Seconds{a="2",b="2"} 1+1x100 metricWith3SampleEvery10Seconds{a="3",b="2"} 1+1x100 + metricWith1HistogramsEvery10Seconds {{schema:1 count:5 sum:20 buckets:[1 2 1 1]}}+{{schema:1 count:10 sum:5 buckets:[1 2 3 4]}}x100 `) t.Cleanup(func() { storage.Close() }) @@ -795,6 +796,15 @@ load 10s 21000: 1, }, }, + { + Query: "metricWith1HistogramEvery10Seconds", + Start: time.Unix(21, 0), + PeakSamples: 1, + TotalSamples: 1, // 1 sample / 10 seconds + TotalSamplesPerStep: stats.TotalSamplesPerStep{ + 21000: 1, + }, + }, { // timestamp function has a special handling. Query: "timestamp(metricWith1SampleEvery10Seconds)", @@ -1041,7 +1051,7 @@ load 10s End: time.Unix(220, 0), Interval: 5 * time.Second, PeakSamples: 5, - TotalSamples: 4, // (1 sample / 10 seconds) * 4 steps + TotalSamples: 4, // 1 sample per query * 4 steps TotalSamplesPerStep: stats.TotalSamplesPerStep{ 201000: 1, 206000: 1, From f48c7a5503a2da9d90c638c5aef14696cfbddbf1 Mon Sep 17 00:00:00 2001 From: beorn7 Date: Wed, 28 Feb 2024 18:10:46 +0100 Subject: [PATCH 014/127] promql: Add histograms to TestQueryStatistics Also, fix the bugs exposed by the tests. Signed-off-by: beorn7 --- promql/engine.go | 63 +++++++++++++++++----------------- promql/engine_test.go | 80 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 38 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index f4b95c3759..151b8657a8 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1542,13 +1542,12 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, annotations.Annotatio histSamples := totalHPointSize(ss.Histograms) if len(ss.Floats)+histSamples > 0 { - if ev.currentSamples+len(ss.Floats)+histSamples <= ev.maxSamples { - mat = append(mat, ss) - prevSS = &mat[len(mat)-1] - ev.currentSamples += len(ss.Floats) + histSamples - } else { + if ev.currentSamples+len(ss.Floats)+histSamples > ev.maxSamples { ev.error(ErrTooManySamples(env)) } + mat = append(mat, ss) + prevSS = &mat[len(mat)-1] + ev.currentSamples += len(ss.Floats) + histSamples } ev.samplesStats.UpdatePeak(ev.currentSamples) @@ -1711,26 +1710,28 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, annotations.Annotatio step++ _, f, h, ok := ev.vectorSelectorSingle(it, e, ts) if ok { - if ev.currentSamples < ev.maxSamples { - if h == nil { - if ss.Floats == nil { - ss.Floats = reuseOrGetFPointSlices(prevSS, numSteps) - } - ss.Floats = append(ss.Floats, FPoint{F: f, T: ts}) - ev.currentSamples++ - ev.samplesStats.IncrementSamplesAtStep(step, 1) - } else { - if ss.Histograms == nil { - ss.Histograms = reuseOrGetHPointSlices(prevSS, numSteps) - } - point := HPoint{H: h, T: ts} - ss.Histograms = append(ss.Histograms, point) - histSize := point.size() - ev.currentSamples += histSize - ev.samplesStats.IncrementSamplesAtStep(step, int64(histSize)) + if h == nil { + ev.currentSamples++ + ev.samplesStats.IncrementSamplesAtStep(step, 1) + if ev.currentSamples > ev.maxSamples { + ev.error(ErrTooManySamples(env)) } + if ss.Floats == nil { + ss.Floats = reuseOrGetFPointSlices(prevSS, numSteps) + } + ss.Floats = append(ss.Floats, FPoint{F: f, T: ts}) } else { - ev.error(ErrTooManySamples(env)) + point := HPoint{H: h, T: ts} + histSize := point.size() + ev.currentSamples += histSize + ev.samplesStats.IncrementSamplesAtStep(step, int64(histSize)) + if ev.currentSamples > ev.maxSamples { + ev.error(ErrTooManySamples(env)) + } + if ss.Histograms == nil { + ss.Histograms = reuseOrGetHPointSlices(prevSS, numSteps) + } + ss.Histograms = append(ss.Histograms, point) } } } @@ -2170,10 +2171,10 @@ loop: histograms = histograms[:n] continue loop } - if ev.currentSamples >= ev.maxSamples { + ev.currentSamples += histograms[n].size() + if ev.currentSamples > ev.maxSamples { ev.error(ErrTooManySamples(env)) } - ev.currentSamples += histograms[n].size() } case chunkenc.ValFloat: t, f := buf.At() @@ -2182,10 +2183,10 @@ loop: } // Values in the buffer are guaranteed to be smaller than maxt. if t >= mintFloats { - if ev.currentSamples >= ev.maxSamples { + ev.currentSamples++ + if ev.currentSamples > ev.maxSamples { ev.error(ErrTooManySamples(env)) } - ev.currentSamples++ if floats == nil { floats = getFPointSlice(16) } @@ -2213,22 +2214,22 @@ loop: histograms = histograms[:n] break } - if ev.currentSamples >= ev.maxSamples { + ev.currentSamples += histograms[n].size() + if ev.currentSamples > ev.maxSamples { ev.error(ErrTooManySamples(env)) } - ev.currentSamples += histograms[n].size() case chunkenc.ValFloat: t, f := it.At() if t == maxt && !value.IsStaleNaN(f) { - if ev.currentSamples >= ev.maxSamples { + ev.currentSamples++ + if ev.currentSamples > ev.maxSamples { ev.error(ErrTooManySamples(env)) } if floats == nil { floats = getFPointSlice(16) } floats = append(floats, FPoint{T: t, F: f}) - ev.currentSamples++ } } ev.samplesStats.UpdatePeak(ev.currentSamples) diff --git a/promql/engine_test.go b/promql/engine_test.go index e541957f26..cc5d0ee780 100644 --- a/promql/engine_test.go +++ b/promql/engine_test.go @@ -755,7 +755,7 @@ load 10s metricWith3SampleEvery10Seconds{a="1",b="1"} 1+1x100 metricWith3SampleEvery10Seconds{a="2",b="2"} 1+1x100 metricWith3SampleEvery10Seconds{a="3",b="2"} 1+1x100 - metricWith1HistogramsEvery10Seconds {{schema:1 count:5 sum:20 buckets:[1 2 1 1]}}+{{schema:1 count:10 sum:5 buckets:[1 2 3 4]}}x100 + metricWith1HistogramEvery10Seconds {{schema:1 count:5 sum:20 buckets:[1 2 1 1]}}+{{schema:1 count:10 sum:5 buckets:[1 2 3 4]}}x100 `) t.Cleanup(func() { storage.Close() }) @@ -799,10 +799,10 @@ load 10s { Query: "metricWith1HistogramEvery10Seconds", Start: time.Unix(21, 0), - PeakSamples: 1, - TotalSamples: 1, // 1 sample / 10 seconds + PeakSamples: 12, + TotalSamples: 12, // 1 histogram sample of size 12 / 10 seconds TotalSamplesPerStep: stats.TotalSamplesPerStep{ - 21000: 1, + 21000: 12, }, }, { @@ -815,6 +815,15 @@ load 10s 21000: 1, }, }, + { + Query: "timestamp(metricWith1HistogramEvery10Seconds)", + Start: time.Unix(21, 0), + PeakSamples: 13, // histogram size 12 + 1 extra because of timestamp + TotalSamples: 1, // 1 float sample (because of timestamp) / 10 seconds + TotalSamplesPerStep: stats.TotalSamplesPerStep{ + 21000: 1, + }, + }, { Query: "metricWith1SampleEvery10Seconds", Start: time.Unix(22, 0), @@ -887,11 +896,20 @@ load 10s 201000: 6, }, }, + { + Query: "metricWith1HistogramEvery10Seconds[60s]", + Start: time.Unix(201, 0), + PeakSamples: 72, + TotalSamples: 72, // 1 histogram (size 12) / 10 seconds * 60 seconds + TotalSamplesPerStep: stats.TotalSamplesPerStep{ + 201000: 72, + }, + }, { Query: "max_over_time(metricWith1SampleEvery10Seconds[59s])[20s:5s]", Start: time.Unix(201, 0), PeakSamples: 10, - TotalSamples: 24, // (1 sample / 10 seconds * 60 seconds) * 60/5 (using 59s so we always return 6 samples + TotalSamples: 24, // (1 sample / 10 seconds * 60 seconds) * 20/5 (using 59s so we always return 6 samples // as if we run a query on 00 looking back 60 seconds we will return 7 samples; // see next test). TotalSamplesPerStep: stats.TotalSamplesPerStep{ @@ -902,12 +920,22 @@ load 10s Query: "max_over_time(metricWith1SampleEvery10Seconds[60s])[20s:5s]", Start: time.Unix(201, 0), PeakSamples: 11, - TotalSamples: 26, // (1 sample / 10 seconds * 60 seconds) + 2 as + TotalSamples: 26, // (1 sample / 10 seconds * 60 seconds) * 4 + 2 as // max_over_time(metricWith1SampleEvery10Seconds[60s]) @ 190 and 200 will return 7 samples. TotalSamplesPerStep: stats.TotalSamplesPerStep{ 201000: 26, }, }, + { + Query: "max_over_time(metricWith1HistogramEvery10Seconds[60s])[20s:5s]", + Start: time.Unix(201, 0), + PeakSamples: 72, + TotalSamples: 312, // (1 histogram (size 12) / 10 seconds * 60 seconds) * 4 + 2 * 12 as + // max_over_time(metricWith1SampleEvery10Seconds[60s]) @ 190 and 200 will return 7 samples. + TotalSamplesPerStep: stats.TotalSamplesPerStep{ + 201000: 312, + }, + }, { Query: "metricWith1SampleEvery10Seconds[60s] @ 30", Start: time.Unix(201, 0), @@ -917,6 +945,15 @@ load 10s 201000: 4, }, }, + { + Query: "metricWith1HistogramEvery10Seconds[60s] @ 30", + Start: time.Unix(201, 0), + PeakSamples: 48, + TotalSamples: 48, // @ modifier force the evaluation to at 30 seconds - So it brings 4 datapoints (0, 10, 20, 30 seconds) * 1 series + TotalSamplesPerStep: stats.TotalSamplesPerStep{ + 201000: 48, + }, + }, { Query: "sum(max_over_time(metricWith3SampleEvery10Seconds[60s] @ 30))", Start: time.Unix(201, 0), @@ -1045,7 +1082,21 @@ load 10s }, }, { - // timestamp function as a special handling + Query: `metricWith1HistogramEvery10Seconds`, + Start: time.Unix(204, 0), + End: time.Unix(223, 0), + Interval: 5 * time.Second, + PeakSamples: 48, + TotalSamples: 48, // 1 histogram (size 12) per query * 4 steps + TotalSamplesPerStep: stats.TotalSamplesPerStep{ + 204000: 12, // aligned to the step time, not the sample time + 209000: 12, + 214000: 12, + 219000: 12, + }, + }, + { + // timestamp function has a special handling Query: "timestamp(metricWith1SampleEvery10Seconds)", Start: time.Unix(201, 0), End: time.Unix(220, 0), @@ -1059,6 +1110,21 @@ load 10s 216000: 1, }, }, + { + // timestamp function has a special handling + Query: "timestamp(metricWith1HistogramEvery10Seconds)", + Start: time.Unix(201, 0), + End: time.Unix(220, 0), + Interval: 5 * time.Second, + PeakSamples: 16, + TotalSamples: 4, // 1 sample per query * 4 steps + TotalSamplesPerStep: stats.TotalSamplesPerStep{ + 201000: 1, + 206000: 1, + 211000: 1, + 216000: 1, + }, + }, { Query: `max_over_time(metricWith3SampleEvery10Seconds{a="1"}[10s])`, Start: time.Unix(991, 0), From 4e77e8e5ef6e32027375732f8ce70d9702c42ea9 Mon Sep 17 00:00:00 2001 From: Charles Korn Date: Wed, 6 Mar 2024 14:54:33 +1100 Subject: [PATCH 015/127] Allow using alternative PromQL engines for rule evaluation Signed-off-by: Charles Korn --- promql/engine.go | 7 +++++++ rules/manager.go | 2 +- web/api/v1/api.go | 11 ++--------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 68e0502907..0d37f184ee 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -115,6 +115,13 @@ func (e ErrStorage) Error() string { return e.Err.Error() } +// QueryEngine defines the interface for the *promql.Engine, so it can be replaced, wrapped or mocked. +type QueryEngine interface { + SetQueryLogger(l QueryLogger) + NewInstantQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, ts time.Time) (Query, error) + NewRangeQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, start, end time.Time, interval time.Duration) (Query, error) +} + // QueryLogger is an interface that can be used to log all the queries logged // by the engine. type QueryLogger interface { diff --git a/rules/manager.go b/rules/manager.go index e87d55b1e1..165dca144e 100644 --- a/rules/manager.go +++ b/rules/manager.go @@ -43,7 +43,7 @@ type QueryFunc func(ctx context.Context, q string, t time.Time) (promql.Vector, // EngineQueryFunc returns a new query function that executes instant queries against // the given engine. // It converts scalar into vector results. -func EngineQueryFunc(engine *promql.Engine, q storage.Queryable) QueryFunc { +func EngineQueryFunc(engine promql.QueryEngine, q storage.Queryable) QueryFunc { return func(ctx context.Context, qs string, t time.Time) (promql.Vector, error) { q, err := engine.NewInstantQuery(ctx, q, nil, qs, t) if err != nil { diff --git a/web/api/v1/api.go b/web/api/v1/api.go index 48938fce12..b56026e45e 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -177,13 +177,6 @@ type TSDBAdminStats interface { WALReplayStatus() (tsdb.WALReplayStatus, error) } -// QueryEngine defines the interface for the *promql.Engine, so it can be replaced, wrapped or mocked. -type QueryEngine interface { - SetQueryLogger(l promql.QueryLogger) - NewInstantQuery(ctx context.Context, q storage.Queryable, opts promql.QueryOpts, qs string, ts time.Time) (promql.Query, error) - NewRangeQuery(ctx context.Context, q storage.Queryable, opts promql.QueryOpts, qs string, start, end time.Time, interval time.Duration) (promql.Query, error) -} - type QueryOpts interface { EnablePerStepStats() bool LookbackDelta() time.Duration @@ -193,7 +186,7 @@ type QueryOpts interface { // them using the provided storage and query engine. type API struct { Queryable storage.SampleAndChunkQueryable - QueryEngine QueryEngine + QueryEngine promql.QueryEngine ExemplarQueryable storage.ExemplarQueryable scrapePoolsRetriever func(context.Context) ScrapePoolsRetriever @@ -226,7 +219,7 @@ type API struct { // NewAPI returns an initialized API type. func NewAPI( - qe QueryEngine, + qe promql.QueryEngine, q storage.SampleAndChunkQueryable, ap storage.Appendable, eq storage.ExemplarQueryable, From e54082a62143952f0ab717f5387e9f3ecd15aba2 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 8 Mar 2024 10:37:28 +0000 Subject: [PATCH 016/127] CI: don't run race-detector on tests with previous Go version The purpose of running with a previous Go version is to spot usage of new language features; we don't need to intensively look for bugs. Signed-off-by: Bryan Boreham --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8251f960e9..2e4b628699 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,8 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - run: make build - - run: make test GO_ONLY=1 + # Don't run NPM build; don't run race-detector. + - run: make test GO_ONLY=1 test-flags="" test_ui: name: UI tests From eea6ab1cdd24ec69c94ba4b0d165030c89860c8b Mon Sep 17 00:00:00 2001 From: michaelact <86778470+michaelact@users.noreply.github.com> Date: Fri, 8 Mar 2024 22:19:39 +0700 Subject: [PATCH 017/127] [BUGFIX] Azure SD: Fix 'error: parameter virtualMachineScaleSetName cannot be empty' (#13702) Erroneous code was introduced during a merge-back-to-main at #13399. Signed-off-by: michaelact <86778470+michaelact@users.noreply.github.com> --- discovery/azure/azure.go | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/discovery/azure/azure.go b/discovery/azure/azure.go index a5d81f4ff6..16628c7bfd 100644 --- a/discovery/azure/azure.go +++ b/discovery/azure/azure.go @@ -413,26 +413,20 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { } else { networkInterface, err = client.getVMScaleSetVMNetworkInterfaceByID(ctx, nicID, vm.ScaleSet, vm.InstanceID) } + if err != nil { if errors.Is(err, errorNotFound) { level.Warn(d.logger).Log("msg", "Network interface does not exist", "name", nicID, "err", err) } else { ch <- target{labelSet: nil, err: err} } - d.addToCache(nicID, networkInterface) - } else { - networkInterface, err = client.getVMScaleSetVMNetworkInterfaceByID(ctx, nicID, vm.ScaleSet, vm.InstanceID) - if err != nil { - if errors.Is(err, errorNotFound) { - level.Warn(d.logger).Log("msg", "Network interface does not exist", "name", nicID, "err", err) - } else { - ch <- target{labelSet: nil, err: err} - } - // Get out of this routine because we cannot continue without a network interface. - return - } - d.addToCache(nicID, networkInterface) + + // Get out of this routine because we cannot continue without a network interface. + return } + + // Continue processing with the network interface + d.addToCache(nicID, networkInterface) } if networkInterface.Properties == nil { From 856f6e49c886f99f7cc0417147fb8d7464d45394 Mon Sep 17 00:00:00 2001 From: carrychair Date: Sat, 9 Mar 2024 17:53:17 +0800 Subject: [PATCH 018/127] fix function and struct name Signed-off-by: carrychair --- promql/engine.go | 2 +- tsdb/index/index.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 68e0502907..5146aae6d6 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1856,7 +1856,7 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, annotations.Annotatio panic(fmt.Errorf("unhandled expression of type: %T", expr)) } -// reuseOrGetFPointSlices reuses the space from previous slice to create new slice if the former has lots of room. +// reuseOrGetHPointSlices reuses the space from previous slice to create new slice if the former has lots of room. // The previous slices capacity is adjusted so when it is re-used from the pool it doesn't overflow into the new one. func reuseOrGetHPointSlices(prevSS *Series, numSteps int) (r []HPoint) { if prevSS != nil && cap(prevSS.Histograms)-2*len(prevSS.Histograms) > 0 { diff --git a/tsdb/index/index.go b/tsdb/index/index.go index 7aae4c2645..7ab890b99e 100644 --- a/tsdb/index/index.go +++ b/tsdb/index/index.go @@ -1829,7 +1829,7 @@ func NewStringListIter(s []string) StringIter { return &stringListIter{l: s} } -// symbolsIter implements StringIter. +// stringListIter implements StringIter. type stringListIter struct { l []string cur string From 1cccdbaedba18d76dded87a73be1032c4b950c85 Mon Sep 17 00:00:00 2001 From: guoguangwu Date: Tue, 12 Mar 2024 17:19:50 +0800 Subject: [PATCH 019/127] chore: use constant instead of numeric literal Signed-off-by: guoguangwu --- web/federate_test.go | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/web/federate_test.go b/web/federate_test.go index b8749dfa32..16637f60a3 100644 --- a/web/federate_test.go +++ b/web/federate_test.go @@ -48,29 +48,29 @@ var scenarios = map[string]struct { }{ "empty": { params: "", - code: 200, + code: http.StatusOK, body: ``, }, "match nothing": { params: "match[]=does_not_match_anything", - code: 200, + code: http.StatusOK, body: ``, }, "invalid params from the beginning": { params: "match[]=-not-a-valid-metric-name", - code: 400, + code: http.StatusBadRequest, body: `1:1: parse error: unexpected `, }, "invalid params somewhere in the middle": { params: "match[]=not-a-valid-metric-name", - code: 400, + code: http.StatusBadRequest, body: `1:4: parse error: unexpected `, }, "test_metric1": { params: "match[]=test_metric1", - code: 200, + code: http.StatusOK, body: `# TYPE test_metric1 untyped test_metric1{foo="bar",instance="i"} 10000 6000000 test_metric1{foo="boo",instance="i"} 1 6000000 @@ -78,33 +78,33 @@ test_metric1{foo="boo",instance="i"} 1 6000000 }, "test_metric2": { params: "match[]=test_metric2", - code: 200, + code: http.StatusOK, body: `# TYPE test_metric2 untyped test_metric2{foo="boo",instance="i"} 1 6000000 `, }, "test_metric_without_labels": { params: "match[]=test_metric_without_labels", - code: 200, + code: http.StatusOK, body: `# TYPE test_metric_without_labels untyped test_metric_without_labels{instance=""} 1001 6000000 `, }, "test_stale_metric": { params: "match[]=test_metric_stale", - code: 200, + code: http.StatusOK, body: ``, }, "test_old_metric": { params: "match[]=test_metric_old", - code: 200, + code: http.StatusOK, body: `# TYPE test_metric_old untyped test_metric_old{instance=""} 981 5880000 `, }, "{foo='boo'}": { params: "match[]={foo='boo'}", - code: 200, + code: http.StatusOK, body: `# TYPE test_metric1 untyped test_metric1{foo="boo",instance="i"} 1 6000000 # TYPE test_metric2 untyped @@ -113,7 +113,7 @@ test_metric2{foo="boo",instance="i"} 1 6000000 }, "two matchers": { params: "match[]=test_metric1&match[]=test_metric2", - code: 200, + code: http.StatusOK, body: `# TYPE test_metric1 untyped test_metric1{foo="bar",instance="i"} 10000 6000000 test_metric1{foo="boo",instance="i"} 1 6000000 @@ -123,7 +123,7 @@ test_metric2{foo="boo",instance="i"} 1 6000000 }, "two matchers with overlap": { params: "match[]={__name__=~'test_metric1'}&match[]={foo='bar'}", - code: 200, + code: http.StatusOK, body: `# TYPE test_metric1 untyped test_metric1{foo="bar",instance="i"} 10000 6000000 test_metric1{foo="boo",instance="i"} 1 6000000 @@ -131,7 +131,7 @@ test_metric1{foo="boo",instance="i"} 1 6000000 }, "everything": { params: "match[]={__name__=~'.%2b'}", // '%2b' is an URL-encoded '+'. - code: 200, + code: http.StatusOK, body: `# TYPE test_metric1 untyped test_metric1{foo="bar",instance="i"} 10000 6000000 test_metric1{foo="boo",instance="i"} 1 6000000 @@ -145,7 +145,7 @@ test_metric_without_labels{instance=""} 1001 6000000 }, "empty label value matches everything that doesn't have that label": { params: "match[]={foo='',__name__=~'.%2b'}", - code: 200, + code: http.StatusOK, body: `# TYPE test_metric_old untyped test_metric_old{instance=""} 981 5880000 # TYPE test_metric_without_labels untyped @@ -154,7 +154,7 @@ test_metric_without_labels{instance=""} 1001 6000000 }, "empty label value for a label that doesn't exist at all, matches everything": { params: "match[]={bar='',__name__=~'.%2b'}", - code: 200, + code: http.StatusOK, body: `# TYPE test_metric1 untyped test_metric1{foo="bar",instance="i"} 10000 6000000 test_metric1{foo="boo",instance="i"} 1 6000000 @@ -169,7 +169,7 @@ test_metric_without_labels{instance=""} 1001 6000000 "external labels are added if not already present": { params: "match[]={__name__=~'.%2b'}", // '%2b' is an URL-encoded '+'. externalLabels: labels.FromStrings("foo", "baz", "zone", "ie"), - code: 200, + code: http.StatusOK, body: `# TYPE test_metric1 untyped test_metric1{foo="bar",instance="i",zone="ie"} 10000 6000000 test_metric1{foo="boo",instance="i",zone="ie"} 1 6000000 @@ -186,7 +186,7 @@ test_metric_without_labels{foo="baz",instance="",zone="ie"} 1001 6000000 // know what it does anyway. params: "match[]={__name__=~'.%2b'}", // '%2b' is an URL-encoded '+'. externalLabels: labels.FromStrings("instance", "baz"), - code: 200, + code: http.StatusOK, body: `# TYPE test_metric1 untyped test_metric1{foo="bar",instance="i"} 10000 6000000 test_metric1{foo="boo",instance="i"} 1 6000000 @@ -390,7 +390,6 @@ func TestFederationWithNativeHistograms(t *testing.T) { require.Equal(t, http.StatusOK, res.Code) body, err := io.ReadAll(res.Body) require.NoError(t, err) - p := textparse.NewProtobufParser(body, false, labels.NewSymbolTable()) var actVec promql.Vector metricFamilies := 0 From 26262a1eb7f2d1d9ffae6df946402f5c2c896667 Mon Sep 17 00:00:00 2001 From: Charles Korn Date: Tue, 12 Mar 2024 10:16:39 +1100 Subject: [PATCH 020/127] Remove unnecessary `SetQueryLogger` method on `QueryEngine` interface Signed-off-by: Charles Korn --- promql/engine.go | 1 - web/api/v1/api_test.go | 2 -- 2 files changed, 3 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 0d37f184ee..93fb09254a 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -117,7 +117,6 @@ func (e ErrStorage) Error() string { // QueryEngine defines the interface for the *promql.Engine, so it can be replaced, wrapped or mocked. type QueryEngine interface { - SetQueryLogger(l QueryLogger) NewInstantQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, ts time.Time) (Query, error) NewRangeQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, start, end time.Time, interval time.Duration) (Query, error) } diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index eae4b9c64b..aa18d9b1dd 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -3884,8 +3884,6 @@ type fakeEngine struct { query fakeQuery } -func (e *fakeEngine) SetQueryLogger(promql.QueryLogger) {} - func (e *fakeEngine) NewInstantQuery(ctx context.Context, q storage.Queryable, opts promql.QueryOpts, qs string, ts time.Time) (promql.Query, error) { return &e.query, nil } From d08f0549506714d417b667fb5af75b7471bbd4d4 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 12 Mar 2024 11:24:27 +0000 Subject: [PATCH 021/127] [ENHANCEMENT] TSDB: Check CRC without allocating (#13742) Use the existing utility function which does this. Signed-off-by: Bryan Boreham --- tsdb/chunks/head_chunks.go | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/tsdb/chunks/head_chunks.go b/tsdb/chunks/head_chunks.go index 5ba5381320..087f25fbb3 100644 --- a/tsdb/chunks/head_chunks.go +++ b/tsdb/chunks/head_chunks.go @@ -15,7 +15,6 @@ package chunks import ( "bufio" - "bytes" "encoding/binary" "errors" "fmt" @@ -690,7 +689,6 @@ func (cdm *ChunkDiskMapper) Chunk(ref ChunkDiskMapperRef) (chunkenc.Chunk, error sgmIndex, chkStart := ref.Unpack() // We skip the series ref and the mint/maxt beforehand. chkStart += SeriesRefSize + (2 * MintMaxtSize) - chkCRC32 := newCRC32() // If it is the current open file, then the chunks can be in the buffer too. if sgmIndex == cdm.curFileSequence { @@ -755,20 +753,13 @@ func (cdm *ChunkDiskMapper) Chunk(ref ChunkDiskMapperRef) (chunkenc.Chunk, error // Check the CRC. sum := mmapFile.byteSlice.Range(chkDataEnd, chkDataEnd+CRCSize) - if _, err := chkCRC32.Write(mmapFile.byteSlice.Range(chkStart-(SeriesRefSize+2*MintMaxtSize), chkDataEnd)); err != nil { + if err := checkCRC32(mmapFile.byteSlice.Range(chkStart-(SeriesRefSize+2*MintMaxtSize), chkDataEnd), sum); err != nil { return nil, &CorruptionErr{ Dir: cdm.dir.Name(), FileIndex: sgmIndex, Err: err, } } - if act := chkCRC32.Sum(nil); !bytes.Equal(act, sum) { - return nil, &CorruptionErr{ - Dir: cdm.dir.Name(), - FileIndex: sgmIndex, - Err: fmt.Errorf("checksum mismatch expected:%x, actual:%x", sum, act), - } - } // The chunk data itself. chkData := mmapFile.byteSlice.Range(chkDataEnd-int(chkDataLen), chkDataEnd) @@ -802,8 +793,6 @@ func (cdm *ChunkDiskMapper) IterateAllChunks(f func(seriesRef HeadSeriesRef, chu cdm.fileMaxtSet = true }() - chkCRC32 := newCRC32() - // Iterate files in ascending order. segIDs := make([]int, 0, len(cdm.mmappedChunkFiles)) for seg := range cdm.mmappedChunkFiles { @@ -838,7 +827,6 @@ func (cdm *ChunkDiskMapper) IterateAllChunks(f func(seriesRef HeadSeriesRef, chu " - required:%v, available:%v, file:%d", idx+MaxHeadChunkMetaSize, fileEnd, segID), } } - chkCRC32.Reset() chunkRef := newChunkDiskMapperRef(uint64(segID), uint64(idx)) startIdx := idx @@ -877,14 +865,11 @@ func (cdm *ChunkDiskMapper) IterateAllChunks(f func(seriesRef HeadSeriesRef, chu // Check CRC. sum := mmapFile.byteSlice.Range(idx, idx+CRCSize) - if _, err := chkCRC32.Write(mmapFile.byteSlice.Range(startIdx, idx)); err != nil { - return err - } - if act := chkCRC32.Sum(nil); !bytes.Equal(act, sum) { + if err := checkCRC32(mmapFile.byteSlice.Range(startIdx, idx), sum); err != nil { return &CorruptionErr{ Dir: cdm.dir.Name(), FileIndex: segID, - Err: fmt.Errorf("checksum mismatch expected:%x, actual:%x", sum, act), + Err: err, } } idx += CRCSize From 0bb55883865fb87efb37760768d03d7576dc6547 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 12 Mar 2024 11:34:03 +0000 Subject: [PATCH 022/127] labels: optimize String method (#13673) Use a stack buffer to reduce memory allocations. `Write(AppendQuote(AvailableBuffer` does not allocate or copy when the buffer has sufficient space. Also add a benchmark, with some refactoring. Signed-off-by: Bryan Boreham --- model/labels/labels_common.go | 5 ++-- model/labels/labels_test.go | 46 ++++++++++++++++------------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/model/labels/labels_common.go b/model/labels/labels_common.go index 4c4a87e872..f46321c97e 100644 --- a/model/labels/labels_common.go +++ b/model/labels/labels_common.go @@ -39,7 +39,8 @@ type Label struct { } func (ls Labels) String() string { - var b bytes.Buffer + var bytea [1024]byte // On stack to avoid memory allocation while building the output. + b := bytes.NewBuffer(bytea[:0]) b.WriteByte('{') i := 0 @@ -50,7 +51,7 @@ func (ls Labels) String() string { } b.WriteString(l.Name) b.WriteByte('=') - b.WriteString(strconv.Quote(l.Value)) + b.Write(strconv.AppendQuote(b.AvailableBuffer(), l.Value)) i++ }) b.WriteByte('}') diff --git a/model/labels/labels_test.go b/model/labels/labels_test.go index c2ac6d63a0..49b4b4e67b 100644 --- a/model/labels/labels_test.go +++ b/model/labels/labels_test.go @@ -43,6 +43,13 @@ func TestLabels_String(t *testing.T) { } } +func BenchmarkString(b *testing.B) { + ls := New(benchmarkLabels...) + for i := 0; i < b.N; i++ { + _ = ls.String() + } +} + func TestLabels_MatchLabels(t *testing.T) { labels := FromStrings( "__name__", "ALERTS", @@ -785,24 +792,24 @@ func BenchmarkLabels_Hash(b *testing.B) { } } -func BenchmarkBuilder(b *testing.B) { - m := []Label{ - {"job", "node"}, - {"instance", "123.123.1.211:9090"}, - {"path", "/api/v1/namespaces//deployments/"}, - {"method", "GET"}, - {"namespace", "system"}, - {"status", "500"}, - {"prometheus", "prometheus-core-1"}, - {"datacenter", "eu-west-1"}, - {"pod_name", "abcdef-99999-defee"}, - } +var benchmarkLabels = []Label{ + {"job", "node"}, + {"instance", "123.123.1.211:9090"}, + {"path", "/api/v1/namespaces//deployments/"}, + {"method", "GET"}, + {"namespace", "system"}, + {"status", "500"}, + {"prometheus", "prometheus-core-1"}, + {"datacenter", "eu-west-1"}, + {"pod_name", "abcdef-99999-defee"}, +} +func BenchmarkBuilder(b *testing.B) { var l Labels builder := NewBuilder(EmptyLabels()) for i := 0; i < b.N; i++ { builder.Reset(EmptyLabels()) - for _, l := range m { + for _, l := range benchmarkLabels { builder.Set(l.Name, l.Value) } l = builder.Labels() @@ -811,18 +818,7 @@ func BenchmarkBuilder(b *testing.B) { } func BenchmarkLabels_Copy(b *testing.B) { - m := map[string]string{ - "job": "node", - "instance": "123.123.1.211:9090", - "path": "/api/v1/namespaces//deployments/", - "method": "GET", - "namespace": "system", - "status": "500", - "prometheus": "prometheus-core-1", - "datacenter": "eu-west-1", - "pod_name": "abcdef-99999-defee", - } - l := FromMap(m) + l := New(benchmarkLabels...) for i := 0; i < b.N; i++ { l = l.Copy() From 2061eb0a6aee30f959f87321bf9dfa8c8efe177e Mon Sep 17 00:00:00 2001 From: SuperQ Date: Tue, 12 Mar 2024 14:13:10 +0100 Subject: [PATCH 023/127] Add GitHub action to publish container README Add a GitHub action to publish the README.md to Docker Hub and Quay.io. Fixes: https://github.com/prometheus/prometheus/issues/5348 Signed-off-by: SuperQ --- .github/workflows/container_description.yml | 53 +++++++++++++++++++++ Makefile.common | 4 ++ 2 files changed, 57 insertions(+) create mode 100644 .github/workflows/container_description.yml diff --git a/.github/workflows/container_description.yml b/.github/workflows/container_description.yml new file mode 100644 index 0000000000..c48ee28dd8 --- /dev/null +++ b/.github/workflows/container_description.yml @@ -0,0 +1,53 @@ +--- +name: Push README to Docker Hub +on: + push: + paths: + - "README.md" + - ".github/workflows/container_description.yml" + branches: [ main, master ] + +permissions: + contents: read + +jobs: + PushDockerHubReadme: + runs-on: ubuntu-latest + name: Push README to Docker Hub + steps: + - name: git checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Set docker hub repo name. + run: echo "DOCKER_REPO_NAME=$(make common-docker-repo-name)" >> $GITHUB_ENV + - name: push README to Dockerhub + uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 + env: + DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }} + DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }} + with: + destination_container_repo: ${{ env.DOCKER_REPO_NAME }} + provider: dockerhub + short_description: ${{ env.DOCKER_REPO_NAME }} + readme_file: 'README.md' + + PushQuayIoReadme: + runs-on: ubuntu-latest + name: Push README to Docker Hub + steps: + - name: git checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Set quay.io org name. + run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV + - name: Set quay.io repo name. + run: echo "DOCKER_REPO_NAME=$(make common-docker-repo-name)" >> $GITHUB_ENV + - name: push README to Dockerhub + uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 + env: + DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }} + with: + destination_container_repo: ${{ env.DOCKER_REPO_NAME }} + provider: quay + readme_file: 'README.md' + + + diff --git a/Makefile.common b/Makefile.common index 92558151e3..49ed5f5478 100644 --- a/Makefile.common +++ b/Makefile.common @@ -208,6 +208,10 @@ common-tarball: promu @echo ">> building release tarball" $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) +.PHONY: common-docker-repo-name +common-docker-repo-name: + @echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)" + .PHONY: common-docker $(BUILD_DOCKER_ARCHS) common-docker: $(BUILD_DOCKER_ARCHS) $(BUILD_DOCKER_ARCHS): common-docker-%: From b8d428b753981db46c7b896709bdc2d09d06dc03 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 8 Mar 2024 14:07:57 +0000 Subject: [PATCH 024/127] Refactor: Azure Discovery: extract function to generate labelSet This should make it easier to test. Signed-off-by: Bryan Boreham --- discovery/azure/azure.go | 181 ++++++++++++++++++++------------------- 1 file changed, 91 insertions(+), 90 deletions(-) diff --git a/discovery/azure/azure.go b/discovery/azure/azure.go index 16628c7bfd..8936a33adb 100644 --- a/discovery/azure/azure.go +++ b/discovery/azure/azure.go @@ -374,96 +374,8 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { for _, vm := range machines { go func(vm virtualMachine) { defer wg.Done() - r, err := newAzureResourceFromID(vm.ID, d.logger) - if err != nil { - ch <- target{labelSet: nil, err: err} - return - } - - labels := model.LabelSet{ - azureLabelSubscriptionID: model.LabelValue(d.cfg.SubscriptionID), - azureLabelTenantID: model.LabelValue(d.cfg.TenantID), - azureLabelMachineID: model.LabelValue(vm.ID), - azureLabelMachineName: model.LabelValue(vm.Name), - azureLabelMachineComputerName: model.LabelValue(vm.ComputerName), - azureLabelMachineOSType: model.LabelValue(vm.OsType), - azureLabelMachineLocation: model.LabelValue(vm.Location), - azureLabelMachineResourceGroup: model.LabelValue(r.ResourceGroupName), - azureLabelMachineSize: model.LabelValue(vm.Size), - } - - if vm.ScaleSet != "" { - labels[azureLabelMachineScaleSet] = model.LabelValue(vm.ScaleSet) - } - - for k, v := range vm.Tags { - name := strutil.SanitizeLabelName(k) - labels[azureLabelMachineTag+model.LabelName(name)] = model.LabelValue(*v) - } - - // Get the IP address information via separate call to the network provider. - for _, nicID := range vm.NetworkInterfaces { - var networkInterface *armnetwork.Interface - if v, ok := d.getFromCache(nicID); ok { - networkInterface = v - d.metrics.cacheHitCount.Add(1) - } else { - if vm.ScaleSet == "" { - networkInterface, err = client.getVMNetworkInterfaceByID(ctx, nicID) - } else { - networkInterface, err = client.getVMScaleSetVMNetworkInterfaceByID(ctx, nicID, vm.ScaleSet, vm.InstanceID) - } - - if err != nil { - if errors.Is(err, errorNotFound) { - level.Warn(d.logger).Log("msg", "Network interface does not exist", "name", nicID, "err", err) - } else { - ch <- target{labelSet: nil, err: err} - } - - // Get out of this routine because we cannot continue without a network interface. - return - } - - // Continue processing with the network interface - d.addToCache(nicID, networkInterface) - } - - if networkInterface.Properties == nil { - continue - } - - // Unfortunately Azure does not return information on whether a VM is deallocated. - // This information is available via another API call however the Go SDK does not - // yet support this. On deallocated machines, this value happens to be nil so it - // is a cheap and easy way to determine if a machine is allocated or not. - if networkInterface.Properties.Primary == nil { - level.Debug(d.logger).Log("msg", "Skipping deallocated virtual machine", "machine", vm.Name) - return - } - - if *networkInterface.Properties.Primary { - for _, ip := range networkInterface.Properties.IPConfigurations { - // IPAddress is a field defined in PublicIPAddressPropertiesFormat, - // therefore we need to validate that both are not nil. - if ip.Properties != nil && ip.Properties.PublicIPAddress != nil && ip.Properties.PublicIPAddress.Properties != nil && ip.Properties.PublicIPAddress.Properties.IPAddress != nil { - labels[azureLabelMachinePublicIP] = model.LabelValue(*ip.Properties.PublicIPAddress.Properties.IPAddress) - } - if ip.Properties != nil && ip.Properties.PrivateIPAddress != nil { - labels[azureLabelMachinePrivateIP] = model.LabelValue(*ip.Properties.PrivateIPAddress) - address := net.JoinHostPort(*ip.Properties.PrivateIPAddress, fmt.Sprintf("%d", d.port)) - labels[model.AddressLabel] = model.LabelValue(address) - ch <- target{labelSet: labels, err: nil} - return - } - // If we made it here, we don't have a private IP which should be impossible. - // Return an empty target and error to ensure an all or nothing situation. - err = fmt.Errorf("unable to find a private IP for VM %s", vm.Name) - ch <- target{labelSet: nil, err: err} - return - } - } - } + labelSet, err := d.vmToLabelSet(ctx, client, vm) + ch <- target{labelSet: labelSet, err: err} }(vm) } @@ -484,6 +396,95 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { return []*targetgroup.Group{&tg}, nil } +func (d *Discovery) vmToLabelSet(ctx context.Context, client azureClient, vm virtualMachine) (model.LabelSet, error) { + r, err := newAzureResourceFromID(vm.ID, d.logger) + if err != nil { + return nil, err + } + + labels := model.LabelSet{ + azureLabelSubscriptionID: model.LabelValue(d.cfg.SubscriptionID), + azureLabelTenantID: model.LabelValue(d.cfg.TenantID), + azureLabelMachineID: model.LabelValue(vm.ID), + azureLabelMachineName: model.LabelValue(vm.Name), + azureLabelMachineComputerName: model.LabelValue(vm.ComputerName), + azureLabelMachineOSType: model.LabelValue(vm.OsType), + azureLabelMachineLocation: model.LabelValue(vm.Location), + azureLabelMachineResourceGroup: model.LabelValue(r.ResourceGroupName), + azureLabelMachineSize: model.LabelValue(vm.Size), + } + + if vm.ScaleSet != "" { + labels[azureLabelMachineScaleSet] = model.LabelValue(vm.ScaleSet) + } + + for k, v := range vm.Tags { + name := strutil.SanitizeLabelName(k) + labels[azureLabelMachineTag+model.LabelName(name)] = model.LabelValue(*v) + } + + // Get the IP address information via separate call to the network provider. + for _, nicID := range vm.NetworkInterfaces { + var networkInterface *armnetwork.Interface + if v, ok := d.getFromCache(nicID); ok { + networkInterface = v + d.metrics.cacheHitCount.Add(1) + } else { + if vm.ScaleSet == "" { + networkInterface, err = client.getVMNetworkInterfaceByID(ctx, nicID) + } else { + networkInterface, err = client.getVMScaleSetVMNetworkInterfaceByID(ctx, nicID, vm.ScaleSet, vm.InstanceID) + } + if err != nil { + if errors.Is(err, errorNotFound) { + level.Warn(d.logger).Log("msg", "Network interface does not exist", "name", nicID, "err", err) + } else { + return nil, err + } + // Get out of this routine because we cannot continue without a network interface. + return nil, nil + } + + // Continue processing with the network interface + d.addToCache(nicID, networkInterface) + } + + if networkInterface.Properties == nil { + continue + } + + // Unfortunately Azure does not return information on whether a VM is deallocated. + // This information is available via another API call however the Go SDK does not + // yet support this. On deallocated machines, this value happens to be nil so it + // is a cheap and easy way to determine if a machine is allocated or not. + if networkInterface.Properties.Primary == nil { + level.Debug(d.logger).Log("msg", "Skipping deallocated virtual machine", "machine", vm.Name) + return nil, nil + } + + if *networkInterface.Properties.Primary { + for _, ip := range networkInterface.Properties.IPConfigurations { + // IPAddress is a field defined in PublicIPAddressPropertiesFormat, + // therefore we need to validate that both are not nil. + if ip.Properties != nil && ip.Properties.PublicIPAddress != nil && ip.Properties.PublicIPAddress.Properties != nil && ip.Properties.PublicIPAddress.Properties.IPAddress != nil { + labels[azureLabelMachinePublicIP] = model.LabelValue(*ip.Properties.PublicIPAddress.Properties.IPAddress) + } + if ip.Properties != nil && ip.Properties.PrivateIPAddress != nil { + labels[azureLabelMachinePrivateIP] = model.LabelValue(*ip.Properties.PrivateIPAddress) + address := net.JoinHostPort(*ip.Properties.PrivateIPAddress, fmt.Sprintf("%d", d.port)) + labels[model.AddressLabel] = model.LabelValue(address) + return labels, nil + } + // If we made it here, we don't have a private IP which should be impossible. + // Return an empty target and error to ensure an all or nothing situation. + return nil, fmt.Errorf("unable to find a private IP for VM %s", vm.Name) + } + } + } + // TODO: Should we say something at this point? + return nil, nil +} + func (client *azureClient) getVMs(ctx context.Context, resourceGroup string) ([]virtualMachine, error) { var vms []virtualMachine if len(resourceGroup) == 0 { From 4e24e5b1d166842d6f2c604c81defd45223e4e37 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 8 Mar 2024 14:41:26 +0000 Subject: [PATCH 025/127] Refactor: Azure Discovery: introduce an interface for the client So we can mock it. Signed-off-by: Bryan Boreham --- discovery/azure/azure.go | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/discovery/azure/azure.go b/discovery/azure/azure.go index 8936a33adb..6bde835e5d 100644 --- a/discovery/azure/azure.go +++ b/discovery/azure/azure.go @@ -212,6 +212,14 @@ func NewDiscovery(cfg *SDConfig, logger log.Logger, metrics discovery.Discoverer return d, nil } +type client interface { + getVMs(ctx context.Context, resourceGroup string) ([]virtualMachine, error) + getScaleSets(ctx context.Context, resourceGroup string) ([]armcompute.VirtualMachineScaleSet, error) + getScaleSetVMs(ctx context.Context, scaleSet armcompute.VirtualMachineScaleSet) ([]virtualMachine, error) + getVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID string) (*armnetwork.Interface, error) + getVMScaleSetVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID, scaleSetName, instanceID string) (*armnetwork.Interface, error) +} + // azureClient represents multiple Azure Resource Manager providers. type azureClient struct { nic *armnetwork.InterfacesClient @@ -221,14 +229,17 @@ type azureClient struct { logger log.Logger } +var _ client = &azureClient{} + // createAzureClient is a helper function for creating an Azure compute client to ARM. -func createAzureClient(cfg SDConfig) (azureClient, error) { +func createAzureClient(cfg SDConfig, logger log.Logger) (client, error) { cloudConfiguration, err := CloudConfigurationFromName(cfg.Environment) if err != nil { - return azureClient{}, err + return &azureClient{}, err } var c azureClient + c.logger = logger telemetry := policy.TelemetryOptions{ ApplicationID: userAgent, @@ -239,12 +250,12 @@ func createAzureClient(cfg SDConfig) (azureClient, error) { Telemetry: telemetry, }) if err != nil { - return azureClient{}, err + return &azureClient{}, err } client, err := config_util.NewClientFromConfig(cfg.HTTPClientConfig, "azure_sd") if err != nil { - return azureClient{}, err + return &azureClient{}, err } options := &arm.ClientOptions{ ClientOptions: policy.ClientOptions{ @@ -256,25 +267,25 @@ func createAzureClient(cfg SDConfig) (azureClient, error) { c.vm, err = armcompute.NewVirtualMachinesClient(cfg.SubscriptionID, credential, options) if err != nil { - return azureClient{}, err + return &azureClient{}, err } c.nic, err = armnetwork.NewInterfacesClient(cfg.SubscriptionID, credential, options) if err != nil { - return azureClient{}, err + return &azureClient{}, err } c.vmss, err = armcompute.NewVirtualMachineScaleSetsClient(cfg.SubscriptionID, credential, options) if err != nil { - return azureClient{}, err + return &azureClient{}, err } c.vmssvm, err = armcompute.NewVirtualMachineScaleSetVMsClient(cfg.SubscriptionID, credential, options) if err != nil { - return azureClient{}, err + return &azureClient{}, err } - return c, nil + return &c, nil } func newCredential(cfg SDConfig, policyClientOptions policy.ClientOptions) (azcore.TokenCredential, error) { @@ -330,12 +341,11 @@ func newAzureResourceFromID(id string, logger log.Logger) (*arm.ResourceID, erro func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { defer level.Debug(d.logger).Log("msg", "Azure discovery completed") - client, err := createAzureClient(*d.cfg) + client, err := createAzureClient(*d.cfg, d.logger) if err != nil { d.metrics.failuresCount.Inc() return nil, fmt.Errorf("could not create Azure client: %w", err) } - client.logger = d.logger machines, err := client.getVMs(ctx, d.cfg.ResourceGroup) if err != nil { @@ -396,7 +406,7 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { return []*targetgroup.Group{&tg}, nil } -func (d *Discovery) vmToLabelSet(ctx context.Context, client azureClient, vm virtualMachine) (model.LabelSet, error) { +func (d *Discovery) vmToLabelSet(ctx context.Context, client client, vm virtualMachine) (model.LabelSet, error) { r, err := newAzureResourceFromID(vm.ID, d.logger) if err != nil { return nil, err From 5f2c0c5283c745652e109c6174b5d539eaf9430a Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 8 Mar 2024 14:59:31 +0000 Subject: [PATCH 026/127] Azure Discovery tests: mock the azure client interface Signed-off-by: Bryan Boreham --- discovery/azure/azure_test.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/discovery/azure/azure_test.go b/discovery/azure/azure_test.go index 1e437c75f2..0daaab86be 100644 --- a/discovery/azure/azure_test.go +++ b/discovery/azure/azure_test.go @@ -14,10 +14,13 @@ package azure import ( + "context" + "fmt" "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4" "github.com/stretchr/testify/require" "go.uber.org/goleak" ) @@ -381,3 +384,35 @@ func TestNewAzureResourceFromID(t *testing.T) { require.Equal(t, tc.expected.ResourceGroupName, actual.ResourceGroupName) } } + +type mockAzureClient struct { + networkInterface *armnetwork.Interface +} + +var _ client = &mockAzureClient{} + +func (*mockAzureClient) getVMs(ctx context.Context, resourceGroup string) ([]virtualMachine, error) { + return nil, nil +} + +func (*mockAzureClient) getScaleSets(ctx context.Context, resourceGroup string) ([]armcompute.VirtualMachineScaleSet, error) { + return nil, nil +} + +func (*mockAzureClient) getScaleSetVMs(ctx context.Context, scaleSet armcompute.VirtualMachineScaleSet) ([]virtualMachine, error) { + return nil, nil +} + +func (m *mockAzureClient) getVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID string) (*armnetwork.Interface, error) { + if networkInterfaceID == "" { + return nil, fmt.Errorf("parameter networkInterfaceID cannot be empty") + } + return m.networkInterface, nil +} + +func (m *mockAzureClient) getVMScaleSetVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID, scaleSetName, instanceID string) (*armnetwork.Interface, error) { + if scaleSetName == "" { + return nil, fmt.Errorf("parameter virtualMachineScaleSetName cannot be empty") + } + return m.networkInterface, nil +} From ab9c544ec7371c0f0b146e082781f539d3f52ab4 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 8 Mar 2024 15:12:41 +0000 Subject: [PATCH 027/127] Azure Discovery tests: Add test for VMToLabelSet Test fails due to bug in code on main. Signed-off-by: Bryan Boreham --- discovery/azure/azure_test.go | 92 ++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/discovery/azure/azure_test.go b/discovery/azure/azure_test.go index 0daaab86be..32dab66c8c 100644 --- a/discovery/azure/azure_test.go +++ b/discovery/azure/azure_test.go @@ -21,12 +21,17 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4" + cache "github.com/Code-Hex/go-generics-cache" + "github.com/Code-Hex/go-generics-cache/policy/lru" + "github.com/go-kit/log" "github.com/stretchr/testify/require" "go.uber.org/goleak" ) func TestMain(m *testing.M) { - goleak.VerifyTestMain(m) + goleak.VerifyTestMain(m, + goleak.IgnoreTopFunction("github.com/Code-Hex/go-generics-cache.(*janitor).run.func1"), + ) } func TestMapFromVMWithEmptyTags(t *testing.T) { @@ -82,6 +87,91 @@ func TestMapFromVMWithEmptyTags(t *testing.T) { require.Equal(t, expectedVM, actualVM) } +func TestVMToLabelSet(t *testing.T) { + id := "/subscriptions/00000000-0000-0000-0000-000000000000/test" + name := "name" + size := "size" + vmSize := armcompute.VirtualMachineSizeTypes(size) + osType := armcompute.OperatingSystemTypesLinux + vmType := "type" + location := "westeurope" + computerName := "computer_name" + networkID := "/subscriptions/00000000-0000-0000-0000-000000000000/network1" + ipAddress := "10.20.30.40" + primary := true + networkProfile := armcompute.NetworkProfile{ + NetworkInterfaces: []*armcompute.NetworkInterfaceReference{ + { + ID: &networkID, + Properties: &armcompute.NetworkInterfaceReferenceProperties{Primary: &primary}, + }, + }, + } + properties := &armcompute.VirtualMachineProperties{ + OSProfile: &armcompute.OSProfile{ + ComputerName: &computerName, + }, + StorageProfile: &armcompute.StorageProfile{ + OSDisk: &armcompute.OSDisk{ + OSType: &osType, + }, + }, + NetworkProfile: &networkProfile, + HardwareProfile: &armcompute.HardwareProfile{ + VMSize: &vmSize, + }, + } + + testVM := armcompute.VirtualMachine{ + ID: &id, + Name: &name, + Type: &vmType, + Location: &location, + Tags: nil, + Properties: properties, + } + + expectedVM := virtualMachine{ + ID: id, + Name: name, + ComputerName: computerName, + Type: vmType, + Location: location, + OsType: "Linux", + Tags: map[string]*string{}, + NetworkInterfaces: []string{networkID}, + Size: size, + } + + actualVM := mapFromVM(testVM) + + require.Equal(t, expectedVM, actualVM) + + cfg := DefaultSDConfig + d := &Discovery{ + cfg: &cfg, + logger: log.NewNopLogger(), + cache: cache.New(cache.AsLRU[string, *armnetwork.Interface](lru.WithCapacity(5))), + } + network := armnetwork.Interface{ + Name: &networkID, + Properties: &armnetwork.InterfacePropertiesFormat{ + Primary: &primary, + IPConfigurations: []*armnetwork.InterfaceIPConfiguration{ + {Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{ + PrivateIPAddress: &ipAddress, + }}, + }, + }, + } + client := &mockAzureClient{ + networkInterface: &network, + } + labelSet, err := d.vmToLabelSet(context.Background(), client, actualVM) + require.NoError(t, err) + require.Len(t, labelSet, 11) +} + func TestMapFromVMWithEmptyOSType(t *testing.T) { id := "test" name := "name" From cd3e0078f063e99c5ec723fdc487675dec775060 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 20:07:03 +0100 Subject: [PATCH 028/127] build(deps): bump github.com/prometheus/common (#13728) Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.49.0 to 0.50.0. - [Release notes](https://github.com/prometheus/common/releases) - [Commits](https://github.com/prometheus/common/compare/v0.49.0...v0.50.0) --- updated-dependencies: - dependency-name: github.com/prometheus/common dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- documentation/examples/remote_storage/go.mod | 12 ++++----- documentation/examples/remote_storage/go.sum | 28 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/documentation/examples/remote_storage/go.mod b/documentation/examples/remote_storage/go.mod index 38497cbf50..917563f00c 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.0 - github.com/prometheus/common v0.49.0 + github.com/prometheus/common v0.50.0 github.com/prometheus/prometheus v0.50.1 github.com/stretchr/testify v1.9.0 ) @@ -58,17 +58,17 @@ require ( go.opentelemetry.io/otel/trace v1.22.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.19.0 // indirect + golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect - golang.org/x/net v0.21.0 // indirect - golang.org/x/oauth2 v0.17.0 // indirect - golang.org/x/sys v0.17.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/oauth2 v0.18.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect google.golang.org/grpc v1.61.0 // indirect - google.golang.org/protobuf v1.32.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apimachinery v0.28.6 // indirect diff --git a/documentation/examples/remote_storage/go.sum b/documentation/examples/remote_storage/go.sum index 6ffa445b29..50db8d7934 100644 --- a/documentation/examples/remote_storage/go.sum +++ b/documentation/examples/remote_storage/go.sum @@ -269,8 +269,8 @@ 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.49.0 h1:ToNTdK4zSnPVJmh698mGFkDor9wBI/iGaJy5dbH1EgI= -github.com/prometheus/common v0.49.0/go.mod h1:Kxm+EULxRbUkjGU6WFsQqo3ORzB4tyKvlWFOE9mB2sE= +github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ= +github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ= 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= @@ -332,8 +332,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk 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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 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= @@ -356,12 +356,12 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 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.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= -golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= 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= @@ -389,12 +389,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 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= @@ -436,8 +436,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi 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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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 3bff79451d0389c8108592eefdb51ef1b4561cd6 Mon Sep 17 00:00:00 2001 From: SuperQ Date: Wed, 13 Mar 2024 14:28:05 +0100 Subject: [PATCH 029/127] Fix container_description workflow Fix yaml indentation. :facepalm: Signed-off-by: SuperQ --- .github/workflows/container_description.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/container_description.yml b/.github/workflows/container_description.yml index c48ee28dd8..c453f7c903 100644 --- a/.github/workflows/container_description.yml +++ b/.github/workflows/container_description.yml @@ -5,7 +5,7 @@ on: paths: - "README.md" - ".github/workflows/container_description.yml" - branches: [ main, master ] + branches: [ main, master ] permissions: contents: read From 505fd638beef7b0131678f1c98efcc36ce6f4982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C4=8Cajka?= Date: Tue, 12 Mar 2024 15:05:26 +0100 Subject: [PATCH 030/127] otlptranslator: fix up import paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jakub Čajka --- storage/remote/otlptranslator/README.md | 3 +-- storage/remote/otlptranslator/prometheus/normalize_label.go | 2 +- storage/remote/otlptranslator/prometheus/normalize_name.go | 2 +- storage/remote/otlptranslator/prometheus/unit_to_ucum.go | 2 +- storage/remote/otlptranslator/update-copy.sh | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/storage/remote/otlptranslator/README.md b/storage/remote/otlptranslator/README.md index c2b04e5aff..774fac5a7f 100644 --- a/storage/remote/otlptranslator/README.md +++ b/storage/remote/otlptranslator/README.md @@ -3,7 +3,6 @@ This files in the `prometheus/` and `prometheusremotewrite/` are copied from the OpenTelemetry Project[^1]. This is done instead of adding a go.mod dependency because OpenTelemetry depends on `prometheus/prometheus` and a cyclic dependency will be created. This is just a temporary solution and the long-term solution is to move the required packages from OpenTelemetry into `prometheus/prometheus`. -We don't copy in `./prometheus` through this script because that package imports a collector specific featuregate package we don't want to import. The featuregate package is being removed now, and in the future we will copy this folder too. To update the dependency is a multi-step process: 1. Vendor the latest `prometheus/prometheus`@`main` into [`opentelemetry/opentelemetry-collector-contrib`](https://github.com/open-telemetry/opentelemetry-collector-contrib) @@ -20,4 +19,4 @@ This means if we depend on the upstream packages directly, we will never able to When we do want to make changes to the types in `prompb`, we might need to edit the files directly. That is OK, please let @gouthamve or @jesusvazquez know so they can take care of updating the upstream code (by vendoring in `prometheus/prometheus` upstream and resolving conflicts) and then will run the copy script again to keep things updated. -[^1]: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/translator/prometheus and https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/translator/prometheusremotewrite \ No newline at end of file +[^1]: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/translator/prometheus and https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/translator/prometheusremotewrite diff --git a/storage/remote/otlptranslator/prometheus/normalize_label.go b/storage/remote/otlptranslator/prometheus/normalize_label.go index c02800c8bc..a6b41d1c37 100644 --- a/storage/remote/otlptranslator/prometheus/normalize_label.go +++ b/storage/remote/otlptranslator/prometheus/normalize_label.go @@ -3,7 +3,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package prometheus // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus" +package prometheus // import "github.com/prometheus/prometheus/storage/remote/otlptranslator/prometheus" import ( "strings" diff --git a/storage/remote/otlptranslator/prometheus/normalize_name.go b/storage/remote/otlptranslator/prometheus/normalize_name.go index 7ae233a187..a976dfb485 100644 --- a/storage/remote/otlptranslator/prometheus/normalize_name.go +++ b/storage/remote/otlptranslator/prometheus/normalize_name.go @@ -3,7 +3,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package prometheus // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus" +package prometheus // import "github.com/prometheus/prometheus/storage/remote/otlptranslator/prometheus" import ( "strings" diff --git a/storage/remote/otlptranslator/prometheus/unit_to_ucum.go b/storage/remote/otlptranslator/prometheus/unit_to_ucum.go index 4a72e683bb..718a520675 100644 --- a/storage/remote/otlptranslator/prometheus/unit_to_ucum.go +++ b/storage/remote/otlptranslator/prometheus/unit_to_ucum.go @@ -3,7 +3,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package prometheus // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus" +package prometheus // import "github.com/prometheus/prometheus/storage/remote/otlptranslator/prometheus" import "strings" diff --git a/storage/remote/otlptranslator/update-copy.sh b/storage/remote/otlptranslator/update-copy.sh index f90be5eedc..8aa645e0bd 100755 --- a/storage/remote/otlptranslator/update-copy.sh +++ b/storage/remote/otlptranslator/update-copy.sh @@ -23,5 +23,5 @@ case $(sed --help 2>&1) in *) set sed -i '';; esac -"$@" -e 's#github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus#github.com/prometheus/prometheus/storage/remote/otlptranslator/prometheus#g' ./prometheusremotewrite/*.go +"$@" -e 's#github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus#github.com/prometheus/prometheus/storage/remote/otlptranslator/prometheus#g' ./prometheusremotewrite/*.go ./prometheus/*.go "$@" -e '1s#^#// DO NOT EDIT. COPIED AS-IS. SEE ../README.md\n\n#g' ./prometheusremotewrite/*.go ./prometheus/*.go From 46401b988e9a4d372b9f039d7507dbce2aa684e2 Mon Sep 17 00:00:00 2001 From: SuperQ Date: Wed, 13 Mar 2024 15:56:37 +0100 Subject: [PATCH 031/127] Normalize and fixup step names. Signed-off-by: SuperQ --- .github/workflows/container_description.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/container_description.yml b/.github/workflows/container_description.yml index c453f7c903..eb505207cb 100644 --- a/.github/workflows/container_description.yml +++ b/.github/workflows/container_description.yml @@ -17,9 +17,9 @@ jobs: steps: - name: git checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Set docker hub repo name. + - name: Set docker hub repo name run: echo "DOCKER_REPO_NAME=$(make common-docker-repo-name)" >> $GITHUB_ENV - - name: push README to Dockerhub + - name: Push README to Dockerhub uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 env: DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }} @@ -36,11 +36,11 @@ jobs: steps: - name: git checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Set quay.io org name. + - name: Set quay.io org name run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV - - name: Set quay.io repo name. + - name: Set quay.io repo name run: echo "DOCKER_REPO_NAME=$(make common-docker-repo-name)" >> $GITHUB_ENV - - name: push README to Dockerhub + - name: Push README to quay.io uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 env: DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }} From 87edf1f960d2e3b59b0f82026fa3283e4e8f21e4 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Wed, 13 Mar 2024 15:57:23 +0000 Subject: [PATCH 032/127] [Cleanup] TSDB: Remove old deprecated WAL implementation Deprecated since 2018. Signed-off-by: Bryan Boreham --- tsdb/db.go | 4 - tsdb/db_test.go | 2 +- tsdb/wal.go | 1303 ---------------------------------------------- tsdb/wal_test.go | 553 -------------------- 4 files changed, 1 insertion(+), 1861 deletions(-) delete mode 100644 tsdb/wal.go delete mode 100644 tsdb/wal_test.go diff --git a/tsdb/db.go b/tsdb/db.go index 4998da6aa7..7aa68da500 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -779,10 +779,6 @@ func open(dir string, l log.Logger, r prometheus.Registerer, opts *Options, rngs walDir := filepath.Join(dir, "wal") wblDir := filepath.Join(dir, wlog.WblDirName) - // Migrate old WAL if one exists. - if err := MigrateWAL(l, walDir); err != nil { - return nil, fmt.Errorf("migrate WAL: %w", err) - } for _, tmpDir := range []string{walDir, dir} { // Remove tmp dirs. if err := removeBestEffortTmpDirs(l, tmpDir); err != nil { diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 45a16b8ba3..b1171b349a 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -3598,7 +3598,7 @@ func testChunkQuerierShouldNotPanicIfHeadChunkIsTruncatedWhileReadingQueriedChun // just to iterate through the bytes slice. We don't really care the reason why // we read this data, we just need to read it to make sure the memory address // of the []byte is still valid. - chkCRC32 := newCRC32() + chkCRC32 := crc32.New(crc32.MakeTable(crc32.Castagnoli)) for _, chunk := range chunks { chkCRC32.Reset() _, err := chkCRC32.Write(chunk.Bytes()) diff --git a/tsdb/wal.go b/tsdb/wal.go deleted file mode 100644 index e06a8aea53..0000000000 --- a/tsdb/wal.go +++ /dev/null @@ -1,1303 +0,0 @@ -// 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 ( - "bufio" - "encoding/binary" - "errors" - "fmt" - "hash" - "hash/crc32" - "io" - "math" - "os" - "path/filepath" - "sync" - "time" - - "github.com/go-kit/log" - "github.com/go-kit/log/level" - "github.com/prometheus/client_golang/prometheus" - - "github.com/prometheus/prometheus/model/labels" - "github.com/prometheus/prometheus/storage" - "github.com/prometheus/prometheus/tsdb/chunks" - "github.com/prometheus/prometheus/tsdb/encoding" - "github.com/prometheus/prometheus/tsdb/fileutil" - "github.com/prometheus/prometheus/tsdb/record" - "github.com/prometheus/prometheus/tsdb/tombstones" - "github.com/prometheus/prometheus/tsdb/wlog" - "github.com/prometheus/prometheus/util/zeropool" -) - -// WALEntryType indicates what data a WAL entry contains. -type WALEntryType uint8 - -const ( - // WALMagic is a 4 byte number every WAL segment file starts with. - WALMagic = uint32(0x43AF00EF) - - // WALFormatDefault is the version flag for the default outer segment file format. - WALFormatDefault = byte(1) -) - -// Entry types in a segment file. -const ( - WALEntrySymbols WALEntryType = 1 - WALEntrySeries WALEntryType = 2 - WALEntrySamples WALEntryType = 3 - WALEntryDeletes WALEntryType = 4 -) - -type walMetrics struct { - fsyncDuration prometheus.Summary - corruptions prometheus.Counter -} - -func newWalMetrics(r prometheus.Registerer) *walMetrics { - m := &walMetrics{} - - m.fsyncDuration = prometheus.NewSummary(prometheus.SummaryOpts{ - Name: "prometheus_tsdb_wal_fsync_duration_seconds", - Help: "Duration of WAL fsync.", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }) - m.corruptions = prometheus.NewCounter(prometheus.CounterOpts{ - Name: "prometheus_tsdb_wal_corruptions_total", - Help: "Total number of WAL corruptions.", - }) - - if r != nil { - r.MustRegister( - m.fsyncDuration, - m.corruptions, - ) - } - return m -} - -// WAL is a write ahead log that can log new series labels and samples. -// It must be completely read before new entries are logged. -// -// Deprecated: use wlog pkg combined with the record codex instead. -type WAL interface { - Reader() WALReader - LogSeries([]record.RefSeries) error - LogSamples([]record.RefSample) error - LogDeletes([]tombstones.Stone) error - Truncate(mint int64, keep func(uint64) bool) error - Close() error -} - -// WALReader reads entries from a WAL. -type WALReader interface { - Read( - seriesf func([]record.RefSeries), - samplesf func([]record.RefSample), - deletesf func([]tombstones.Stone), - ) error -} - -// segmentFile wraps a file object of a segment and tracks the highest timestamp -// it contains. During WAL truncating, all segments with no higher timestamp than -// the truncation threshold can be compacted. -type segmentFile struct { - *os.File - maxTime int64 // highest tombstone or sample timestamp in segment - minSeries chunks.HeadSeriesRef // lowerst series ID in segment -} - -func newSegmentFile(f *os.File) *segmentFile { - return &segmentFile{ - File: f, - maxTime: math.MinInt64, - minSeries: math.MaxUint64, - } -} - -const ( - walSegmentSizeBytes = 256 * 1024 * 1024 // 256 MB -) - -// The table gets initialized with sync.Once but may still cause a race -// with any other use of the crc32 package anywhere. Thus we initialize it -// before. -var castagnoliTable *crc32.Table - -func init() { - castagnoliTable = crc32.MakeTable(crc32.Castagnoli) -} - -// newCRC32 initializes a CRC32 hash with a preconfigured polynomial, so the -// polynomial may be easily changed in one location at a later time, if necessary. -func newCRC32() hash.Hash32 { - return crc32.New(castagnoliTable) -} - -// SegmentWAL is a write ahead log for series data. -// -// Deprecated: use wlog pkg combined with the record coders instead. -type SegmentWAL struct { - mtx sync.Mutex - metrics *walMetrics - - dirFile *os.File - files []*segmentFile - - logger log.Logger - flushInterval time.Duration - segmentSize int64 - - crc32 hash.Hash32 - cur *bufio.Writer - curN int64 - - stopc chan struct{} - donec chan struct{} - actorc chan func() error // sequentialized background operations - buffers sync.Pool -} - -// OpenSegmentWAL opens or creates a write ahead log in the given directory. -// The WAL must be read completely before new data is written. -func OpenSegmentWAL(dir string, logger log.Logger, flushInterval time.Duration, r prometheus.Registerer) (*SegmentWAL, error) { - if err := os.MkdirAll(dir, 0o777); err != nil { - return nil, err - } - df, err := fileutil.OpenDir(dir) - if err != nil { - return nil, err - } - if logger == nil { - logger = log.NewNopLogger() - } - - w := &SegmentWAL{ - dirFile: df, - logger: logger, - flushInterval: flushInterval, - donec: make(chan struct{}), - stopc: make(chan struct{}), - actorc: make(chan func() error, 2), - segmentSize: walSegmentSizeBytes, - crc32: newCRC32(), - } - w.metrics = newWalMetrics(r) - - fns, err := sequenceFiles(w.dirFile.Name()) - if err != nil { - return nil, err - } - - for i, fn := range fns { - f, err := w.openSegmentFile(fn) - if err == nil { - w.files = append(w.files, newSegmentFile(f)) - continue - } - level.Warn(logger).Log("msg", "Invalid segment file detected, truncating WAL", "err", err, "file", fn) - - for _, fn := range fns[i:] { - if err := os.Remove(fn); err != nil { - return w, fmt.Errorf("removing segment failed: %w", err) - } - } - break - } - - go w.run(flushInterval) - - return w, nil -} - -// repairingWALReader wraps a WAL reader and truncates its underlying SegmentWAL after the last -// valid entry if it encounters corruption. -type repairingWALReader struct { - wal *SegmentWAL - r WALReader -} - -func (r *repairingWALReader) Read( - seriesf func([]record.RefSeries), - samplesf func([]record.RefSample), - deletesf func([]tombstones.Stone), -) error { - err := r.r.Read(seriesf, samplesf, deletesf) - if err == nil { - return nil - } - var cerr *walCorruptionErr - if !errors.As(err, &cerr) { - return err - } - r.wal.metrics.corruptions.Inc() - return r.wal.truncate(cerr.err, cerr.file, cerr.lastOffset) -} - -// truncate the WAL after the last valid entry. -func (w *SegmentWAL) truncate(err error, file int, lastOffset int64) error { - level.Error(w.logger).Log("msg", "WAL corruption detected; truncating", - "err", err, "file", w.files[file].Name(), "pos", lastOffset) - - // Close and delete all files after the current one. - for _, f := range w.files[file+1:] { - if err := f.Close(); err != nil { - return err - } - if err := os.Remove(f.Name()); err != nil { - return err - } - } - w.mtx.Lock() - defer w.mtx.Unlock() - - w.files = w.files[:file+1] - - // Seek the current file to the last valid offset where we continue writing from. - _, err = w.files[file].Seek(lastOffset, io.SeekStart) - return err -} - -// Reader returns a new reader over the write ahead log data. -// It must be completely consumed before writing to the WAL. -func (w *SegmentWAL) Reader() WALReader { - return &repairingWALReader{ - wal: w, - r: newWALReader(w.files, w.logger), - } -} - -func (w *SegmentWAL) getBuffer() *encoding.Encbuf { - b := w.buffers.Get() - if b == nil { - return &encoding.Encbuf{B: make([]byte, 0, 64*1024)} - } - return b.(*encoding.Encbuf) -} - -func (w *SegmentWAL) putBuffer(b *encoding.Encbuf) { - b.Reset() - w.buffers.Put(b) -} - -// Truncate deletes the values prior to mint and the series which the keep function -// does not indicate to preserve. -func (w *SegmentWAL) Truncate(mint int64, keep func(chunks.HeadSeriesRef) bool) error { - // The last segment is always active. - if len(w.files) < 2 { - return nil - } - var candidates []*segmentFile - - // All files have to be traversed as there could be two segments for a block - // with first block having times (10000, 20000) and SECOND one having (0, 10000). - for _, sf := range w.files[:len(w.files)-1] { - if sf.maxTime >= mint { - break - } - // Past WAL files are closed. We have to reopen them for another read. - f, err := w.openSegmentFile(sf.Name()) - if err != nil { - return fmt.Errorf("open old WAL segment for read: %w", err) - } - candidates = append(candidates, &segmentFile{ - File: f, - minSeries: sf.minSeries, - maxTime: sf.maxTime, - }) - } - if len(candidates) == 0 { - return nil - } - - r := newWALReader(candidates, w.logger) - - // Create a new tmp file. - f, err := w.createSegmentFile(filepath.Join(w.dirFile.Name(), "compact.tmp")) - if err != nil { - return fmt.Errorf("create compaction segment: %w", err) - } - defer func() { - if err := os.RemoveAll(f.Name()); err != nil { - level.Error(w.logger).Log("msg", "remove tmp file", "err", err.Error()) - } - }() - - var ( - csf = newSegmentFile(f) - crc32 = newCRC32() - decSeries = []record.RefSeries{} - activeSeries = []record.RefSeries{} - ) - - for r.next() { - rt, flag, byt := r.at() - - if rt != WALEntrySeries { - continue - } - decSeries = decSeries[:0] - activeSeries = activeSeries[:0] - - err := r.decodeSeries(flag, byt, &decSeries) - if err != nil { - return fmt.Errorf("decode samples while truncating: %w", err) - } - for _, s := range decSeries { - if keep(s.Ref) { - activeSeries = append(activeSeries, s) - } - } - - buf := w.getBuffer() - flag = w.encodeSeries(buf, activeSeries) - - _, err = w.writeTo(csf, crc32, WALEntrySeries, flag, buf.Get()) - w.putBuffer(buf) - - if err != nil { - return fmt.Errorf("write to compaction segment: %w", err) - } - } - if err := r.Err(); err != nil { - return fmt.Errorf("read candidate WAL files: %w", err) - } - - off, err := csf.Seek(0, io.SeekCurrent) - if err != nil { - return err - } - if err := csf.Truncate(off); err != nil { - return err - } - if err := csf.Sync(); err != nil { - return nil - } - if err := csf.Close(); err != nil { - return nil - } - - _ = candidates[0].Close() // need close before remove on platform windows - if err := fileutil.Replace(csf.Name(), candidates[0].Name()); err != nil { - return fmt.Errorf("rename compaction segment: %w", err) - } - for _, f := range candidates[1:] { - f.Close() // need close before remove on platform windows - if err := os.RemoveAll(f.Name()); err != nil { - return fmt.Errorf("delete WAL segment file: %w", err) - } - } - if err := w.dirFile.Sync(); err != nil { - return err - } - - // The file object of csf still holds the name before rename. Recreate it so - // subsequent truncations do not look at a non-existent file name. - csf.File, err = w.openSegmentFile(candidates[0].Name()) - if err != nil { - return err - } - // We don't need it to be open. - if err := csf.Close(); err != nil { - return err - } - - w.mtx.Lock() - w.files = append([]*segmentFile{csf}, w.files[len(candidates):]...) - w.mtx.Unlock() - - return nil -} - -// LogSeries writes a batch of new series labels to the log. -// The series have to be ordered. -func (w *SegmentWAL) LogSeries(series []record.RefSeries) error { - buf := w.getBuffer() - - flag := w.encodeSeries(buf, series) - - w.mtx.Lock() - defer w.mtx.Unlock() - - err := w.write(WALEntrySeries, flag, buf.Get()) - - w.putBuffer(buf) - - if err != nil { - return fmt.Errorf("log series: %w", err) - } - - tf := w.head() - - for _, s := range series { - if tf.minSeries > s.Ref { - tf.minSeries = s.Ref - } - } - return nil -} - -// LogSamples writes a batch of new samples to the log. -func (w *SegmentWAL) LogSamples(samples []record.RefSample) error { - buf := w.getBuffer() - - flag := w.encodeSamples(buf, samples) - - w.mtx.Lock() - defer w.mtx.Unlock() - - err := w.write(WALEntrySamples, flag, buf.Get()) - - w.putBuffer(buf) - - if err != nil { - return fmt.Errorf("log series: %w", err) - } - tf := w.head() - - for _, s := range samples { - if tf.maxTime < s.T { - tf.maxTime = s.T - } - } - return nil -} - -// LogDeletes write a batch of new deletes to the log. -func (w *SegmentWAL) LogDeletes(stones []tombstones.Stone) error { - buf := w.getBuffer() - - flag := w.encodeDeletes(buf, stones) - - w.mtx.Lock() - defer w.mtx.Unlock() - - err := w.write(WALEntryDeletes, flag, buf.Get()) - - w.putBuffer(buf) - - if err != nil { - return fmt.Errorf("log series: %w", err) - } - tf := w.head() - - for _, s := range stones { - for _, iv := range s.Intervals { - if tf.maxTime < iv.Maxt { - tf.maxTime = iv.Maxt - } - } - } - return nil -} - -// openSegmentFile opens the given segment file and consumes and validates header. -func (w *SegmentWAL) openSegmentFile(name string) (*os.File, error) { - // We must open all files in read/write mode as we may have to truncate along - // the way and any file may become the head. - f, err := os.OpenFile(name, os.O_RDWR, 0o666) - if err != nil { - return nil, err - } - metab := make([]byte, 8) - - // If there is an error, we need close f for platform windows before gc. - // Otherwise, file op may fail. - hasError := true - defer func() { - if hasError { - f.Close() - } - }() - - switch n, err := f.Read(metab); { - case err != nil: - return nil, fmt.Errorf("validate meta %q: %w", f.Name(), err) - case n != 8: - return nil, fmt.Errorf("invalid header size %d in %q", n, f.Name()) - } - - if m := binary.BigEndian.Uint32(metab[:4]); m != WALMagic { - return nil, fmt.Errorf("invalid magic header %x in %q", m, f.Name()) - } - if metab[4] != WALFormatDefault { - return nil, fmt.Errorf("unknown WAL segment format %d in %q", metab[4], f.Name()) - } - hasError = false - return f, nil -} - -// createSegmentFile creates a new segment file with the given name. It preallocates -// the standard segment size if possible and writes the header. -func (w *SegmentWAL) createSegmentFile(name string) (*os.File, error) { - f, err := os.Create(name) - if err != nil { - return nil, err - } - if err = fileutil.Preallocate(f, w.segmentSize, true); err != nil { - return nil, err - } - // Write header metadata for new file. - metab := make([]byte, 8) - binary.BigEndian.PutUint32(metab[:4], WALMagic) - metab[4] = WALFormatDefault - - if _, err := f.Write(metab); err != nil { - return nil, err - } - return f, err -} - -// cut finishes the currently active segments and opens the next one. -// The encoder is reset to point to the new segment. -func (w *SegmentWAL) cut() error { - // Sync current head to disk and close. - if hf := w.head(); hf != nil { - if err := w.flush(); err != nil { - return err - } - // Finish last segment asynchronously to not block the WAL moving along - // in the new segment. - go func() { - w.actorc <- func() error { - off, err := hf.Seek(0, io.SeekCurrent) - if err != nil { - return fmt.Errorf("finish old segment %s: %w", hf.Name(), err) - } - if err := hf.Truncate(off); err != nil { - return fmt.Errorf("finish old segment %s: %w", hf.Name(), err) - } - if err := hf.Sync(); err != nil { - return fmt.Errorf("finish old segment %s: %w", hf.Name(), err) - } - if err := hf.Close(); err != nil { - return fmt.Errorf("finish old segment %s: %w", hf.Name(), err) - } - return nil - } - }() - } - - p, _, err := nextSequenceFile(w.dirFile.Name()) - if err != nil { - return err - } - f, err := w.createSegmentFile(p) - if err != nil { - return err - } - - go func() { - w.actorc <- func() error { - if err := w.dirFile.Sync(); err != nil { - return fmt.Errorf("sync WAL directory: %w", err) - } - return nil - } - }() - - w.files = append(w.files, newSegmentFile(f)) - - // TODO(gouthamve): make the buffer size a constant. - w.cur = bufio.NewWriterSize(f, 8*1024*1024) - w.curN = 8 - - return nil -} - -func (w *SegmentWAL) head() *segmentFile { - if len(w.files) == 0 { - return nil - } - return w.files[len(w.files)-1] -} - -// Sync flushes the changes to disk. -func (w *SegmentWAL) Sync() error { - var head *segmentFile - var err error - - // Flush the writer and retrieve the reference to the head segment under mutex lock. - func() { - w.mtx.Lock() - defer w.mtx.Unlock() - if err = w.flush(); err != nil { - return - } - head = w.head() - }() - if err != nil { - return fmt.Errorf("flush buffer: %w", err) - } - if head != nil { - // But only fsync the head segment after releasing the mutex as it will block on disk I/O. - start := time.Now() - err := fileutil.Fdatasync(head.File) - w.metrics.fsyncDuration.Observe(time.Since(start).Seconds()) - return err - } - return nil -} - -func (w *SegmentWAL) sync() error { - if err := w.flush(); err != nil { - return err - } - if w.head() == nil { - return nil - } - - start := time.Now() - err := fileutil.Fdatasync(w.head().File) - w.metrics.fsyncDuration.Observe(time.Since(start).Seconds()) - return err -} - -func (w *SegmentWAL) flush() error { - if w.cur == nil { - return nil - } - return w.cur.Flush() -} - -func (w *SegmentWAL) run(interval time.Duration) { - var tick <-chan time.Time - - if interval > 0 { - ticker := time.NewTicker(interval) - defer ticker.Stop() - tick = ticker.C - } - defer close(w.donec) - - for { - // Processing all enqueued operations has precedence over shutdown and - // background syncs. - select { - case f := <-w.actorc: - if err := f(); err != nil { - level.Error(w.logger).Log("msg", "operation failed", "err", err) - } - continue - default: - } - select { - case <-w.stopc: - return - case f := <-w.actorc: - if err := f(); err != nil { - level.Error(w.logger).Log("msg", "operation failed", "err", err) - } - case <-tick: - if err := w.Sync(); err != nil { - level.Error(w.logger).Log("msg", "sync failed", "err", err) - } - } - } -} - -// Close syncs all data and closes the underlying resources. -func (w *SegmentWAL) Close() error { - // Make sure you can call Close() multiple times. - select { - case <-w.stopc: - return nil // Already closed. - default: - } - - close(w.stopc) - <-w.donec - - w.mtx.Lock() - defer w.mtx.Unlock() - - if err := w.sync(); err != nil { - return err - } - // On opening, a WAL must be fully consumed once. Afterwards - // only the current segment will still be open. - if hf := w.head(); hf != nil { - if err := hf.Close(); err != nil { - return fmt.Errorf("closing WAL head %s: %w", hf.Name(), err) - } - } - if err := w.dirFile.Close(); err != nil { - return fmt.Errorf("closing WAL dir %s: %w", w.dirFile.Name(), err) - } - return nil -} - -func (w *SegmentWAL) write(t WALEntryType, flag uint8, buf []byte) error { - // Cut to the next segment if the entry exceeds the file size unless it would also - // exceed the size of a new segment. - // TODO(gouthamve): Add a test for this case where the commit is greater than segmentSize. - var ( - sz = int64(len(buf)) + 6 - newsz = w.curN + sz - ) - // XXX(fabxc): this currently cuts a new file whenever the WAL was newly opened. - // Probably fine in general but may yield a lot of short files in some cases. - if w.cur == nil || w.curN > w.segmentSize || newsz > w.segmentSize && sz <= w.segmentSize { - if err := w.cut(); err != nil { - return err - } - } - n, err := w.writeTo(w.cur, w.crc32, t, flag, buf) - - w.curN += int64(n) - - return err -} - -func (w *SegmentWAL) writeTo(wr io.Writer, crc32 hash.Hash, t WALEntryType, flag uint8, buf []byte) (int, error) { - if len(buf) == 0 { - return 0, nil - } - crc32.Reset() - wr = io.MultiWriter(crc32, wr) - - var b [6]byte - b[0] = byte(t) - b[1] = flag - - binary.BigEndian.PutUint32(b[2:], uint32(len(buf))) - - n1, err := wr.Write(b[:]) - if err != nil { - return n1, err - } - n2, err := wr.Write(buf) - if err != nil { - return n1 + n2, err - } - n3, err := wr.Write(crc32.Sum(b[:0])) - - return n1 + n2 + n3, err -} - -const ( - walSeriesSimple = 1 - walSamplesSimple = 1 - walDeletesSimple = 1 -) - -func (w *SegmentWAL) encodeSeries(buf *encoding.Encbuf, series []record.RefSeries) uint8 { - for _, s := range series { - buf.PutBE64(uint64(s.Ref)) - record.EncodeLabels(buf, s.Labels) - } - return walSeriesSimple -} - -func (w *SegmentWAL) encodeSamples(buf *encoding.Encbuf, samples []record.RefSample) uint8 { - if len(samples) == 0 { - return walSamplesSimple - } - // Store base timestamp and base reference number of first sample. - // All samples encode their timestamp and ref as delta to those. - // - // TODO(fabxc): optimize for all samples having the same timestamp. - first := samples[0] - - buf.PutBE64(uint64(first.Ref)) - buf.PutBE64int64(first.T) - - for _, s := range samples { - buf.PutVarint64(int64(s.Ref) - int64(first.Ref)) - buf.PutVarint64(s.T - first.T) - buf.PutBE64(math.Float64bits(s.V)) - } - return walSamplesSimple -} - -func (w *SegmentWAL) encodeDeletes(buf *encoding.Encbuf, stones []tombstones.Stone) uint8 { - for _, s := range stones { - for _, iv := range s.Intervals { - buf.PutBE64(uint64(s.Ref)) - buf.PutVarint64(iv.Mint) - buf.PutVarint64(iv.Maxt) - } - } - return walDeletesSimple -} - -// walReader decodes and emits write ahead log entries. -type walReader struct { - logger log.Logger - - files []*segmentFile - cur int - buf []byte - crc32 hash.Hash32 - dec record.Decoder - - curType WALEntryType - curFlag byte - curBuf []byte - lastOffset int64 // offset after last successfully read entry - - err error -} - -func newWALReader(files []*segmentFile, l log.Logger) *walReader { - if l == nil { - l = log.NewNopLogger() - } - return &walReader{ - logger: l, - files: files, - buf: make([]byte, 0, 128*4096), - crc32: newCRC32(), - dec: record.NewDecoder(labels.NewSymbolTable()), - } -} - -// Err returns the last error the reader encountered. -func (r *walReader) Err() error { - return r.err -} - -func (r *walReader) Read( - seriesf func([]record.RefSeries), - samplesf func([]record.RefSample), - deletesf func([]tombstones.Stone), -) error { - // Concurrency for replaying the WAL is very limited. We at least split out decoding and - // processing into separate threads. - // Historically, the processing is the bottleneck with reading and decoding using only - // 15% of the CPU. - var ( - seriesPool zeropool.Pool[[]record.RefSeries] - samplePool zeropool.Pool[[]record.RefSample] - deletePool zeropool.Pool[[]tombstones.Stone] - ) - donec := make(chan struct{}) - datac := make(chan interface{}, 100) - - go func() { - defer close(donec) - - for x := range datac { - switch v := x.(type) { - case []record.RefSeries: - if seriesf != nil { - seriesf(v) - } - seriesPool.Put(v[:0]) - case []record.RefSample: - if samplesf != nil { - samplesf(v) - } - samplePool.Put(v[:0]) - case []tombstones.Stone: - if deletesf != nil { - deletesf(v) - } - deletePool.Put(v[:0]) - default: - level.Error(r.logger).Log("msg", "unexpected data type") - } - } - }() - - var err error - - for r.next() { - et, flag, b := r.at() - - // In decoding below we never return a walCorruptionErr for now. - // Those should generally be caught by entry decoding before. - switch et { - case WALEntrySeries: - series := seriesPool.Get() - if series == nil { - series = make([]record.RefSeries, 0, 512) - } - - err = r.decodeSeries(flag, b, &series) - if err != nil { - err = fmt.Errorf("decode series entry: %w", err) - break - } - datac <- series - - cf := r.current() - for _, s := range series { - if cf.minSeries > s.Ref { - cf.minSeries = s.Ref - } - } - case WALEntrySamples: - samples := samplePool.Get() - if samples == nil { - samples = make([]record.RefSample, 0, 512) - } - - err = r.decodeSamples(flag, b, &samples) - if err != nil { - err = fmt.Errorf("decode samples entry: %w", err) - break - } - datac <- samples - - // Update the times for the WAL segment file. - cf := r.current() - for _, s := range samples { - if cf.maxTime < s.T { - cf.maxTime = s.T - } - } - case WALEntryDeletes: - deletes := deletePool.Get() - if deletes == nil { - deletes = make([]tombstones.Stone, 0, 512) - } - - err = r.decodeDeletes(flag, b, &deletes) - if err != nil { - err = fmt.Errorf("decode delete entry: %w", err) - break - } - datac <- deletes - - // Update the times for the WAL segment file. - cf := r.current() - for _, s := range deletes { - for _, iv := range s.Intervals { - if cf.maxTime < iv.Maxt { - cf.maxTime = iv.Maxt - } - } - } - } - } - close(datac) - <-donec - - if err != nil { - return err - } - if err := r.Err(); err != nil { - return fmt.Errorf("read entry: %w", err) - } - return nil -} - -func (r *walReader) at() (WALEntryType, byte, []byte) { - return r.curType, r.curFlag, r.curBuf -} - -// next returns decodes the next entry pair and returns true -// if it was successful. -func (r *walReader) next() bool { - if r.cur >= len(r.files) { - return false - } - cf := r.files[r.cur] - - // Remember the offset after the last correctly read entry. If the next one - // is corrupted, this is where we can safely truncate. - r.lastOffset, r.err = cf.Seek(0, io.SeekCurrent) - if r.err != nil { - return false - } - - et, flag, b, err := r.entry(cf) - // If we reached the end of the reader, advance to the next one - // and close. - // Do not close on the last one as it will still be appended to. - if errors.Is(err, io.EOF) { - if r.cur == len(r.files)-1 { - return false - } - // Current reader completed, close and move to the next one. - if err := cf.Close(); err != nil { - r.err = err - return false - } - r.cur++ - return r.next() - } - if err != nil { - r.err = err - return false - } - - r.curType = et - r.curFlag = flag - r.curBuf = b - return r.err == nil -} - -func (r *walReader) current() *segmentFile { - return r.files[r.cur] -} - -// walCorruptionErr is a type wrapper for errors that indicate WAL corruption -// and trigger a truncation. -type walCorruptionErr struct { - err error - file int - lastOffset int64 -} - -func (e *walCorruptionErr) Error() string { - return fmt.Sprintf("%s ", e.err, e.file, e.lastOffset) -} - -func (e *walCorruptionErr) Unwrap() error { - return e.err -} - -func (r *walReader) corruptionErr(s string, args ...interface{}) error { - return &walCorruptionErr{ - err: fmt.Errorf(s, args...), - file: r.cur, - lastOffset: r.lastOffset, - } -} - -func (r *walReader) entry(cr io.Reader) (WALEntryType, byte, []byte, error) { - r.crc32.Reset() - tr := io.TeeReader(cr, r.crc32) - - b := make([]byte, 6) - switch n, err := tr.Read(b); { - case err != nil: - return 0, 0, nil, err - case n != 6: - return 0, 0, nil, r.corruptionErr("invalid entry header size %d", n) - } - - var ( - etype = WALEntryType(b[0]) - flag = b[1] - length = int(binary.BigEndian.Uint32(b[2:])) - ) - // Exit if we reached pre-allocated space. - if etype == 0 { - return 0, 0, nil, io.EOF - } - if etype != WALEntrySeries && etype != WALEntrySamples && etype != WALEntryDeletes { - return 0, 0, nil, r.corruptionErr("invalid entry type %d", etype) - } - - if length > len(r.buf) { - r.buf = make([]byte, length) - } - buf := r.buf[:length] - - switch n, err := tr.Read(buf); { - case err != nil: - return 0, 0, nil, err - case n != length: - return 0, 0, nil, r.corruptionErr("invalid entry body size %d", n) - } - - switch n, err := cr.Read(b[:4]); { - case err != nil: - return 0, 0, nil, err - case n != 4: - return 0, 0, nil, r.corruptionErr("invalid checksum length %d", n) - } - if exp, has := binary.BigEndian.Uint32(b[:4]), r.crc32.Sum32(); has != exp { - return 0, 0, nil, r.corruptionErr("unexpected CRC32 checksum %x, want %x", has, exp) - } - - return etype, flag, buf, nil -} - -func (r *walReader) decodeSeries(flag byte, b []byte, res *[]record.RefSeries) error { - dec := encoding.Decbuf{B: b} - - for len(dec.B) > 0 && dec.Err() == nil { - ref := chunks.HeadSeriesRef(dec.Be64()) - lset := r.dec.DecodeLabels(&dec) - - *res = append(*res, record.RefSeries{ - Ref: ref, - Labels: lset, - }) - } - if dec.Err() != nil { - return dec.Err() - } - if len(dec.B) > 0 { - return fmt.Errorf("unexpected %d bytes left in entry", len(dec.B)) - } - return nil -} - -func (r *walReader) decodeSamples(flag byte, b []byte, res *[]record.RefSample) error { - if len(b) == 0 { - return nil - } - dec := encoding.Decbuf{B: b} - - var ( - baseRef = dec.Be64() - baseTime = dec.Be64int64() - ) - - for len(dec.B) > 0 && dec.Err() == nil { - dref := dec.Varint64() - dtime := dec.Varint64() - val := dec.Be64() - - *res = append(*res, record.RefSample{ - Ref: chunks.HeadSeriesRef(int64(baseRef) + dref), - T: baseTime + dtime, - V: math.Float64frombits(val), - }) - } - - if err := dec.Err(); err != nil { - return fmt.Errorf("decode error after %d samples: %w", len(*res), err) - } - if len(dec.B) > 0 { - return fmt.Errorf("unexpected %d bytes left in entry", len(dec.B)) - } - return nil -} - -func (r *walReader) decodeDeletes(flag byte, b []byte, res *[]tombstones.Stone) error { - dec := &encoding.Decbuf{B: b} - - for dec.Len() > 0 && dec.Err() == nil { - *res = append(*res, tombstones.Stone{ - Ref: storage.SeriesRef(dec.Be64()), - Intervals: tombstones.Intervals{ - {Mint: dec.Varint64(), Maxt: dec.Varint64()}, - }, - }) - } - if dec.Err() != nil { - return dec.Err() - } - if len(dec.B) > 0 { - return fmt.Errorf("unexpected %d bytes left in entry", len(dec.B)) - } - return nil -} - -func deprecatedWALExists(logger log.Logger, dir string) (bool, error) { - // Detect whether we still have the old WAL. - fns, err := sequenceFiles(dir) - if err != nil && !os.IsNotExist(err) { - return false, fmt.Errorf("list sequence files: %w", err) - } - if len(fns) == 0 { - return false, nil // No WAL at all yet. - } - // Check header of first segment to see whether we are still dealing with an - // old WAL. - f, err := os.Open(fns[0]) - if err != nil { - return false, fmt.Errorf("check first existing segment: %w", err) - } - defer f.Close() - - var hdr [4]byte - if _, err := f.Read(hdr[:]); err != nil && !errors.Is(err, io.EOF) { - return false, fmt.Errorf("read header from first segment: %w", err) - } - // If we cannot read the magic header for segments of the old WAL, abort. - // Either it's migrated already or there's a corruption issue with which - // we cannot deal here anyway. Subsequent attempts to open the WAL will error in that case. - if binary.BigEndian.Uint32(hdr[:]) != WALMagic { - return false, nil - } - return true, nil -} - -// MigrateWAL rewrites the deprecated write ahead log into the new format. -func MigrateWAL(logger log.Logger, dir string) (err error) { - if logger == nil { - logger = log.NewNopLogger() - } - if exists, err := deprecatedWALExists(logger, dir); err != nil || !exists { - return err - } - level.Info(logger).Log("msg", "Migrating WAL format") - - tmpdir := dir + ".tmp" - if err := os.RemoveAll(tmpdir); err != nil { - return fmt.Errorf("cleanup replacement dir: %w", err) - } - repl, err := wlog.New(logger, nil, tmpdir, wlog.CompressionNone) - if err != nil { - return fmt.Errorf("open new WAL: %w", err) - } - - // It should've already been closed as part of the previous finalization. - // Do it once again in case of prior errors. - defer func() { - if err != nil { - repl.Close() - } - }() - - w, err := OpenSegmentWAL(dir, logger, time.Minute, nil) - if err != nil { - return fmt.Errorf("open old WAL: %w", err) - } - defer w.Close() - - rdr := w.Reader() - - var ( - enc record.Encoder - b []byte - ) - decErr := rdr.Read( - func(s []record.RefSeries) { - if err != nil { - return - } - err = repl.Log(enc.Series(s, b[:0])) - }, - func(s []record.RefSample) { - if err != nil { - return - } - err = repl.Log(enc.Samples(s, b[:0])) - }, - func(s []tombstones.Stone) { - if err != nil { - return - } - err = repl.Log(enc.Tombstones(s, b[:0])) - }, - ) - if decErr != nil { - return fmt.Errorf("decode old entries: %w", err) - } - if err != nil { - return fmt.Errorf("write new entries: %w", err) - } - // We explicitly close even when there is a defer for Windows to be - // able to delete it. The defer is in place to close it in-case there - // are errors above. - if err := w.Close(); err != nil { - return fmt.Errorf("close old WAL: %w", err) - } - if err := repl.Close(); err != nil { - return fmt.Errorf("close new WAL: %w", err) - } - if err := fileutil.Replace(tmpdir, dir); err != nil { - return fmt.Errorf("replace old WAL: %w", err) - } - return nil -} diff --git a/tsdb/wal_test.go b/tsdb/wal_test.go deleted file mode 100644 index 7794a54547..0000000000 --- a/tsdb/wal_test.go +++ /dev/null @@ -1,553 +0,0 @@ -// 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. - -//go:build !windows - -package tsdb - -import ( - "encoding/binary" - "io" - "math/rand" - "os" - "path" - "path/filepath" - "testing" - "time" - - "github.com/go-kit/log" - "github.com/stretchr/testify/require" - - "github.com/prometheus/prometheus/model/labels" - "github.com/prometheus/prometheus/storage" - "github.com/prometheus/prometheus/tsdb/chunks" - "github.com/prometheus/prometheus/tsdb/record" - "github.com/prometheus/prometheus/tsdb/tombstones" - "github.com/prometheus/prometheus/tsdb/wlog" - "github.com/prometheus/prometheus/util/testutil" -) - -func TestSegmentWAL_cut(t *testing.T) { - tmpdir := t.TempDir() - - // This calls cut() implicitly the first time without a previous tail. - w, err := OpenSegmentWAL(tmpdir, nil, 0, nil) - require.NoError(t, err) - - require.NoError(t, w.write(WALEntrySeries, 1, []byte("Hello World!!"))) - - require.NoError(t, w.cut()) - - // Cutting creates a new file. - require.Len(t, w.files, 2) - - require.NoError(t, w.write(WALEntrySeries, 1, []byte("Hello World!!"))) - - require.NoError(t, w.Close()) - - for _, of := range w.files { - f, err := os.Open(of.Name()) - require.NoError(t, err) - - // Verify header data. - metab := make([]byte, 8) - _, err = f.Read(metab) - require.NoError(t, err) - require.Equal(t, WALMagic, binary.BigEndian.Uint32(metab[:4])) - require.Equal(t, WALFormatDefault, metab[4]) - - // We cannot actually check for correct pre-allocation as it is - // optional per filesystem and handled transparently. - et, flag, b, err := newWALReader(nil, nil).entry(f) - require.NoError(t, err) - require.Equal(t, WALEntrySeries, et) - require.Equal(t, byte(walSeriesSimple), flag) - require.Equal(t, []byte("Hello World!!"), b) - } -} - -func TestSegmentWAL_Truncate(t *testing.T) { - const ( - numMetrics = 20000 - batch = 100 - ) - series, err := labels.ReadLabels(filepath.Join("testdata", "20kseries.json"), numMetrics) - require.NoError(t, err) - - dir := t.TempDir() - - w, err := OpenSegmentWAL(dir, nil, 0, nil) - require.NoError(t, err) - defer func(wal *SegmentWAL) { require.NoError(t, wal.Close()) }(w) - w.segmentSize = 10000 - - for i := 0; i < numMetrics; i += batch { - var rs []record.RefSeries - - for j, s := range series[i : i+batch] { - rs = append(rs, record.RefSeries{Labels: s, Ref: chunks.HeadSeriesRef(i+j) + 1}) - } - err := w.LogSeries(rs) - require.NoError(t, err) - } - - // We mark the 2nd half of the files with a min timestamp that should discard - // them from the selection of compactable files. - for i, f := range w.files[len(w.files)/2:] { - f.maxTime = int64(1000 + i) - } - // All series in those files must be preserved regarding of the provided postings list. - boundarySeries := w.files[len(w.files)/2].minSeries - - // We truncate while keeping every 2nd series. - keep := map[chunks.HeadSeriesRef]struct{}{} - for i := 1; i <= numMetrics; i += 2 { - keep[chunks.HeadSeriesRef(i)] = struct{}{} - } - keepf := func(id chunks.HeadSeriesRef) bool { - _, ok := keep[id] - return ok - } - - err = w.Truncate(1000, keepf) - require.NoError(t, err) - - var expected []record.RefSeries - - for i := 1; i <= numMetrics; i++ { - if i%2 == 1 || chunks.HeadSeriesRef(i) >= boundarySeries { - expected = append(expected, record.RefSeries{Ref: chunks.HeadSeriesRef(i), Labels: series[i-1]}) - } - } - - // Call Truncate once again to see whether we can read the written file without - // creating a new WAL. - err = w.Truncate(1000, keepf) - require.NoError(t, err) - require.NoError(t, w.Close()) - - // The same again with a new WAL. - w, err = OpenSegmentWAL(dir, nil, 0, nil) - require.NoError(t, err) - defer func(wal *SegmentWAL) { require.NoError(t, wal.Close()) }(w) - - var readSeries []record.RefSeries - r := w.Reader() - - require.NoError(t, r.Read(func(s []record.RefSeries) { - readSeries = append(readSeries, s...) - }, nil, nil)) - - testutil.RequireEqual(t, expected, readSeries) -} - -// Symmetrical test of reading and writing to the WAL via its main interface. -func TestSegmentWAL_Log_Restore(t *testing.T) { - const ( - numMetrics = 50 - iterations = 5 - stepSize = 5 - ) - // Generate testing data. It does not make semantic sense but - // for the purpose of this test. - series, err := labels.ReadLabels(filepath.Join("testdata", "20kseries.json"), numMetrics) - require.NoError(t, err) - - dir := t.TempDir() - - var ( - recordedSeries [][]record.RefSeries - recordedSamples [][]record.RefSample - recordedDeletes [][]tombstones.Stone - ) - var totalSamples int - - // Open WAL a bunch of times, validate all previous data can be read, - // write more data to it, close it. - for k := 0; k < numMetrics; k += numMetrics / iterations { - w, err := OpenSegmentWAL(dir, nil, 0, nil) - require.NoError(t, err) - - // Set smaller segment size so we can actually write several files. - w.segmentSize = 1000 * 1000 - - r := w.Reader() - - var ( - resultSeries [][]record.RefSeries - resultSamples [][]record.RefSample - resultDeletes [][]tombstones.Stone - ) - - serf := func(series []record.RefSeries) { - if len(series) > 0 { - clsets := make([]record.RefSeries, len(series)) - copy(clsets, series) - resultSeries = append(resultSeries, clsets) - } - } - smplf := func(smpls []record.RefSample) { - if len(smpls) > 0 { - csmpls := make([]record.RefSample, len(smpls)) - copy(csmpls, smpls) - resultSamples = append(resultSamples, csmpls) - } - } - - delf := func(stones []tombstones.Stone) { - if len(stones) > 0 { - cst := make([]tombstones.Stone, len(stones)) - copy(cst, stones) - resultDeletes = append(resultDeletes, cst) - } - } - - require.NoError(t, r.Read(serf, smplf, delf)) - - testutil.RequireEqual(t, recordedSamples, resultSamples) - testutil.RequireEqual(t, recordedSeries, resultSeries) - testutil.RequireEqual(t, recordedDeletes, resultDeletes) - - series := series[k : k+(numMetrics/iterations)] - - // Insert in batches and generate different amounts of samples for each. - for i := 0; i < len(series); i += stepSize { - var samples []record.RefSample - var stones []tombstones.Stone - - for j := 0; j < i*10; j++ { - samples = append(samples, record.RefSample{ - Ref: chunks.HeadSeriesRef(j % 10000), - T: int64(j * 2), - V: rand.Float64(), - }) - } - - for j := 0; j < i*20; j++ { - ts := rand.Int63() - stones = append(stones, tombstones.Stone{Ref: storage.SeriesRef(rand.Uint64()), Intervals: tombstones.Intervals{{Mint: ts, Maxt: ts + rand.Int63n(10000)}}}) - } - - lbls := series[i : i+stepSize] - series := make([]record.RefSeries, 0, len(series)) - for j, l := range lbls { - series = append(series, record.RefSeries{ - Ref: chunks.HeadSeriesRef(i + j), - Labels: l, - }) - } - - require.NoError(t, w.LogSeries(series)) - require.NoError(t, w.LogSamples(samples)) - require.NoError(t, w.LogDeletes(stones)) - - if len(lbls) > 0 { - recordedSeries = append(recordedSeries, series) - } - if len(samples) > 0 { - recordedSamples = append(recordedSamples, samples) - totalSamples += len(samples) - } - if len(stones) > 0 { - recordedDeletes = append(recordedDeletes, stones) - } - } - - require.NoError(t, w.Close()) - } -} - -func TestWALRestoreCorrupted_invalidSegment(t *testing.T) { - dir := t.TempDir() - - wal, err := OpenSegmentWAL(dir, nil, 0, nil) - require.NoError(t, err) - defer func(wal *SegmentWAL) { require.NoError(t, wal.Close()) }(wal) - - _, err = wal.createSegmentFile(filepath.Join(dir, "000000")) - require.NoError(t, err) - f, err := wal.createSegmentFile(filepath.Join(dir, "000001")) - require.NoError(t, err) - f2, err := wal.createSegmentFile(filepath.Join(dir, "000002")) - require.NoError(t, err) - require.NoError(t, f2.Close()) - - // Make header of second segment invalid. - _, err = f.WriteAt([]byte{1, 2, 3, 4}, 0) - require.NoError(t, err) - require.NoError(t, f.Close()) - - require.NoError(t, wal.Close()) - - wal, err = OpenSegmentWAL(dir, log.NewLogfmtLogger(os.Stderr), 0, nil) - require.NoError(t, err) - defer func(wal *SegmentWAL) { require.NoError(t, wal.Close()) }(wal) - - files, err := os.ReadDir(dir) - require.NoError(t, err) - fns := []string{} - for _, f := range files { - fns = append(fns, f.Name()) - } - require.Equal(t, []string{"000000"}, fns) -} - -// Test reading from a WAL that has been corrupted through various means. -func TestWALRestoreCorrupted(t *testing.T) { - cases := []struct { - name string - f func(*testing.T, *SegmentWAL) - }{ - { - name: "truncate_checksum", - f: func(t *testing.T, w *SegmentWAL) { - f, err := os.OpenFile(w.files[0].Name(), os.O_WRONLY, 0o666) - require.NoError(t, err) - defer f.Close() - - off, err := f.Seek(0, io.SeekEnd) - require.NoError(t, err) - - require.NoError(t, f.Truncate(off-1)) - }, - }, - { - name: "truncate_body", - f: func(t *testing.T, w *SegmentWAL) { - f, err := os.OpenFile(w.files[0].Name(), os.O_WRONLY, 0o666) - require.NoError(t, err) - defer f.Close() - - off, err := f.Seek(0, io.SeekEnd) - require.NoError(t, err) - - require.NoError(t, f.Truncate(off-8)) - }, - }, - { - name: "body_content", - f: func(t *testing.T, w *SegmentWAL) { - f, err := os.OpenFile(w.files[0].Name(), os.O_WRONLY, 0o666) - require.NoError(t, err) - defer f.Close() - - off, err := f.Seek(0, io.SeekEnd) - require.NoError(t, err) - - // Write junk before checksum starts. - _, err = f.WriteAt([]byte{1, 2, 3, 4}, off-8) - require.NoError(t, err) - }, - }, - { - name: "checksum", - f: func(t *testing.T, w *SegmentWAL) { - f, err := os.OpenFile(w.files[0].Name(), os.O_WRONLY, 0o666) - require.NoError(t, err) - defer f.Close() - - off, err := f.Seek(0, io.SeekEnd) - require.NoError(t, err) - - // Write junk into checksum - _, err = f.WriteAt([]byte{1, 2, 3, 4}, off-4) - require.NoError(t, err) - }, - }, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - // Generate testing data. It does not make semantic sense but - // for the purpose of this test. - dir := t.TempDir() - - w, err := OpenSegmentWAL(dir, nil, 0, nil) - require.NoError(t, err) - defer func(wal *SegmentWAL) { require.NoError(t, wal.Close()) }(w) - - require.NoError(t, w.LogSamples([]record.RefSample{{T: 1, V: 2}})) - require.NoError(t, w.LogSamples([]record.RefSample{{T: 2, V: 3}})) - - require.NoError(t, w.cut()) - - // Sleep 2 seconds to avoid error where cut and test "cases" function may write or - // truncate the file out of orders as "cases" are not synchronized with cut. - // Hopefully cut will complete by 2 seconds. - time.Sleep(2 * time.Second) - - require.NoError(t, w.LogSamples([]record.RefSample{{T: 3, V: 4}})) - require.NoError(t, w.LogSamples([]record.RefSample{{T: 5, V: 6}})) - - require.NoError(t, w.Close()) - - // cut() truncates and fsyncs the first segment async. If it happens after - // the corruption we apply below, the corruption will be overwritten again. - // Fire and forget a sync to avoid flakiness. - w.files[0].Sync() - // Corrupt the second entry in the first file. - // After re-opening we must be able to read the first entry - // and the rest, including the second file, must be truncated for clean further - // writes. - c.f(t, w) - - logger := log.NewLogfmtLogger(os.Stderr) - - w2, err := OpenSegmentWAL(dir, logger, 0, nil) - require.NoError(t, err) - defer func(wal *SegmentWAL) { require.NoError(t, wal.Close()) }(w2) - - r := w2.Reader() - - serf := func(l []record.RefSeries) { - require.Empty(t, l) - } - - // Weird hack to check order of reads. - i := 0 - samplef := func(s []record.RefSample) { - if i == 0 { - require.Equal(t, []record.RefSample{{T: 1, V: 2}}, s) - i++ - } else { - require.Equal(t, []record.RefSample{{T: 99, V: 100}}, s) - } - } - - require.NoError(t, r.Read(serf, samplef, nil)) - - require.NoError(t, w2.LogSamples([]record.RefSample{{T: 99, V: 100}})) - require.NoError(t, w2.Close()) - - // We should see the first valid entry and the new one, everything after - // is truncated. - w3, err := OpenSegmentWAL(dir, logger, 0, nil) - require.NoError(t, err) - defer func(wal *SegmentWAL) { require.NoError(t, wal.Close()) }(w3) - - r = w3.Reader() - - i = 0 - require.NoError(t, r.Read(serf, samplef, nil)) - }) - } -} - -func TestMigrateWAL_Empty(t *testing.T) { - // The migration procedure must properly deal with a zero-length segment, - // which is valid in the new format. - dir := t.TempDir() - - wdir := path.Join(dir, "wal") - - // Initialize empty WAL. - w, err := wlog.New(nil, nil, wdir, wlog.CompressionNone) - require.NoError(t, err) - require.NoError(t, w.Close()) - - require.NoError(t, MigrateWAL(nil, wdir)) -} - -func TestMigrateWAL_Fuzz(t *testing.T) { - dir := t.TempDir() - - wdir := path.Join(dir, "wal") - - // Should pass if no WAL exists yet. - require.NoError(t, MigrateWAL(nil, wdir)) - - oldWAL, err := OpenSegmentWAL(wdir, nil, time.Minute, nil) - require.NoError(t, err) - - // Write some data. - require.NoError(t, oldWAL.LogSeries([]record.RefSeries{ - {Ref: 100, Labels: labels.FromStrings("abc", "def", "123", "456")}, - {Ref: 1, Labels: labels.FromStrings("abc", "def2", "1234", "4567")}, - })) - require.NoError(t, oldWAL.LogSamples([]record.RefSample{ - {Ref: 1, T: 100, V: 200}, - {Ref: 2, T: 300, V: 400}, - })) - require.NoError(t, oldWAL.LogSeries([]record.RefSeries{ - {Ref: 200, Labels: labels.FromStrings("xyz", "def", "foo", "bar")}, - })) - require.NoError(t, oldWAL.LogSamples([]record.RefSample{ - {Ref: 3, T: 100, V: 200}, - {Ref: 4, T: 300, V: 400}, - })) - require.NoError(t, oldWAL.LogDeletes([]tombstones.Stone{ - {Ref: 1, Intervals: []tombstones.Interval{{Mint: 100, Maxt: 200}}}, - })) - - require.NoError(t, oldWAL.Close()) - - // Perform migration. - require.NoError(t, MigrateWAL(nil, wdir)) - - w, err := wlog.New(nil, nil, wdir, wlog.CompressionNone) - require.NoError(t, err) - - // We can properly write some new data after migration. - var enc record.Encoder - require.NoError(t, w.Log(enc.Samples([]record.RefSample{ - {Ref: 500, T: 1, V: 1}, - }, nil))) - - require.NoError(t, w.Close()) - - // Read back all data. - sr, err := wlog.NewSegmentsReader(wdir) - require.NoError(t, err) - - r := wlog.NewReader(sr) - var res []interface{} - dec := record.NewDecoder(labels.NewSymbolTable()) - - for r.Next() { - rec := r.Record() - - switch dec.Type(rec) { - case record.Series: - s, err := dec.Series(rec, nil) - require.NoError(t, err) - res = append(res, s) - case record.Samples: - s, err := dec.Samples(rec, nil) - require.NoError(t, err) - res = append(res, s) - case record.Tombstones: - s, err := dec.Tombstones(rec, nil) - require.NoError(t, err) - res = append(res, s) - default: - require.Fail(t, "unknown record type %d", dec.Type(rec)) - } - } - require.NoError(t, r.Err()) - - testutil.RequireEqual(t, []interface{}{ - []record.RefSeries{ - {Ref: 100, Labels: labels.FromStrings("abc", "def", "123", "456")}, - {Ref: 1, Labels: labels.FromStrings("abc", "def2", "1234", "4567")}, - }, - []record.RefSample{{Ref: 1, T: 100, V: 200}, {Ref: 2, T: 300, V: 400}}, - []record.RefSeries{ - {Ref: 200, Labels: labels.FromStrings("xyz", "def", "foo", "bar")}, - }, - []record.RefSample{{Ref: 3, T: 100, V: 200}, {Ref: 4, T: 300, V: 400}}, - []tombstones.Stone{{Ref: 1, Intervals: []tombstones.Interval{{Mint: 100, Maxt: 200}}}}, - []record.RefSample{{Ref: 500, T: 1, V: 1}}, - }, res) - - // Migrating an already migrated WAL shouldn't do anything. - require.NoError(t, MigrateWAL(nil, wdir)) -} From a0fbc75f341949bc90335799719599efc9660b99 Mon Sep 17 00:00:00 2001 From: SuperQ Date: Thu, 14 Mar 2024 09:20:40 +0100 Subject: [PATCH 033/127] Add container_description.yml to repo sync Add the container_description.yml workflow to the repo file sync script. * Skip sync if there is no Dockerfile. * Fixup minor typo in container_description.yml. Signed-off-by: SuperQ --- .github/workflows/container_description.yml | 2 +- scripts/sync_repo_files.sh | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/container_description.yml b/.github/workflows/container_description.yml index eb505207cb..f922e3a07c 100644 --- a/.github/workflows/container_description.yml +++ b/.github/workflows/container_description.yml @@ -32,7 +32,7 @@ jobs: PushQuayIoReadme: runs-on: ubuntu-latest - name: Push README to Docker Hub + name: Push README to quay.io steps: - name: git checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 diff --git a/scripts/sync_repo_files.sh b/scripts/sync_repo_files.sh index 6965a45452..a46f289c49 100755 --- a/scripts/sync_repo_files.sh +++ b/scripts/sync_repo_files.sh @@ -37,7 +37,7 @@ if [ -z "${GITHUB_TOKEN}" ]; then fi # List of files that should be synced. -SYNC_FILES="CODE_OF_CONDUCT.md LICENSE Makefile.common SECURITY.md .yamllint scripts/golangci-lint.yml .github/workflows/scorecards.yml" +SYNC_FILES="CODE_OF_CONDUCT.md LICENSE Makefile.common SECURITY.md .yamllint scripts/golangci-lint.yml .github/workflows/scorecards.yml .github/workflows/container_description.yml" # Go to the root of the repo cd "$(git rev-parse --show-cdup)" || exit 1 @@ -99,6 +99,15 @@ check_go() { curl -sLf -o /dev/null "https://raw.githubusercontent.com/${org_repo}/${default_branch}/go.mod" } +check_docker() { + local org_repo + local default_branch + org_repo="$1" + default_branch="$2" + + curl -sLf -o /dev/null "https://raw.githubusercontent.com/${org_repo}/${default_branch}/Dockerfile" +} + process_repo() { local org_repo local default_branch @@ -119,6 +128,10 @@ process_repo() { echo "${org_repo} is not Go, skipping golangci-lint.yml." continue fi + if [[ "${source_file}" == '.github/workflows/container_description.yml' ]] && ! check_docker "${org_repo}" "${default_branch}" ; then + echo "${org_repo} has no Dockerfile, skipping container_description.yml." + continue + fi if [[ "${source_file}" == 'LICENSE' ]] && ! check_license "${target_file}" ; then echo "LICENSE in ${org_repo} is not apache, skipping." continue From 1de49d5b6915620da666d0e9872413f9091f1d93 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Thu, 14 Mar 2024 11:15:17 +0100 Subject: [PATCH 034/127] Remove unused function tsdb/chunks.PopulatedChunk (#13763) Signed-off-by: Arve Knudsen --- tsdb/chunks/chunks.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tsdb/chunks/chunks.go b/tsdb/chunks/chunks.go index 543b98c289..0826f69670 100644 --- a/tsdb/chunks/chunks.go +++ b/tsdb/chunks/chunks.go @@ -202,15 +202,6 @@ func ChunkFromSamplesGeneric(s Samples) (Meta, error) { }, nil } -// PopulatedChunk creates a chunk populated with samples every second starting at minTime. -func PopulatedChunk(numSamples int, minTime int64) (Meta, error) { - samples := make([]Sample, numSamples) - for i := 0; i < numSamples; i++ { - samples[i] = sample{t: minTime + int64(i*1000), f: 1.0} - } - return ChunkFromSamples(samples) -} - // ChunkMetasToSamples converts a slice of chunk meta data to a slice of samples. // Used in tests to compare the content of chunks. func ChunkMetasToSamples(chunks []Meta) (result []Sample) { From 3eed6c760a5026cd68938d208d16f9264cf509e6 Mon Sep 17 00:00:00 2001 From: machine424 Date: Thu, 14 Mar 2024 18:35:40 +0100 Subject: [PATCH 035/127] adjust signal termination warning log Signed-off-by: machine424 --- cmd/prometheus/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go index 614d884637..f64c00e824 100644 --- a/cmd/prometheus/main.go +++ b/cmd/prometheus/main.go @@ -960,8 +960,8 @@ func main() { func() error { // Don't forget to release the reloadReady channel so that waiting blocks can exit normally. select { - case <-term: - level.Warn(logger).Log("msg", "Received SIGTERM, exiting gracefully...") + case sig := <-term: + level.Warn(logger).Log("msg", "Received an OS signal, exiting gracefully...", "signal", sig.String()) reloadReady.Close() case <-webHandler.Quit(): level.Warn(logger).Log("msg", "Received termination request via web service, exiting gracefully...") From dca47ce2c98d16a5b5959ca9ea41bc7059a8da3c Mon Sep 17 00:00:00 2001 From: Charles Korn Date: Thu, 14 Mar 2024 13:39:54 +1100 Subject: [PATCH 036/127] Only run on prometheus/prometheus repo Signed-off-by: Charles Korn --- .github/workflows/container_description.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/container_description.yml b/.github/workflows/container_description.yml index f922e3a07c..5dfc8efa58 100644 --- a/.github/workflows/container_description.yml +++ b/.github/workflows/container_description.yml @@ -14,6 +14,7 @@ jobs: PushDockerHubReadme: runs-on: ubuntu-latest name: Push README to Docker Hub + if: github.repository == 'prometheus/prometheus' steps: - name: git checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 @@ -33,6 +34,7 @@ jobs: PushQuayIoReadme: runs-on: ubuntu-latest name: Push README to quay.io + if: github.repository == 'prometheus/prometheus' steps: - name: git checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 From 3274cac0d383025a45655977704a7aeea60aef19 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 15 Mar 2024 08:51:57 +0000 Subject: [PATCH 037/127] TSDB: remove unused function Was only used in old WAL implementation. Signed-off-by: Bryan Boreham --- tsdb/db.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tsdb/db.go b/tsdb/db.go index 7aa68da500..33b7e8ab65 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -2225,23 +2225,6 @@ func sequenceFiles(dir string) ([]string, error) { return res, nil } -func nextSequenceFile(dir string) (string, int, error) { - files, err := os.ReadDir(dir) - if err != nil { - return "", 0, err - } - - i := uint64(0) - for _, f := range files { - j, err := strconv.ParseUint(f.Name(), 10, 64) - if err != nil { - continue - } - i = j - } - return filepath.Join(dir, fmt.Sprintf("%0.6d", i+1)), int(i + 1), nil -} - func exponential(d, min, max time.Duration) time.Duration { d *= 2 if d < min { From d45b5deb75d3637a9198153030ad2041b2a7fed9 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 15 Mar 2024 08:54:47 +0000 Subject: [PATCH 038/127] TSDB: move function only used in tests Signed-off-by: Bryan Boreham --- tsdb/block_test.go | 16 ++++++++++++++++ tsdb/db.go | 17 ----------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/tsdb/block_test.go b/tsdb/block_test.go index ad7eb55575..21e20a61c8 100644 --- a/tsdb/block_test.go +++ b/tsdb/block_test.go @@ -209,6 +209,22 @@ func TestCorruptedChunk(t *testing.T) { } } +func sequenceFiles(dir string) ([]string, error) { + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + var res []string + + for _, fi := range files { + if _, err := strconv.ParseUint(fi.Name(), 10, 64); err != nil { + continue + } + res = append(res, filepath.Join(dir, fi.Name())) + } + return res, nil +} + func TestLabelValuesWithMatchers(t *testing.T) { tmpdir := t.TempDir() ctx := context.Background() diff --git a/tsdb/db.go b/tsdb/db.go index 33b7e8ab65..dc77978b7f 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -24,7 +24,6 @@ import ( "os" "path/filepath" "slices" - "strconv" "strings" "sync" "time" @@ -2209,22 +2208,6 @@ func blockDirs(dir string) ([]string, error) { return dirs, nil } -func sequenceFiles(dir string) ([]string, error) { - files, err := os.ReadDir(dir) - if err != nil { - return nil, err - } - var res []string - - for _, fi := range files { - if _, err := strconv.ParseUint(fi.Name(), 10, 64); err != nil { - continue - } - res = append(res, filepath.Join(dir, fi.Name())) - } - return res, nil -} - func exponential(d, min, max time.Duration) time.Duration { d *= 2 if d < min { From cef1025ea80a1a901cf0e2bbe7b589563dd16a8e Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Fri, 15 Mar 2024 10:11:04 +0100 Subject: [PATCH 039/127] tsdb/wlog.Checkpoint: Fix counting of histogram samples Signed-off-by: Arve Knudsen --- tsdb/wlog/checkpoint.go | 4 ++-- tsdb/wlog/checkpoint_test.go | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tsdb/wlog/checkpoint.go b/tsdb/wlog/checkpoint.go index 4ad1bb2365..5491d9718e 100644 --- a/tsdb/wlog/checkpoint.go +++ b/tsdb/wlog/checkpoint.go @@ -224,8 +224,8 @@ func Checkpoint(logger log.Logger, w *WL, from, to int, keep func(id chunks.Head if len(repl) > 0 { buf = enc.HistogramSamples(repl, buf) } - stats.TotalSamples += len(samples) - stats.DroppedSamples += len(samples) - len(repl) + stats.TotalSamples += len(histogramSamples) + stats.DroppedSamples += len(histogramSamples) - len(repl) case record.Tombstones: tstones, err = dec.Tombstones(rec, tstones) diff --git a/tsdb/wlog/checkpoint_test.go b/tsdb/wlog/checkpoint_test.go index 142a5a9d49..0d22171768 100644 --- a/tsdb/wlog/checkpoint_test.go +++ b/tsdb/wlog/checkpoint_test.go @@ -220,12 +220,14 @@ func TestCheckpoint(t *testing.T) { } require.NoError(t, w.Close()) - _, err = Checkpoint(log.NewNopLogger(), w, 100, 106, func(x chunks.HeadSeriesRef) bool { + stats, err := Checkpoint(log.NewNopLogger(), w, 100, 106, func(x chunks.HeadSeriesRef) bool { return x%2 == 0 }, last/2) require.NoError(t, err) require.NoError(t, w.Truncate(107)) require.NoError(t, DeleteCheckpoints(w.Dir(), 106)) + require.Equal(t, histogramsInWAL+samplesInWAL, stats.TotalSamples) + require.Greater(t, stats.DroppedSamples, 0) // Only the new checkpoint should be left. files, err := os.ReadDir(dir) From 53091126c28f518c154c365a47d617c9ee7634de Mon Sep 17 00:00:00 2001 From: Julien Pivotto Date: Wed, 13 Mar 2024 15:36:53 +0100 Subject: [PATCH 040/127] Chunked remote read: close the querier earlier I have seen prometheis instances misebehaving because of broken chinked remote read requests. In order to avoid OOM's when this happens, I propose to close the queries used by the streamed remote read requests earlier. Signed-off-by: Julien Pivotto --- storage/remote/read_handler.go | 53 ++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/storage/remote/read_handler.go b/storage/remote/read_handler.go index ffc64c9c3f..2a00ce897f 100644 --- a/storage/remote/read_handler.go +++ b/storage/remote/read_handler.go @@ -202,34 +202,16 @@ func (h *readHandler) remoteReadStreamedXORChunks(ctx context.Context, w http.Re return err } - querier, err := h.queryable.ChunkQuerier(query.StartTimestampMs, query.EndTimestampMs) - if err != nil { + chunks := h.getChunkSeriesSet(ctx, query, filteredMatchers) + if err := chunks.Err(); err != nil { return err } - defer func() { - if err := querier.Close(); err != nil { - level.Warn(h.logger).Log("msg", "Error on chunk querier close", "err", err.Error()) - } - }() - - var hints *storage.SelectHints - if query.Hints != nil { - hints = &storage.SelectHints{ - Start: query.Hints.StartMs, - End: query.Hints.EndMs, - Step: query.Hints.StepMs, - Func: query.Hints.Func, - Grouping: query.Hints.Grouping, - Range: query.Hints.RangeMs, - By: query.Hints.By, - } - } ws, err := StreamChunkedReadResponses( NewChunkedWriter(w, f), int64(i), // The streaming API has to provide the series sorted. - querier.Select(ctx, true, hints, filteredMatchers...), + chunks, sortedExternalLabels, h.remoteReadMaxBytesInFrame, h.marshalPool, @@ -254,6 +236,35 @@ func (h *readHandler) remoteReadStreamedXORChunks(ctx context.Context, w http.Re } } +// getChunkSeriesSet executes a query to retrieve a ChunkSeriesSet, +// encapsulating the operation in its own function to ensure timely release of +// the querier resources. +func (h *readHandler) getChunkSeriesSet(ctx context.Context, query *prompb.Query, filteredMatchers []*labels.Matcher) storage.ChunkSeriesSet { + querier, err := h.queryable.ChunkQuerier(query.StartTimestampMs, query.EndTimestampMs) + if err != nil { + return storage.ErrChunkSeriesSet(err) + } + defer func() { + if err := querier.Close(); err != nil { + level.Warn(h.logger).Log("msg", "Error on chunk querier close", "err", err.Error()) + } + }() + + var hints *storage.SelectHints + if query.Hints != nil { + hints = &storage.SelectHints{ + Start: query.Hints.StartMs, + End: query.Hints.EndMs, + Step: query.Hints.StepMs, + Func: query.Hints.Func, + Grouping: query.Hints.Grouping, + Range: query.Hints.RangeMs, + By: query.Hints.By, + } + } + return querier.Select(ctx, true, hints, filteredMatchers...) +} + // filterExtLabelsFromMatchers change equality matchers which match external labels // to a matcher that looks for an empty label, // as that label should not be present in the storage. From c8c1ab36dc0ce6fda90e0f0a266e6f3772384ca3 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 15 Mar 2024 16:47:14 +0000 Subject: [PATCH 041/127] MAINTAINERS: Add Bryan Boreham, Ayoub Mrini (#13771) Also simplify structure. Ordering of 'general' maintainers is alphabetical by 2nd name. Signed-off-by: Bryan Boreham --- MAINTAINERS.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index a776eb3594..8113ac5296 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1,6 +1,10 @@ # Maintainers -Julien Pivotto ( / @roidelapluie) and Levi Harrison ( / @LeviHarrison) are the main/default maintainers, some parts of the codebase have other maintainers: +General maintainers: +* Bryan Boreham (bjboreham@gmail.com / @bboreham) +* Levi Harrison (levi@leviharrison.dev / @LeviHarrison) +* Ayoub Mrini (ayoubmrini424@gmail.com / @machine424) +* Julien Pivotto (roidelapluie@prometheus.io / @roidelapluie) * `cmd` * `promtool`: David Leadbeater ( / @dgl) @@ -19,7 +23,6 @@ George Krajcsovits ( / @krajorama) * `module`: Augustin Husson ( @nexucis) * `Makefile` and related build configuration: Simon Pasquier ( / @simonpasquier), Ben Kochie ( / @SuperQ) - For the sake of brevity, not all subtrees are explicitly listed. Due to the size of this repository, the natural changes in focus of maintainers over time, and nuances of where particular features live, this list will always be From b7047f7fcb3fb35c6ebf349ac5b7a65973befcde Mon Sep 17 00:00:00 2001 From: Darshan Chaudhary Date: Sat, 16 Mar 2024 00:05:16 +0530 Subject: [PATCH 042/127] Fix retention boundary so 2h retention deletes blocks right at the 2h boundary (#9633) Signed-off-by: darshanime --- CHANGELOG.md | 4 ++++ tsdb/db.go | 2 +- tsdb/db_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b0350004f..da1979defd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## unreleased + +* [CHANGE] TSDB: Fix the predicate checking for blocks which are beyond the retention period to include the ones right at the retention boundary. #9633 + ## 2.50.1 / 2024-02-26 * [BUGFIX] API: Fix metadata API using wrong field names. #13633 diff --git a/tsdb/db.go b/tsdb/db.go index 4998da6aa7..eaba508558 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -1615,7 +1615,7 @@ func BeyondTimeRetention(db *DB, blocks []*Block) (deletable map[ulid.ULID]struc for i, block := range blocks { // The difference between the first block and this block is larger than // the retention period so any blocks after that are added as deletable. - if i > 0 && blocks[0].Meta().MaxTime-block.Meta().MaxTime > db.opts.RetentionDuration { + if i > 0 && blocks[0].Meta().MaxTime-block.Meta().MaxTime >= db.opts.RetentionDuration { for _, b := range blocks[i:] { deletable[b.meta.ULID] = struct{}{} } diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 45a16b8ba3..9e88877890 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -681,6 +681,34 @@ func TestDB_Snapshot(t *testing.T) { require.Equal(t, 1000.0, sum) } +func TestDB_BeyondTimeRetention(t *testing.T) { + opts := DefaultOptions() + opts.RetentionDuration = 100 + db := openTestDB(t, opts, nil) + defer func() { + require.NoError(t, db.Close()) + }() + + // We have 4 blocks, 3 of which are beyond the retention duration. + metas := []BlockMeta{ + {MinTime: 300, MaxTime: 500}, + {MinTime: 200, MaxTime: 300}, + {MinTime: 100, MaxTime: 200}, + {MinTime: 0, MaxTime: 100}, + } + + for _, m := range metas { + createBlock(t, db.Dir(), genSeries(1, 1, m.MinTime, m.MaxTime)) + } + + // Reloading should truncate the 3 blocks which are >= the retention period. + require.NoError(t, db.reloadBlocks()) + blocks := db.Blocks() + require.Len(t, blocks, 1) + require.Equal(t, metas[0].MinTime, blocks[0].Meta().MinTime) + require.Equal(t, metas[0].MaxTime, blocks[0].Meta().MaxTime) +} + // TestDB_Snapshot_ChunksOutsideOfCompactedRange ensures that a snapshot removes chunks samples // that are outside the set block time range. // See https://github.com/prometheus/prometheus/issues/5105 From 302e151de8937c4c2c77567e33e7c9ae208a2e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sat, 16 Mar 2024 12:06:57 +0100 Subject: [PATCH 043/127] {discovery,remote_write}/azure: Support default SDK authentication (#13099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * discovery/azure: Offer default SDK authentication Signed-off-by: Jan-Otto Kröpke --- config/config_test.go | 2 +- discovery/azure/azure.go | 15 +++++- docs/configuration/configuration.md | 9 +++- storage/remote/azuread/azuread.go | 48 ++++++++++++++++++- storage/remote/azuread/azuread_test.go | 38 +++++++++++++-- .../testdata/azuread_bad_oauthsdkconfig.yaml | 7 +++ .../azuread/testdata/azuread_good_sdk.yaml | 3 ++ 7 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 storage/remote/azuread/testdata/azuread_bad_oauthsdkconfig.yaml create mode 100644 storage/remote/azuread/testdata/azuread_good_sdk.yaml diff --git a/config/config_test.go b/config/config_test.go index 36b125f794..14981d25f0 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1840,7 +1840,7 @@ var expectedErrors = []struct { }, { filename: "azure_authentication_method.bad.yml", - errMsg: "unknown authentication_type \"invalid\". Supported types are \"OAuth\" or \"ManagedIdentity\"", + errMsg: "unknown authentication_type \"invalid\". Supported types are \"OAuth\", \"ManagedIdentity\" or \"SDK\"", }, { filename: "azure_bearertoken_basicauth.bad.yml", diff --git a/discovery/azure/azure.go b/discovery/azure/azure.go index 16628c7bfd..746496a699 100644 --- a/discovery/azure/azure.go +++ b/discovery/azure/azure.go @@ -65,6 +65,7 @@ const ( azureLabelMachineSize = azureLabel + "machine_size" authMethodOAuth = "OAuth" + authMethodSDK = "SDK" authMethodManagedIdentity = "ManagedIdentity" ) @@ -164,8 +165,8 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { } } - if c.AuthenticationMethod != authMethodOAuth && c.AuthenticationMethod != authMethodManagedIdentity { - return fmt.Errorf("unknown authentication_type %q. Supported types are %q or %q", c.AuthenticationMethod, authMethodOAuth, authMethodManagedIdentity) + if c.AuthenticationMethod != authMethodOAuth && c.AuthenticationMethod != authMethodManagedIdentity && c.AuthenticationMethod != authMethodSDK { + return fmt.Errorf("unknown authentication_type %q. Supported types are %q, %q or %q", c.AuthenticationMethod, authMethodOAuth, authMethodManagedIdentity, authMethodSDK) } return c.HTTPClientConfig.Validate() @@ -294,6 +295,16 @@ func newCredential(cfg SDConfig, policyClientOptions policy.ClientOptions) (azco return nil, err } credential = azcore.TokenCredential(secretCredential) + case authMethodSDK: + options := &azidentity.DefaultAzureCredentialOptions{ClientOptions: policyClientOptions} + if len(cfg.TenantID) != 0 { + options.TenantID = cfg.TenantID + } + sdkCredential, err := azidentity.NewDefaultAzureCredential(options) + if err != nil { + return nil, err + } + credential = azcore.TokenCredential(sdkCredential) } return credential, nil } diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index e134ae02b7..d751a4084e 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -600,8 +600,10 @@ See below for the configuration options for Azure discovery: # The Azure environment. [ environment: | default = AzurePublicCloud ] -# The authentication method, either OAuth or ManagedIdentity. +# The authentication method, either OAuth, ManagedIdentity or SDK. # See https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview +# SDK authentication method uses environment variables by default. +# See https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication [ authentication_method: | default = OAuth] # The subscription ID. Always required. subscription_id: @@ -3619,6 +3621,11 @@ azuread: [ client_secret: ] [ tenant_id: ] ] + # Azure SDK auth. + # See https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication + [ sdk: + [ tenant_id: ] ] + # Configures the remote write request's TLS settings. tls_config: [ ] diff --git a/storage/remote/azuread/azuread.go b/storage/remote/azuread/azuread.go index 20d48d0087..e2058fb54d 100644 --- a/storage/remote/azuread/azuread.go +++ b/storage/remote/azuread/azuread.go @@ -61,6 +61,12 @@ type OAuthConfig struct { TenantID string `yaml:"tenant_id,omitempty"` } +// SDKConfig is used to store azure SDK config values. +type SDKConfig struct { + // TenantID is the tenantId of the azure active directory application that is being used to authenticate. + TenantID string `yaml:"tenant_id,omitempty"` +} + // AzureADConfig is used to store the config values. type AzureADConfig struct { //nolint:revive // exported. // ManagedIdentity is the managed identity that is being used to authenticate. @@ -69,6 +75,9 @@ type AzureADConfig struct { //nolint:revive // exported. // OAuth is the oauth config that is being used to authenticate. OAuth *OAuthConfig `yaml:"oauth,omitempty"` + // OAuth is the oauth config that is being used to authenticate. + SDK *SDKConfig `yaml:"sdk,omitempty"` + // Cloud is the Azure cloud in which the service is running. Example: AzurePublic/AzureGovernment/AzureChina. Cloud string `yaml:"cloud,omitempty"` } @@ -102,14 +111,22 @@ func (c *AzureADConfig) Validate() error { return fmt.Errorf("must provide a cloud in the Azure AD config") } - if c.ManagedIdentity == nil && c.OAuth == nil { - return fmt.Errorf("must provide an Azure Managed Identity or Azure OAuth in the Azure AD config") + if c.ManagedIdentity == nil && c.OAuth == nil && c.SDK == nil { + return fmt.Errorf("must provide an Azure Managed Identity, Azure OAuth or Azure SDK in the Azure AD config") } if c.ManagedIdentity != nil && c.OAuth != nil { return fmt.Errorf("cannot provide both Azure Managed Identity and Azure OAuth in the Azure AD config") } + if c.ManagedIdentity != nil && c.SDK != nil { + return fmt.Errorf("cannot provide both Azure Managed Identity and Azure SDK in the Azure AD config") + } + + if c.OAuth != nil && c.SDK != nil { + return fmt.Errorf("cannot provide both Azure OAuth and Azure SDK in the Azure AD config") + } + if c.ManagedIdentity != nil { if c.ManagedIdentity.ClientID == "" { return fmt.Errorf("must provide an Azure Managed Identity client_id in the Azure AD config") @@ -143,6 +160,17 @@ func (c *AzureADConfig) Validate() error { } } + if c.SDK != nil { + var err error + + if c.SDK.TenantID != "" { + _, err = regexp.MatchString("^[0-9a-zA-Z-.]+$", c.SDK.TenantID) + if err != nil { + return fmt.Errorf("the provided Azure OAuth tenant_id is invalid") + } + } + } + return nil } @@ -225,6 +253,16 @@ func newTokenCredential(cfg *AzureADConfig) (azcore.TokenCredential, error) { } } + if cfg.SDK != nil { + sdkConfig := &SDKConfig{ + TenantID: cfg.SDK.TenantID, + } + cred, err = newSDKTokenCredential(clientOpts, sdkConfig) + if err != nil { + return nil, err + } + } + return cred, nil } @@ -241,6 +279,12 @@ func newOAuthTokenCredential(clientOpts *azcore.ClientOptions, oAuthConfig *OAut return azidentity.NewClientSecretCredential(oAuthConfig.TenantID, oAuthConfig.ClientID, oAuthConfig.ClientSecret, opts) } +// newSDKTokenCredential returns new SDK token credential. +func newSDKTokenCredential(clientOpts *azcore.ClientOptions, sdkConfig *SDKConfig) (azcore.TokenCredential, error) { + opts := &azidentity.DefaultAzureCredentialOptions{ClientOptions: *clientOpts, TenantID: sdkConfig.TenantID} + return azidentity.NewDefaultAzureCredential(opts) +} + // newTokenProvider helps to fetch accessToken for different types of credential. This also takes care of // refreshing the accessToken before expiry. This accessToken is attached to the Authorization header while making requests. func newTokenProvider(cfg *AzureADConfig, cred azcore.TokenCredential) (*tokenProvider, error) { diff --git a/storage/remote/azuread/azuread_test.go b/storage/remote/azuread/azuread_test.go index 5eed2c0b19..7c97138120 100644 --- a/storage/remote/azuread/azuread_test.go +++ b/storage/remote/azuread/azuread_test.go @@ -39,7 +39,7 @@ const ( testTokenString = "testTokenString" ) -var testTokenExpiry = time.Now().Add(5 * time.Second) +func testTokenExpiry() time.Time { return time.Now().Add(5 * time.Second) } type AzureAdTestSuite struct { suite.Suite @@ -94,7 +94,7 @@ func (ad *AzureAdTestSuite) TestAzureAdRoundTripper() { testToken := &azcore.AccessToken{ Token: testTokenString, - ExpiresOn: testTokenExpiry, + ExpiresOn: testTokenExpiry(), } ad.mockCredential.On("GetToken", mock.Anything, mock.Anything).Return(*testToken, nil) @@ -145,7 +145,7 @@ func TestAzureAdConfig(t *testing.T) { // Missing managedidentiy or oauth field. { filename: "testdata/azuread_bad_configmissing.yaml", - err: "must provide an Azure Managed Identity or Azure OAuth in the Azure AD config", + err: "must provide an Azure Managed Identity, Azure OAuth or Azure SDK in the Azure AD config", }, // Invalid managedidentity client id. { @@ -162,6 +162,11 @@ func TestAzureAdConfig(t *testing.T) { filename: "testdata/azuread_bad_twoconfig.yaml", err: "cannot provide both Azure Managed Identity and Azure OAuth in the Azure AD config", }, + // Invalid config when both sdk and oauth is provided. + { + filename: "testdata/azuread_bad_oauthsdkconfig.yaml", + err: "cannot provide both Azure OAuth and Azure SDK in the Azure AD config", + }, // Valid config with missing optionally cloud field. { filename: "testdata/azuread_good_cloudmissing.yaml", @@ -174,6 +179,10 @@ func TestAzureAdConfig(t *testing.T) { { filename: "testdata/azuread_good_oauth.yaml", }, + // Valid SDK config. + { + filename: "testdata/azuread_good_sdk.yaml", + }, } for _, c := range cases { _, err := loadAzureAdConfig(c.filename) @@ -232,6 +241,16 @@ func (s *TokenProviderTestSuite) TestNewTokenProvider() { }, err: "Cloud is not specified or is incorrect: ", }, + // Invalid tokenProvider for SDK. + { + cfg: &AzureADConfig{ + Cloud: "PublicAzure", + SDK: &SDKConfig{ + TenantID: dummyTenantID, + }, + }, + err: "Cloud is not specified or is incorrect: ", + }, // Valid tokenProvider for managedidentity. { cfg: &AzureADConfig{ @@ -252,6 +271,15 @@ func (s *TokenProviderTestSuite) TestNewTokenProvider() { }, }, }, + // Valid tokenProvider for SDK. + { + cfg: &AzureADConfig{ + Cloud: "AzurePublic", + SDK: &SDKConfig{ + TenantID: dummyTenantID, + }, + }, + }, } mockGetTokenCallCounter := 1 for _, c := range cases { @@ -264,11 +292,11 @@ func (s *TokenProviderTestSuite) TestNewTokenProvider() { } else { testToken := &azcore.AccessToken{ Token: testTokenString, - ExpiresOn: testTokenExpiry, + ExpiresOn: testTokenExpiry(), } s.mockCredential.On("GetToken", mock.Anything, mock.Anything).Return(*testToken, nil).Once(). - On("GetToken", mock.Anything, mock.Anything).Return(getToken(), nil) + On("GetToken", mock.Anything, mock.Anything).Return(getToken(), nil).Once() actualTokenProvider, actualErr := newTokenProvider(c.cfg, s.mockCredential) diff --git a/storage/remote/azuread/testdata/azuread_bad_oauthsdkconfig.yaml b/storage/remote/azuread/testdata/azuread_bad_oauthsdkconfig.yaml new file mode 100644 index 0000000000..825759d313 --- /dev/null +++ b/storage/remote/azuread/testdata/azuread_bad_oauthsdkconfig.yaml @@ -0,0 +1,7 @@ +cloud: AzurePublic +oauth: + client_id: 00000000-0000-0000-0000-000000000000 + client_secret: Cl1ent$ecret! + tenant_id: 00000000-a12b-3cd4-e56f-000000000000 +sdk: + tenant_id: 00000000-a12b-3cd4-e56f-000000000000 diff --git a/storage/remote/azuread/testdata/azuread_good_sdk.yaml b/storage/remote/azuread/testdata/azuread_good_sdk.yaml new file mode 100644 index 0000000000..53de8897d5 --- /dev/null +++ b/storage/remote/azuread/testdata/azuread_good_sdk.yaml @@ -0,0 +1,3 @@ +cloud: AzurePublic +sdk: + tenant_id: 00000000-a12b-3cd4-e56f-000000000000 From ed9deb1f9080fe4de8d8053ea48ef353a88cc5c5 Mon Sep 17 00:00:00 2001 From: Goutham Date: Sun, 17 Mar 2024 12:23:25 +0100 Subject: [PATCH 044/127] Add Arve as OTLP ingest maintainer Also remove myself :) Arve has been doing a lot of maintainance on the upstream component and also reviewing PRs on Prometheus for this otlp ingest. I continue to have less and less time for this, so I'd like make Arve a maintainer for OTLP Ingestion. Signed-off-by: Goutham --- .github/CODEOWNERS | 2 +- MAINTAINERS.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 41530d4654..7f7cec9cda 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,7 +1,7 @@ /web/ui @juliusv /web/ui/module @juliusv @nexucis /storage/remote @cstyan @bwplotka @tomwilkie -/storage/remote/otlptranslator @gouthamve @jesusvazquez +/storage/remote/otlptranslator @aknuds1 @jesusvazquez /discovery/kubernetes @brancz /tsdb @jesusvazquez /promql @roidelapluie diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 8113ac5296..f23d0eb898 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -16,6 +16,7 @@ General maintainers: George Krajcsovits ( / @krajorama) * `storage` * `remote`: Callum Styan ( / @cstyan), BartƂomiej PƂotka ( / @bwplotka), Tom Wilkie ( / @tomwilkie) + * `otlptranslator`: Arve Knudsen ( / @aknuds1), JesĂșs VĂĄzquez ( / @jesusvazquez) * `tsdb`: Ganesh Vernekar ( / @codesome), BartƂomiej PƂotka ( / @bwplotka), JesĂșs VĂĄzquez ( / @jesusvazquez) * `agent`: Robert Fratto ( / @rfratto) * `web` From 969959895200686eef33874ba2e3b98d698817a1 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Sun, 17 Mar 2024 16:56:12 +0100 Subject: [PATCH 045/127] Improve Labels.Compare performance w/stringlabels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I was bored on a train and I spent some amount of time trying to scratch some nanoseconds off the Labels.Compare when running with stringlabels. I would be ashamed to admit the real amount of time I spent on it. The worst thing is, I can't really explain why this is performing so much better, and someone should re-run the benchmarks on their machine to confirm that it's not something related to general relativity because the train is moving. I also added some extra real-life benchmark cases with longer labelsets (these aren't the longest we have in production, but kubernetes labelsets are fairly common in Prometheus so I thought it would be nice to have them). My benchmarks show this diff: goos: darwin goarch: arm64 pkg: github.com/prometheus/prometheus/model/labels │ old │ new │ │ sec/op │ sec/op vs base │ Labels_Compare/equal 5.898n ± 0% 5.875n ± 1% -0.40% (p=0.037 n=10) Labels_Compare/not_equal 11.78n ± 2% 11.01n ± 1% -6.54% (p=0.000 n=10) Labels_Compare/different_sizes 4.959n ± 1% 4.906n ± 2% -1.05% (p=0.050 n=10) Labels_Compare/lots 21.32n ± 0% 17.54n ± 5% -17.75% (p=0.000 n=10) Labels_Compare/real_long_equal 15.06n ± 1% 14.92n ± 0% -0.93% (p=0.000 n=10) Labels_Compare/real_long_different_end 25.20n ± 0% 24.43n ± 0% -3.04% (p=0.000 n=10) geomean 11.86n 11.25n -5.16% Signed-off-by: Oleg Zaytsev --- model/labels/labels_stringlabels.go | 8 ++------ model/labels/labels_test.go | 10 ++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/model/labels/labels_stringlabels.go b/model/labels/labels_stringlabels.go index 2e718c2b1f..44a2e0fb53 100644 --- a/model/labels/labels_stringlabels.go +++ b/model/labels/labels_stringlabels.go @@ -363,12 +363,8 @@ func Compare(a, b Labels) int { // Now we know that there is some difference before the end of a and b. // Go back through the fields and find which field that difference is in. - firstCharDifferent := i - for i = 0; ; { - size, nextI := decodeSize(a.data, i) - if nextI+size > firstCharDifferent { - break - } + firstCharDifferent, i := i, 0 + for size, nextI := decodeSize(a.data, i); nextI+size <= firstCharDifferent; size, nextI = decodeSize(a.data, i) { i = nextI + size } // Difference is inside this entry. diff --git a/model/labels/labels_test.go b/model/labels/labels_test.go index 49b4b4e67b..cedeb95a6c 100644 --- a/model/labels/labels_test.go +++ b/model/labels/labels_test.go @@ -532,6 +532,16 @@ var comparisonBenchmarkScenarios = []struct { FromStrings("aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii", "jjj", "kkk", "lll", "mmm", "nnn", "ooo", "ppp", "qqq", "rrz"), FromStrings("aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii", "jjj", "kkk", "lll", "mmm", "nnn", "ooo", "ppp", "qqq", "rrr"), }, + { + "real long equal", + FromStrings("__name__", "kube_pod_container_status_last_terminated_exitcode", "cluster", "prod-af-north-0", " container", "prometheus", "instance", "kube-state-metrics-0:kube-state-metrics:ksm", "job", "kube-state-metrics/kube-state-metrics", " namespace", "observability-prometheus", "pod", "observability-prometheus-0", "uid", "d3ec90b2-4975-4607-b45d-b9ad64bb417e"), + FromStrings("__name__", "kube_pod_container_status_last_terminated_exitcode", "cluster", "prod-af-north-0", " container", "prometheus", "instance", "kube-state-metrics-0:kube-state-metrics:ksm", "job", "kube-state-metrics/kube-state-metrics", " namespace", "observability-prometheus", "pod", "observability-prometheus-0", "uid", "d3ec90b2-4975-4607-b45d-b9ad64bb417e"), + }, + { + "real long different end", + FromStrings("__name__", "kube_pod_container_status_last_terminated_exitcode", "cluster", "prod-af-north-0", " container", "prometheus", "instance", "kube-state-metrics-0:kube-state-metrics:ksm", "job", "kube-state-metrics/kube-state-metrics", " namespace", "observability-prometheus", "pod", "observability-prometheus-0", "uid", "d3ec90b2-4975-4607-b45d-b9ad64bb417e"), + FromStrings("__name__", "kube_pod_container_status_last_terminated_exitcode", "cluster", "prod-af-north-0", " container", "prometheus", "instance", "kube-state-metrics-0:kube-state-metrics:ksm", "job", "kube-state-metrics/kube-state-metrics", " namespace", "observability-prometheus", "pod", "observability-prometheus-0", "uid", "deadbeef-0000-1111-2222-b9ad64bb417e"), + }, } func BenchmarkLabels_Equals(b *testing.B) { From b64171536302f41f482967acf331ad3a0df2a93a Mon Sep 17 00:00:00 2001 From: Charles Korn Date: Mon, 18 Mar 2024 10:33:56 +1100 Subject: [PATCH 046/127] Support copying workflow as-is to other Prometheus repositories. Signed-off-by: Charles Korn --- .github/workflows/container_description.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/container_description.yml b/.github/workflows/container_description.yml index 5dfc8efa58..0b8a8cc05f 100644 --- a/.github/workflows/container_description.yml +++ b/.github/workflows/container_description.yml @@ -14,7 +14,7 @@ jobs: PushDockerHubReadme: runs-on: ubuntu-latest name: Push README to Docker Hub - if: github.repository == 'prometheus/prometheus' + if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. steps: - name: git checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 @@ -34,7 +34,7 @@ jobs: PushQuayIoReadme: runs-on: ubuntu-latest name: Push README to quay.io - if: github.repository == 'prometheus/prometheus' + if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. steps: - name: git checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 From 161354e861f32f385d5f2a78c1b532b842eaccf1 Mon Sep 17 00:00:00 2001 From: SuperQ Date: Mon, 18 Mar 2024 08:20:42 +0100 Subject: [PATCH 047/127] Add container_description.yml to force sync list Since we skip repos that don't have a Dockerfile, we can force sync the `.github/workflows/container_description.yml` config. Signed-off-by: SuperQ --- scripts/sync_repo_files.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sync_repo_files.sh b/scripts/sync_repo_files.sh index a46f289c49..6459fb1e7a 100755 --- a/scripts/sync_repo_files.sh +++ b/scripts/sync_repo_files.sh @@ -144,7 +144,7 @@ process_repo() { if [[ -z "${target_file}" ]]; then echo "${target_filename} doesn't exist in ${org_repo}" case "${source_file}" in - CODE_OF_CONDUCT.md | SECURITY.md) + CODE_OF_CONDUCT.md | SECURITY.md | .github/workflows/container_description.yml) echo "${source_file} missing in ${org_repo}, force updating." needs_update+=("${source_file}") ;; From d12e785075ed42f0d0edafe7018e2d512d0f8ce6 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Mon, 18 Mar 2024 11:16:09 +0100 Subject: [PATCH 048/127] Improve readability As suggested by @bboreham Signed-off-by: Oleg Zaytsev --- model/labels/labels_stringlabels.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/model/labels/labels_stringlabels.go b/model/labels/labels_stringlabels.go index 44a2e0fb53..9ef764daec 100644 --- a/model/labels/labels_stringlabels.go +++ b/model/labels/labels_stringlabels.go @@ -364,8 +364,10 @@ func Compare(a, b Labels) int { // Now we know that there is some difference before the end of a and b. // Go back through the fields and find which field that difference is in. firstCharDifferent, i := i, 0 - for size, nextI := decodeSize(a.data, i); nextI+size <= firstCharDifferent; size, nextI = decodeSize(a.data, i) { + size, nextI := decodeSize(a.data, i) + for nextI+size <= firstCharDifferent { i = nextI + size + size, nextI = decodeSize(a.data, i) } // Difference is inside this entry. aStr, _ := decodeString(a.data, i) From 9c7a7340630b61d296bb770e6d450b09be759878 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Tue, 19 Mar 2024 09:10:21 +0100 Subject: [PATCH 049/127] tsdb.BeyondTimeRetention: Fix comment and test at retention duration Signed-off-by: Arve Knudsen --- tsdb/db.go | 2 +- tsdb/db_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tsdb/db.go b/tsdb/db.go index e49c5811d2..f83c7c4958 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -1608,7 +1608,7 @@ func BeyondTimeRetention(db *DB, blocks []*Block) (deletable map[ulid.ULID]struc deletable = make(map[ulid.ULID]struct{}) for i, block := range blocks { - // The difference between the first block and this block is larger than + // The difference between the first block and this block is greater than or equal to // the retention period so any blocks after that are added as deletable. if i > 0 && blocks[0].Meta().MaxTime-block.Meta().MaxTime >= db.opts.RetentionDuration { for _, b := range blocks[i:] { diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 498e26c588..c7ea564d0e 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -689,10 +689,10 @@ func TestDB_BeyondTimeRetention(t *testing.T) { require.NoError(t, db.Close()) }() - // We have 4 blocks, 3 of which are beyond the retention duration. + // We have 4 blocks, 3 of which are beyond or at the retention duration. metas := []BlockMeta{ {MinTime: 300, MaxTime: 500}, - {MinTime: 200, MaxTime: 300}, + {MinTime: 200, MaxTime: 400}, {MinTime: 100, MaxTime: 200}, {MinTime: 0, MaxTime: 100}, } From 45c2b111871d569273d10484c65bc108d6dfa5ac Mon Sep 17 00:00:00 2001 From: SuperQ Date: Tue, 19 Mar 2024 06:53:10 +0100 Subject: [PATCH 050/127] Fixup make target in container description Use `docker-repo-name` as the make target so that a repo Makfile can override the common target implementation. Signed-off-by: SuperQ --- .github/workflows/container_description.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/container_description.yml b/.github/workflows/container_description.yml index 0b8a8cc05f..8a57107dd9 100644 --- a/.github/workflows/container_description.yml +++ b/.github/workflows/container_description.yml @@ -19,7 +19,7 @@ jobs: - name: git checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set docker hub repo name - run: echo "DOCKER_REPO_NAME=$(make common-docker-repo-name)" >> $GITHUB_ENV + run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV - name: Push README to Dockerhub uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 env: @@ -41,7 +41,7 @@ jobs: - name: Set quay.io org name run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV - name: Set quay.io repo name - run: echo "DOCKER_REPO_NAME=$(make common-docker-repo-name)" >> $GITHUB_ENV + run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV - name: Push README to quay.io uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 env: @@ -50,6 +50,3 @@ jobs: destination_container_repo: ${{ env.DOCKER_REPO_NAME }} provider: quay readme_file: 'README.md' - - - From e14af52e485e6fd7e9f257ff19a08bc1aabc6dad Mon Sep 17 00:00:00 2001 From: beorn7 Date: Tue, 19 Mar 2024 18:23:55 +0100 Subject: [PATCH 051/127] Tweak MAINTAINERS.md After #13771, the list for specific parts of the codebase looks like it is part of the "general maintainers" list. This commit makes things clearer. Signed-off-by: beorn7 --- MAINTAINERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 8113ac5296..2cb65c06d9 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -6,6 +6,7 @@ General maintainers: * Ayoub Mrini (ayoubmrini424@gmail.com / @machine424) * Julien Pivotto (roidelapluie@prometheus.io / @roidelapluie) +Maintainers for specific parts of the codebase: * `cmd` * `promtool`: David Leadbeater ( / @dgl) * `discovery` From 742196b6c4918d9d08b8349182b12471d9b223e1 Mon Sep 17 00:00:00 2001 From: Erlan Zholdubai uulu Date: Tue, 19 Mar 2024 10:37:43 -0700 Subject: [PATCH 052/127] add context cancellation check at get series result iteration (#13766) * add context cancellation check at get series iteration * add warnings and closer on error * add test --------- Signed-off-by: Erlan Zholdubai uulu --- web/api/v1/api.go | 3 +++ web/api/v1/api_test.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/web/api/v1/api.go b/web/api/v1/api.go index b56026e45e..dc22365073 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -882,6 +882,9 @@ func (api *API) series(r *http.Request) (result apiFuncResult) { warnings := set.Warnings() for set.Next() { + if err := ctx.Err(); err != nil { + return apiFuncResult{nil, returnAPIError(err), warnings, closer} + } metrics = append(metrics, set.At().Labels()) if len(metrics) >= limit { diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index 63a0225357..4158e544ef 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -3568,6 +3568,9 @@ func TestReturnAPIError(t *testing.T) { }, { err: errors.New("exec error"), expected: errorExec, + }, { + err: context.Canceled, + expected: errorCanceled, }, } From 11fc7b1d835e04a7f426841fd0cc8ab178d12717 Mon Sep 17 00:00:00 2001 From: dongjiang Date: Wed, 20 Mar 2024 02:24:37 +0800 Subject: [PATCH 053/127] chores: bump to golangci-lint v1.56.2 (#13753) bump to golangci-lint to v1.56.2 --------- Signed-off-by: dongjiang1989 --- .github/workflows/ci.yml | 2 +- Makefile.common | 2 +- scripts/golangci-lint.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e4b628699..8303cad528 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,7 +162,7 @@ jobs: with: args: --verbose # Make sure to sync this with Makefile.common and scripts/golangci-lint.yml. - version: v1.55.2 + version: v1.56.2 fuzzing: uses: ./.github/workflows/fuzzing.yml if: github.event_name == 'pull_request' diff --git a/Makefile.common b/Makefile.common index 49ed5f5478..483b3bf9df 100644 --- a/Makefile.common +++ b/Makefile.common @@ -61,7 +61,7 @@ PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_ SKIP_GOLANGCI_LINT := GOLANGCI_LINT := GOLANGCI_LINT_OPTS ?= -GOLANGCI_LINT_VERSION ?= v1.55.2 +GOLANGCI_LINT_VERSION ?= v1.56.2 # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. # windows isn't included here because of the path separator being different. ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) diff --git a/scripts/golangci-lint.yml b/scripts/golangci-lint.yml index 4dc7b830f6..5670d6079b 100644 --- a/scripts/golangci-lint.yml +++ b/scripts/golangci-lint.yml @@ -35,4 +35,4 @@ jobs: - name: Lint uses: golangci/golangci-lint-action@3cfe3a4abbb849e10058ce4af15d205b6da42804 # v4.0.0 with: - version: v1.55.2 + version: v1.56.2 From af694dc29549aee7862893b9916df1560b293f66 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Wed, 20 Mar 2024 09:07:16 +0100 Subject: [PATCH 054/127] Merge TestDB_BeyondTimeRetention into TestTimeRetention Signed-off-by: Arve Knudsen --- tsdb/db_test.go | 40 ++++++++-------------------------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/tsdb/db_test.go b/tsdb/db_test.go index c7ea564d0e..50d5141bd5 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -681,34 +681,6 @@ func TestDB_Snapshot(t *testing.T) { require.Equal(t, 1000.0, sum) } -func TestDB_BeyondTimeRetention(t *testing.T) { - opts := DefaultOptions() - opts.RetentionDuration = 100 - db := openTestDB(t, opts, nil) - defer func() { - require.NoError(t, db.Close()) - }() - - // We have 4 blocks, 3 of which are beyond or at the retention duration. - metas := []BlockMeta{ - {MinTime: 300, MaxTime: 500}, - {MinTime: 200, MaxTime: 400}, - {MinTime: 100, MaxTime: 200}, - {MinTime: 0, MaxTime: 100}, - } - - for _, m := range metas { - createBlock(t, db.Dir(), genSeries(1, 1, m.MinTime, m.MaxTime)) - } - - // Reloading should truncate the 3 blocks which are >= the retention period. - require.NoError(t, db.reloadBlocks()) - blocks := db.Blocks() - require.Len(t, blocks, 1) - require.Equal(t, metas[0].MinTime, blocks[0].Meta().MinTime) - require.Equal(t, metas[0].MaxTime, blocks[0].Meta().MaxTime) -} - // TestDB_Snapshot_ChunksOutsideOfCompactedRange ensures that a snapshot removes chunks samples // that are outside the set block time range. // See https://github.com/prometheus/prometheus/issues/5105 @@ -1497,7 +1469,8 @@ func TestTimeRetention(t *testing.T) { }() blocks := []*BlockMeta{ - {MinTime: 500, MaxTime: 900}, // Oldest block + {MinTime: 500, MaxTime: 900}, // Oldest block + {MinTime: 500, MaxTime: 1000}, // Coinciding exactly with the retention duration. {MinTime: 1000, MaxTime: 1500}, {MinTime: 1500, MaxTime: 2000}, // Newest Block } @@ -1509,14 +1482,17 @@ func TestTimeRetention(t *testing.T) { require.NoError(t, db.reloadBlocks()) // Reload the db to register the new blocks. require.Equal(t, len(blocks), len(db.Blocks())) // Ensure all blocks are registered. - db.opts.RetentionDuration = blocks[2].MaxTime - blocks[1].MinTime + // By setting retention duration as follows, we verify that also blocks[1], + // coinciding exactly with the boundary, is deleted. + db.opts.RetentionDuration = blocks[3].MaxTime - blocks[1].MaxTime + // Reloading should truncate the blocks which are >= the retention duration. require.NoError(t, db.reloadBlocks()) - expBlocks := blocks[1:] + expBlocks := blocks[2:] actBlocks := db.Blocks() require.Equal(t, 1, int(prom_testutil.ToFloat64(db.metrics.timeRetentionCount)), "metric retention count mismatch") - require.Equal(t, len(expBlocks), len(actBlocks)) + require.Len(t, actBlocks, len(expBlocks)) require.Equal(t, expBlocks[0].MaxTime, actBlocks[0].meta.MaxTime) require.Equal(t, expBlocks[len(expBlocks)-1].MaxTime, actBlocks[len(actBlocks)-1].meta.MaxTime) } From 07332f7427ed9a1d6a04c124ae545eb3989134d6 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Wed, 20 Mar 2024 14:58:09 +0100 Subject: [PATCH 055/127] TestTimeRetention: Split into two sub-tests Signed-off-by: Arve Knudsen --- tsdb/db_test.go | 86 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 50d5141bd5..ce5efd0e43 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -1463,38 +1463,66 @@ func (*mockCompactorFailing) CompactOOO(string, *OOOCompactionHead) (result []ul } func TestTimeRetention(t *testing.T) { - db := openTestDB(t, nil, []int64{1000}) - defer func() { - require.NoError(t, db.Close()) - }() - - blocks := []*BlockMeta{ - {MinTime: 500, MaxTime: 900}, // Oldest block - {MinTime: 500, MaxTime: 1000}, // Coinciding exactly with the retention duration. - {MinTime: 1000, MaxTime: 1500}, - {MinTime: 1500, MaxTime: 2000}, // Newest Block + testCases := []struct { + name string + blocks []*BlockMeta + expBlocks []*BlockMeta + retentionDuration int64 + }{ + { + name: "Block max time delta greater than retention duration", + blocks: []*BlockMeta{ + {MinTime: 500, MaxTime: 900}, // Oldest block, beyond retention + {MinTime: 1000, MaxTime: 1500}, + {MinTime: 1500, MaxTime: 2000}, // Newest block + }, + expBlocks: []*BlockMeta{ + {MinTime: 1000, MaxTime: 1500}, + {MinTime: 1500, MaxTime: 2000}, + }, + retentionDuration: 1000, + }, + { + name: "Block max time delta equal to retention duration", + blocks: []*BlockMeta{ + {MinTime: 500, MaxTime: 900}, // Oldest block + {MinTime: 1000, MaxTime: 1500}, // Coinciding exactly with the retention duration. + {MinTime: 1500, MaxTime: 2000}, // Newest block + }, + expBlocks: []*BlockMeta{ + {MinTime: 1500, MaxTime: 2000}, + }, + retentionDuration: 500, + }, } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + db := openTestDB(t, nil, []int64{1000}) + defer func() { + require.NoError(t, db.Close()) + }() - for _, m := range blocks { - createBlock(t, db.Dir(), genSeries(10, 10, m.MinTime, m.MaxTime)) + for _, m := range tc.blocks { + createBlock(t, db.Dir(), genSeries(10, 10, m.MinTime, m.MaxTime)) + } + + require.NoError(t, db.reloadBlocks()) // Reload the db to register the new blocks. + require.Len(t, db.Blocks(), len(tc.blocks)) // Ensure all blocks are registered. + + db.opts.RetentionDuration = tc.retentionDuration + // Reloading should truncate the blocks which are >= the retention duration vs the first block. + require.NoError(t, db.reloadBlocks()) + + actBlocks := db.Blocks() + + require.Equal(t, 1, int(prom_testutil.ToFloat64(db.metrics.timeRetentionCount)), "metric retention count mismatch") + require.Len(t, actBlocks, len(tc.expBlocks)) + for i, eb := range tc.expBlocks { + require.Equal(t, eb.MinTime, actBlocks[i].meta.MinTime) + require.Equal(t, eb.MaxTime, actBlocks[i].meta.MaxTime) + } + }) } - - require.NoError(t, db.reloadBlocks()) // Reload the db to register the new blocks. - require.Equal(t, len(blocks), len(db.Blocks())) // Ensure all blocks are registered. - - // By setting retention duration as follows, we verify that also blocks[1], - // coinciding exactly with the boundary, is deleted. - db.opts.RetentionDuration = blocks[3].MaxTime - blocks[1].MaxTime - // Reloading should truncate the blocks which are >= the retention duration. - require.NoError(t, db.reloadBlocks()) - - expBlocks := blocks[2:] - actBlocks := db.Blocks() - - require.Equal(t, 1, int(prom_testutil.ToFloat64(db.metrics.timeRetentionCount)), "metric retention count mismatch") - require.Len(t, actBlocks, len(expBlocks)) - require.Equal(t, expBlocks[0].MaxTime, actBlocks[0].meta.MaxTime) - require.Equal(t, expBlocks[len(expBlocks)-1].MaxTime, actBlocks[len(actBlocks)-1].meta.MaxTime) } func TestRetentionDurationMetric(t *testing.T) { From 2a2e2ed28bc9f1f9205e874ed1be705e765ecb9b Mon Sep 17 00:00:00 2001 From: machine424 Date: Wed, 20 Mar 2024 14:09:21 +0100 Subject: [PATCH 056/127] chore(tsdb): set the wbl to nil as well in DBReadOnly.loadDataAsQueryable Signed-off-by: machine424 --- tsdb/db.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsdb/db.go b/tsdb/db.go index e49c5811d2..28cecde4f4 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -529,9 +529,10 @@ func (db *DBReadOnly) loadDataAsQueryable(maxt int64) (storage.SampleAndChunkQue if err := head.Init(maxBlockTime); err != nil { return nil, fmt.Errorf("read WAL: %w", err) } - // Set the wal to nil to disable all wal operations. + // Set the wal and the wbl to nil to disable related operations. // This is mainly to avoid blocking when closing the head. head.wal = nil + head.wbl = nil } db.closers = append(db.closers, head) From f6834c347ad64d2768b60b60f411597a467b4b64 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Wed, 20 Mar 2024 22:01:05 +0300 Subject: [PATCH 057/127] promql: validate `label_join` destination label Signed-off-by: tdakkota --- promql/functions.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/promql/functions.go b/promql/functions.go index da66af2f02..d5840374e7 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -1386,6 +1386,9 @@ func (ev *evaluator) evalLabelJoin(args parser.Expressions) (parser.Value, annot } srcLabels[i-3] = src } + if !model.LabelName(dst).IsValid() { + panic(fmt.Errorf("invalid destination label name in label_join(): %s", dst)) + } val, ws := ev.eval(args[0]) matrix := val.(Matrix) From c7ca85388f0adbc166902757aef6184e73355034 Mon Sep 17 00:00:00 2001 From: heyitao Date: Thu, 21 Mar 2024 11:32:02 +0800 Subject: [PATCH 058/127] Fix yaml file format and clear ci errors Signed-off-by: heyitao --- .github/dependabot.yml | 8 ++-- .golangci.yml | 37 +++++++++---------- .yamllint | 2 + .../testdata/no-test-group-interval.yml | 2 +- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9617e04a48..3d56ff2b22 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,11 +6,11 @@ updates: interval: "monthly" groups: k8s.io: - patterns: - - "k8s.io/*" + patterns: + - "k8s.io/*" go.opentelemetry.io: - patterns: - - "go.opentelemetry.io/*" + patterns: + - "go.opentelemetry.io/*" - package-ecosystem: "gomod" directory: "/documentation/examples/remote_storage" schedule: diff --git a/.golangci.yml b/.golangci.yml index 2eeb6c1c8f..42410aebf8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -48,24 +48,24 @@ linters-settings: rules: main: deny: - - pkg: "sync/atomic" - desc: "Use go.uber.org/atomic instead of sync/atomic" - - pkg: "github.com/stretchr/testify/assert" - desc: "Use github.com/stretchr/testify/require instead of github.com/stretchr/testify/assert" - - pkg: "github.com/go-kit/kit/log" - desc: "Use github.com/go-kit/log instead of github.com/go-kit/kit/log" - - pkg: "io/ioutil" - desc: "Use corresponding 'os' or 'io' functions instead." - - pkg: "regexp" - desc: "Use github.com/grafana/regexp instead of regexp" - - pkg: "github.com/pkg/errors" - desc: "Use 'errors' or 'fmt' instead of github.com/pkg/errors" - - pkg: "gzip" - desc: "Use github.com/klauspost/compress instead of gzip" - - pkg: "zlib" - desc: "Use github.com/klauspost/compress instead of zlib" - - pkg: "golang.org/x/exp/slices" - desc: "Use 'slices' instead." + - pkg: "sync/atomic" + desc: "Use go.uber.org/atomic instead of sync/atomic" + - pkg: "github.com/stretchr/testify/assert" + desc: "Use github.com/stretchr/testify/require instead of github.com/stretchr/testify/assert" + - pkg: "github.com/go-kit/kit/log" + desc: "Use github.com/go-kit/log instead of github.com/go-kit/kit/log" + - pkg: "io/ioutil" + desc: "Use corresponding 'os' or 'io' functions instead." + - pkg: "regexp" + desc: "Use github.com/grafana/regexp instead of regexp" + - pkg: "github.com/pkg/errors" + desc: "Use 'errors' or 'fmt' instead of github.com/pkg/errors" + - pkg: "gzip" + desc: "Use github.com/klauspost/compress instead of gzip" + - pkg: "zlib" + desc: "Use github.com/klauspost/compress instead of zlib" + - pkg: "golang.org/x/exp/slices" + desc: "Use 'slices' instead." errcheck: exclude-functions: # Don't flag lines such as "io.Copy(io.Discard, resp.Body)". @@ -135,4 +135,3 @@ linters-settings: - require-error - suite-dont-use-pkg - suite-extra-assert-call - diff --git a/.yamllint b/.yamllint index 955a5a6270..1859cb624b 100644 --- a/.yamllint +++ b/.yamllint @@ -1,5 +1,7 @@ --- extends: default +ignore: | + ui/react-app/node_modules rules: braces: diff --git a/cmd/promtool/testdata/no-test-group-interval.yml b/cmd/promtool/testdata/no-test-group-interval.yml index d1f6935cd6..99f2ec6467 100644 --- a/cmd/promtool/testdata/no-test-group-interval.yml +++ b/cmd/promtool/testdata/no-test-group-interval.yml @@ -12,4 +12,4 @@ tests: eval_time: 1m exp_samples: - value: 1 - labels: test \ No newline at end of file + labels: test From d9e43535202ef6a5846d62ac360cc5763559bb8b Mon Sep 17 00:00:00 2001 From: heyitao Date: Thu, 21 Mar 2024 11:32:23 +0800 Subject: [PATCH 059/127] Fix binary detection in Makefile Signed-off-by: heyitao --- Makefile | 2 +- Makefile.common | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index ab229f9311..d78813068e 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,7 @@ assets-tarball: assets .PHONY: parser parser: @echo ">> running goyacc to generate the .go file." -ifeq (, $(shell command -v goyacc > /dev/null)) +ifeq (, $(shell command -v goyacc 2> /dev/null)) @echo "goyacc not installed so skipping" @echo "To install: go install golang.org/x/tools/cmd/goyacc@v0.6.0" else diff --git a/Makefile.common b/Makefile.common index 483b3bf9df..0acfb9d806 100644 --- a/Makefile.common +++ b/Makefile.common @@ -49,7 +49,7 @@ endif GOTEST := $(GO) test GOTEST_DIR := ifneq ($(CIRCLE_JOB),) -ifneq ($(shell command -v gotestsum > /dev/null),) +ifneq ($(shell command -v gotestsum 2> /dev/null),) GOTEST_DIR := test-results GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml -- endif @@ -182,7 +182,7 @@ endif .PHONY: common-yamllint common-yamllint: @echo ">> running yamllint on all YAML files in the repository" -ifeq (, $(shell command -v yamllint > /dev/null)) +ifeq (, $(shell command -v yamllint 2> /dev/null)) @echo "yamllint not installed so skipping" else yamllint . From a192d4a04b3e0479cf9d00696300f5a15c083b6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Mar 2024 08:14:12 +0000 Subject: [PATCH 060/127] build(deps): bump actions/checkout from 4.1.1 to 4.1.2 in /scripts Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.1 to 4.1.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/b4ffde65f46336ab88eb53be808477a3936bae11...9bb56186c3b09b4f86b1c65136769dd318469633) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- scripts/golangci-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/golangci-lint.yml b/scripts/golangci-lint.yml index 5670d6079b..a7a40c1be5 100644 --- a/scripts/golangci-lint.yml +++ b/scripts/golangci-lint.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - name: install Go uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: From b5a2331c69771951aa380cfaade068b6ccd89a04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Mar 2024 08:14:18 +0000 Subject: [PATCH 061/127] build(deps): bump golangci/golangci-lint-action from 3.7.0 to 4.0.0 Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3.7.0 to 4.0.0. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/3a919529898de77ec3da873e3063ca4b10e7f5cc...3cfe3a4abbb849e10058ce4af15d205b6da42804) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8303cad528..498db12b6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -158,7 +158,7 @@ jobs: run: sudo apt-get update && sudo apt-get -y install libsnmp-dev if: github.repository == 'prometheus/snmp_exporter' - name: Lint - uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0 + uses: golangci/golangci-lint-action@3cfe3a4abbb849e10058ce4af15d205b6da42804 # v4.0.0 with: args: --verbose # Make sure to sync this with Makefile.common and scripts/golangci-lint.yml. From 4eadb42c435fa989171400d59455efbccaef7662 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Mar 2024 08:14:24 +0000 Subject: [PATCH 062/127] build(deps): bump bufbuild/buf-setup-action from 1.28.1 to 1.30.0 Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.28.1 to 1.30.0. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](https://github.com/bufbuild/buf-setup-action/compare/382440cdb8ec7bc25a68d7b4711163d95f7cc3aa...517ee23296d5caf38df31c21945e6a54bbc8a89f) --- updated-dependencies: - dependency-name: bufbuild/buf-setup-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/buf-lint.yml | 2 +- .github/workflows/buf.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/buf-lint.yml b/.github/workflows/buf-lint.yml index 91de37aa6f..942db6e9b2 100644 --- a/.github/workflows/buf-lint.yml +++ b/.github/workflows/buf-lint.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - uses: bufbuild/buf-setup-action@382440cdb8ec7bc25a68d7b4711163d95f7cc3aa # v1.28.1 + - uses: bufbuild/buf-setup-action@517ee23296d5caf38df31c21945e6a54bbc8a89f # v1.30.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} - uses: bufbuild/buf-lint-action@044d13acb1f155179c606aaa2e53aea304d22058 # v1.1.0 diff --git a/.github/workflows/buf.yml b/.github/workflows/buf.yml index 04d4ed8682..9bbfd236e7 100644 --- a/.github/workflows/buf.yml +++ b/.github/workflows/buf.yml @@ -13,7 +13,7 @@ jobs: if: github.repository_owner == 'prometheus' steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - uses: bufbuild/buf-setup-action@382440cdb8ec7bc25a68d7b4711163d95f7cc3aa # v1.28.1 + - uses: bufbuild/buf-setup-action@517ee23296d5caf38df31c21945e6a54bbc8a89f # v1.30.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} - uses: bufbuild/buf-lint-action@044d13acb1f155179c606aaa2e53aea304d22058 # v1.1.0 From 33138174d454399be72942bb80b93d96d14d63ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Mar 2024 08:15:04 +0000 Subject: [PATCH 063/127] build(deps): bump the k8s-io group with 3 updates Bumps the k8s-io group with 3 updates: [k8s.io/api](https://github.com/kubernetes/api), [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) and [k8s.io/client-go](https://github.com/kubernetes/client-go). Updates `k8s.io/api` from 0.29.2 to 0.29.3 - [Commits](https://github.com/kubernetes/api/compare/v0.29.2...v0.29.3) Updates `k8s.io/apimachinery` from 0.29.2 to 0.29.3 - [Commits](https://github.com/kubernetes/apimachinery/compare/v0.29.2...v0.29.3) Updates `k8s.io/client-go` from 0.29.2 to 0.29.3 - [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/kubernetes/client-go/compare/v0.29.2...v0.29.3) --- updated-dependencies: - dependency-name: k8s.io/api dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/apimachinery dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/client-go dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io ... Signed-off-by: dependabot[bot] --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 8039a16af4..1e2ea84551 100644 --- a/go.mod +++ b/go.mod @@ -84,12 +84,12 @@ require ( google.golang.org/api v0.168.0 google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8 google.golang.org/grpc v1.62.1 - google.golang.org/protobuf v1.32.0 + google.golang.org/protobuf v1.33.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.29.2 - k8s.io/apimachinery v0.29.2 - k8s.io/client-go v0.29.2 + k8s.io/api v0.29.3 + k8s.io/apimachinery v0.29.3 + k8s.io/client-go v0.29.3 k8s.io/klog v1.0.0 k8s.io/klog/v2 v2.120.1 ) @@ -134,7 +134,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.0 // indirect github.com/golang/glog v1.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect diff --git a/go.sum b/go.sum index 599f22a1f7..3fb337c860 100644 --- a/go.sum +++ b/go.sum @@ -280,8 +280,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -1118,8 +1118,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1161,12 +1161,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= -k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0= -k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= -k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= -k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= -k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA= +k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= +k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= +k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= +k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= +k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= +k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= From 9bdb6f6554768683d780ca4fb6c371c6ae0af59c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Mar 2024 08:15:15 +0000 Subject: [PATCH 064/127] build(deps): bump github.com/linode/linodego from 1.29.0 to 1.30.0 Bumps [github.com/linode/linodego](https://github.com/linode/linodego) from 1.29.0 to 1.30.0. - [Release notes](https://github.com/linode/linodego/releases) - [Commits](https://github.com/linode/linodego/compare/v1.29.0...v1.30.0) --- updated-dependencies: - dependency-name: github.com/linode/linodego dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 8039a16af4..dfa7098801 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/klauspost/compress v1.17.7 github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b - github.com/linode/linodego v1.29.0 + github.com/linode/linodego v1.30.0 github.com/miekg/dns v1.1.58 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f diff --git a/go.sum b/go.sum index 599f22a1f7..9de7ed96a1 100644 --- a/go.sum +++ b/go.sum @@ -424,8 +424,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/ionos-cloud/sdk-go/v6 v6.1.11 h1:J/uRN4UWO3wCyGOeDdMKv8LWRzKu6UIkLEaes38Kzh8= github.com/ionos-cloud/sdk-go/v6 v6.1.11/go.mod h1:EzEgRIDxBELvfoa/uBN0kOQaqovLjUWEB7iW4/Q+t4k= -github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= -github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= +github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -471,8 +471,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/linode/linodego v1.29.0 h1:gDSQWAbKMAQX8db9FDCXHhodQPrJmLcmthjx6m+PyV4= -github.com/linode/linodego v1.29.0/go.mod h1:3k6WvCM10gillgYcnoLqIL23ST27BD9HhMsCJWb3Bpk= +github.com/linode/linodego v1.30.0 h1:6HJli+LX7NGu+Sne2G+ux790EkVOWOV/SR4mK3jcs6k= +github.com/linode/linodego v1.30.0/go.mod h1:/46h/XpmWi//oSA92GX2p3FIxb8HbX7grslPPQalR2o= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= From 7ec4a11472648e994b1ebe4dfe80497d43a05455 Mon Sep 17 00:00:00 2001 From: David Leadbeater Date: Thu, 21 Mar 2024 20:23:40 +1100 Subject: [PATCH 065/127] promtool: Avoid using testify for user rule tests Using testify outside of unit tests results in panics rather than a useful error for the user. Fixes #13703 Signed-off-by: David Leadbeater --- cmd/promtool/unittest.go | 11 ++++++++--- promql/test.go | 29 +++++++++++++++++------------ promql/test_test.go | 2 +- util/teststorage/storage.go | 23 +++++++++++++++++++---- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/cmd/promtool/unittest.go b/cmd/promtool/unittest.go index 4777b88098..6d6683a934 100644 --- a/cmd/promtool/unittest.go +++ b/cmd/promtool/unittest.go @@ -175,13 +175,18 @@ type testGroup struct { } // test performs the unit tests. -func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]int, queryOpts promql.LazyLoaderOpts, diffFlag bool, ruleFiles ...string) []error { +func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]int, queryOpts promql.LazyLoaderOpts, diffFlag bool, ruleFiles ...string) (outErr []error) { // Setup testing suite. - suite, err := promql.NewLazyLoader(nil, tg.seriesLoadingString(), queryOpts) + suite, err := promql.NewLazyLoader(tg.seriesLoadingString(), queryOpts) if err != nil { return []error{err} } - defer suite.Close() + defer func() { + err := suite.Close() + if err != nil { + outErr = append(outErr, err) + } + }() suite.SubqueryInterval = evalInterval // Load the rule files. diff --git a/promql/test.go b/promql/test.go index 589b1e5b6b..c45cfd8a51 100644 --- a/promql/test.go +++ b/promql/test.go @@ -704,8 +704,6 @@ func parseNumber(s string) (float64, error) { // LazyLoader lazily loads samples into storage. // This is specifically implemented for unit testing of rules. type LazyLoader struct { - testutil.T - loadCmd *loadCmd storage storage.Storage @@ -727,13 +725,15 @@ type LazyLoaderOpts struct { } // NewLazyLoader returns an initialized empty LazyLoader. -func NewLazyLoader(t testutil.T, input string, opts LazyLoaderOpts) (*LazyLoader, error) { +func NewLazyLoader(input string, opts LazyLoaderOpts) (*LazyLoader, error) { ll := &LazyLoader{ - T: t, opts: opts, } err := ll.parse(input) - ll.clear() + if err != nil { + return nil, err + } + err = ll.clear() return ll, err } @@ -761,15 +761,20 @@ func (ll *LazyLoader) parse(input string) error { } // clear the current test storage of all inserted samples. -func (ll *LazyLoader) clear() { +func (ll *LazyLoader) clear() error { if ll.storage != nil { - err := ll.storage.Close() - require.NoError(ll.T, err, "Unexpected error while closing test storage.") + if err := ll.storage.Close(); err != nil { + return fmt.Errorf("closing test storage: %w", err) + } } if ll.cancelCtx != nil { ll.cancelCtx() } - ll.storage = teststorage.New(ll) + var err error + ll.storage, err = teststorage.NewWithError() + if err != nil { + return err + } opts := EngineOpts{ Logger: nil, @@ -783,6 +788,7 @@ func (ll *LazyLoader) clear() { ll.queryEngine = NewEngine(opts) ll.context, ll.cancelCtx = context.WithCancel(context.Background()) + return nil } // appendTill appends the defined time series to the storage till the given timestamp (in milliseconds). @@ -836,8 +842,7 @@ func (ll *LazyLoader) Storage() storage.Storage { } // Close closes resources associated with the LazyLoader. -func (ll *LazyLoader) Close() { +func (ll *LazyLoader) Close() error { ll.cancelCtx() - err := ll.storage.Close() - require.NoError(ll.T, err, "Unexpected error while closing test storage.") + return ll.storage.Close() } diff --git a/promql/test_test.go b/promql/test_test.go index ee2a0e264b..316c177d77 100644 --- a/promql/test_test.go +++ b/promql/test_test.go @@ -110,7 +110,7 @@ func TestLazyLoader_WithSamplesTill(t *testing.T) { } for _, c := range cases { - suite, err := NewLazyLoader(t, c.loadString, LazyLoaderOpts{}) + suite, err := NewLazyLoader(c.loadString, LazyLoaderOpts{}) require.NoError(t, err) defer suite.Close() diff --git a/util/teststorage/storage.go b/util/teststorage/storage.go index 5d95437e99..7d1f9dda24 100644 --- a/util/teststorage/storage.go +++ b/util/teststorage/storage.go @@ -14,6 +14,7 @@ package teststorage import ( + "fmt" "os" "time" @@ -30,8 +31,18 @@ import ( // New returns a new TestStorage for testing purposes // that removes all associated files on closing. func New(t testutil.T) *TestStorage { + stor, err := NewWithError() + require.NoError(t, err) + return stor +} + +// NewWithError returns a new TestStorage for user facing tests, which reports +// errors directly. +func NewWithError() (*TestStorage, error) { dir, err := os.MkdirTemp("", "test_storage") - require.NoError(t, err, "unexpected error while opening test directory") + if err != nil { + return nil, fmt.Errorf("opening test directory: %w", err) + } // Tests just load data for a series sequentially. Thus we // need a long appendable window. @@ -41,13 +52,17 @@ func New(t testutil.T) *TestStorage { opts.RetentionDuration = 0 opts.EnableNativeHistograms = true db, err := tsdb.Open(dir, nil, nil, opts, tsdb.NewDBStats()) - require.NoError(t, err, "unexpected error while opening test storage") + if err != nil { + return nil, fmt.Errorf("opening test storage: %w", err) + } reg := prometheus.NewRegistry() eMetrics := tsdb.NewExemplarMetrics(reg) es, err := tsdb.NewCircularExemplarStorage(10, eMetrics) - require.NoError(t, err, "unexpected error while opening test exemplar storage") - return &TestStorage{DB: db, exemplarStorage: es, dir: dir} + if err != nil { + return nil, fmt.Errorf("opening test exemplar storage: %w", err) + } + return &TestStorage{DB: db, exemplarStorage: es, dir: dir}, nil } type TestStorage struct { From 3bb27c33e938e92cd505d852365e5e55ec805ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Thu, 21 Mar 2024 15:59:18 +0000 Subject: [PATCH 066/127] Use consistent keys for logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rule warnings are logged with numDropped=N while every other component uses num_dropped=N: ``` notifier/notifier.go: level.Warn(n.logger).Log("msg", "Alert batch larger than queue capacity, dropping alerts", "num_dropped", d) notifier/notifier.go: level.Warn(n.logger).Log("msg", "Alert notification queue full, dropping alerts", "num_dropped", d) storage/remote/write_handler.go: _ = level.Warn(h.logger).Log("msg", "Error on ingesting out-of-order exemplars", "num_dropped", outOfOrderExemplarErrs) rules/group.go: level.Warn(logger).Log("msg", "Error on ingesting out-of-order result from rule evaluation", "num_dropped", numOutOfOrder) rules/group.go: level.Warn(logger).Log("msg", "Error on ingesting too old result from rule evaluation", "num_dropped", numTooOld) rules/group.go: level.Warn(logger).Log("msg", "Error on ingesting results from rule evaluation with different value but same timestamp", "num_dropped", numDuplicates) scrape/scrape.go: level.Warn(sl.l).Log("msg", "Error on ingesting out-of-order samples", "num_dropped", appErrs.numOutOfOrder) scrape/scrape.go: level.Warn(sl.l).Log("msg", "Error on ingesting samples with different value but same timestamp", "num_dropped", appErrs.numDuplicates) scrape/scrape.go: level.Warn(sl.l).Log("msg", "Error on ingesting samples that are too old or are too far into the future", "num_dropped", appErrs.numOutOfBounds) scrape/scrape.go: level.Warn(sl.l).Log("msg", "Error on ingesting out-of-order exemplars", "num_dropped", appErrs.numExemplarOutOfOrder) ``` Rename numDropped to num_dropped for consistency. Signed-off-by: Ɓukasz Mierzwa --- rules/group.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rules/group.go b/rules/group.go index b8694a255e..c268d2df7d 100644 --- a/rules/group.go +++ b/rules/group.go @@ -546,13 +546,13 @@ func (g *Group) Eval(ctx context.Context, ts time.Time) { } } if numOutOfOrder > 0 { - level.Warn(logger).Log("msg", "Error on ingesting out-of-order result from rule evaluation", "numDropped", numOutOfOrder) + level.Warn(logger).Log("msg", "Error on ingesting out-of-order result from rule evaluation", "num_dropped", numOutOfOrder) } if numTooOld > 0 { - level.Warn(logger).Log("msg", "Error on ingesting too old result from rule evaluation", "numDropped", numTooOld) + level.Warn(logger).Log("msg", "Error on ingesting too old result from rule evaluation", "num_dropped", numTooOld) } if numDuplicates > 0 { - level.Warn(logger).Log("msg", "Error on ingesting results from rule evaluation with different value but same timestamp", "numDropped", numDuplicates) + level.Warn(logger).Log("msg", "Error on ingesting results from rule evaluation with different value but same timestamp", "num_dropped", numDuplicates) } for metric, lset := range g.seriesInPreviousEval[i] { From 9d32754bc0456f3026be8cb8ad7009e776b3d1a6 Mon Sep 17 00:00:00 2001 From: Jeanette Tan Date: Fri, 22 Mar 2024 03:42:50 +0800 Subject: [PATCH 067/127] add unit tests with all negative values for histogram_stddev and var Signed-off-by: Jeanette Tan --- promql/engine_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/promql/engine_test.go b/promql/engine_test.go index cc5d0ee780..3f5f2dc132 100644 --- a/promql/engine_test.go +++ b/promql/engine_test.go @@ -3498,6 +3498,38 @@ func TestNativeHistogram_HistogramStdDevVar(t *testing.T) { }, stdVar: 1544.8582535368798, // actual variance: 1738.4082 }, + { + name: "-100000, -10000, -1000, -888, -888, -100, -50, -9, -8, -3", + h: &histogram.Histogram{ + Count: 10, + ZeroCount: 0, + Sum: -112946, + Schema: 0, + NegativeSpans: []histogram.Span{ + {Offset: 2, Length: 3}, + {Offset: 1, Length: 2}, + {Offset: 2, Length: 1}, + {Offset: 3, Length: 1}, + {Offset: 2, Length: 1}, + }, + NegativeBuckets: []int64{1, 0, 0, 0, 0, 2, -2, 0}, + }, + stdVar: 1240930974.5260057, // actual variance: 882690990 + }, + { + name: "-10 x10", + h: &histogram.Histogram{ + Count: 10, + ZeroCount: 0, + Sum: -100, + Schema: 0, + NegativeSpans: []histogram.Span{ + {Offset: 4, Length: 1}, + }, + NegativeBuckets: []int64{10}, + }, + stdVar: 454.2741699796952, // actual variance: 0 + }, { name: "-50, -8, 0, 3, 8, 9, 100, NaN", h: &histogram.Histogram{ From d8e4230696123ec6750bc62847abb91c76b042ed Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Fri, 22 Mar 2024 07:44:38 +0100 Subject: [PATCH 068/127] storage: Fix mockChunkQuerier type name Signed-off-by: Arve Knudsen --- storage/merge_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/storage/merge_test.go b/storage/merge_test.go index 05e1c75278..c3a4725aa0 100644 --- a/storage/merge_test.go +++ b/storage/merge_test.go @@ -357,12 +357,12 @@ func TestMergeChunkQuerierWithNoVerticalChunkSeriesMerger(t *testing.T) { t.Run(tc.name, func(t *testing.T) { var p ChunkQuerier if tc.primaryChkQuerierSeries != nil { - p = &mockChunkQurier{toReturn: tc.primaryChkQuerierSeries} + p = &mockChunkQuerier{toReturn: tc.primaryChkQuerierSeries} } var qs []ChunkQuerier for _, in := range tc.chkQuerierSeries { - qs = append(qs, &mockChunkQurier{toReturn: in}) + qs = append(qs, &mockChunkQuerier{toReturn: in}) } qs = append(qs, tc.extraQueriers...) @@ -934,7 +934,7 @@ func (m *mockQuerier) Select(_ context.Context, sortSeries bool, _ *SelectHints, return NewMockSeriesSet(cpy...) } -type mockChunkQurier struct { +type mockChunkQuerier struct { LabelQuerier toReturn []ChunkSeries @@ -948,7 +948,7 @@ func (a chunkSeriesByLabel) Less(i, j int) bool { return labels.Compare(a[i].Labels(), a[j].Labels()) < 0 } -func (m *mockChunkQurier) Select(_ context.Context, sortSeries bool, _ *SelectHints, _ ...*labels.Matcher) ChunkSeriesSet { +func (m *mockChunkQuerier) Select(_ context.Context, sortSeries bool, _ *SelectHints, _ ...*labels.Matcher) ChunkSeriesSet { cpy := make([]ChunkSeries, len(m.toReturn)) copy(cpy, m.toReturn) if sortSeries { From fab629855027c63e3cc746fa91ce8628c390efd6 Mon Sep 17 00:00:00 2001 From: deterclosed Date: Sat, 23 Mar 2024 13:39:27 +0800 Subject: [PATCH 069/127] chore: remove repetitive word Signed-off-by: deterclosed --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57055ef38c..7687826ba4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,7 +90,7 @@ can modify the `./promql/parser/generated_parser.y.go` manually. ```golang // As of writing this was somewhere around line 600. var ( - yyDebug = 0 // This can be be a number 0 -> 5. + yyDebug = 0 // This can be a number 0 -> 5. yyErrorVerbose = false // This can be set to true. ) From 44dcf02c697eba456bfeb28cd6587ee5b834d46f Mon Sep 17 00:00:00 2001 From: Artur Melanchyk Date: Sat, 23 Mar 2024 19:44:30 +0100 Subject: [PATCH 070/127] TSDB: make total lock-free by using atomic Signed-off-by: Artur Melanchyk --- cmd/promtool/tsdb.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cmd/promtool/tsdb.go b/cmd/promtool/tsdb.go index 73258754e2..b786c92976 100644 --- a/cmd/promtool/tsdb.go +++ b/cmd/promtool/tsdb.go @@ -33,6 +33,7 @@ import ( "github.com/alecthomas/units" "github.com/go-kit/log" + "go.uber.org/atomic" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql/parser" @@ -149,8 +150,7 @@ func benchmarkWrite(outPath, samplesFile string, numMetrics, numScrapes int) err } func (b *writeBenchmark) ingestScrapes(lbls []labels.Labels, scrapeCount int) (uint64, error) { - var mu sync.Mutex - var total uint64 + var total atomic.Uint64 for i := 0; i < scrapeCount; i += 100 { var wg sync.WaitGroup @@ -165,22 +165,21 @@ func (b *writeBenchmark) ingestScrapes(lbls []labels.Labels, scrapeCount int) (u wg.Add(1) go func() { + defer wg.Done() + n, err := b.ingestScrapesShard(batch, 100, int64(timeDelta*i)) if err != nil { // exitWithError(err) fmt.Println(" err", err) } - mu.Lock() - total += n - mu.Unlock() - wg.Done() + total.Add(n) }() } wg.Wait() } fmt.Println("ingestion completed") - return total, nil + return total.Load(), nil } func (b *writeBenchmark) ingestScrapesShard(lbls []labels.Labels, scrapeCount int, baset int64) (uint64, error) { From ff15b17400dc579bdb7b9d9ebcea6465453a64b2 Mon Sep 17 00:00:00 2001 From: sellskin Date: Mon, 25 Mar 2024 12:18:33 +0800 Subject: [PATCH 071/127] remove code that will not be executed Signed-off-by: sellskin --- discovery/file/file_test.go | 1 - discovery/legacymanager/manager_test.go | 1 - discovery/manager_test.go | 1 - 3 files changed, 3 deletions(-) diff --git a/discovery/file/file_test.go b/discovery/file/file_test.go index 521a3c0f16..179ac5cd1c 100644 --- a/discovery/file/file_test.go +++ b/discovery/file/file_test.go @@ -208,7 +208,6 @@ func (t *testRunner) requireUpdate(ref time.Time, expected []*targetgroup.Group) select { case <-timeout: t.Fatalf("Expected update but got none") - return case <-time.After(defaultWait / 10): if ref.Equal(t.lastReceive()) { // No update received. diff --git a/discovery/legacymanager/manager_test.go b/discovery/legacymanager/manager_test.go index 6fbecabc2a..1ed699645d 100644 --- a/discovery/legacymanager/manager_test.go +++ b/discovery/legacymanager/manager_test.go @@ -733,7 +733,6 @@ func verifyPresence(t *testing.T, tSets map[poolKey]map[string]*targetgroup.Grou t.Helper() if _, ok := tSets[poolKey]; !ok { t.Fatalf("'%s' should be present in Pool keys: %v", poolKey, tSets) - return } match := false diff --git a/discovery/manager_test.go b/discovery/manager_test.go index 52159d94f6..8f2345911e 100644 --- a/discovery/manager_test.go +++ b/discovery/manager_test.go @@ -733,7 +733,6 @@ func verifySyncedPresence(t *testing.T, tGroups map[string][]*targetgroup.Group, t.Helper() if _, ok := tGroups[key]; !ok { t.Fatalf("'%s' should be present in Group map keys: %v", key, tGroups) - return } match := false var mergedTargets string From 48786ad4e8b644c31a0ce21f5275bb2dcba4a85f Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Mon, 25 Mar 2024 12:20:18 +0000 Subject: [PATCH 072/127] Use slices insteda of exp/slices Signed-off-by: Bryan Boreham --- go.mod | 2 +- model/labels/regexp.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 499104570d..5ea1124fa7 100644 --- a/go.mod +++ b/go.mod @@ -74,7 +74,6 @@ require ( go.uber.org/automaxprocs v1.5.3 go.uber.org/goleak v1.3.0 go.uber.org/multierr v1.11.0 - golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect golang.org/x/net v0.22.0 golang.org/x/oauth2 v0.18.0 golang.org/x/sync v0.6.0 @@ -186,6 +185,7 @@ require ( go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect golang.org/x/crypto v0.21.0 // indirect + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/model/labels/regexp.go b/model/labels/regexp.go index 5e470afa3b..f35dc76f60 100644 --- a/model/labels/regexp.go +++ b/model/labels/regexp.go @@ -14,11 +14,11 @@ package labels import ( + "slices" "strings" "github.com/grafana/regexp" "github.com/grafana/regexp/syntax" - "golang.org/x/exp/slices" ) const ( From ceca6c4716e275d39bd28798bd6ac0ec998938e7 Mon Sep 17 00:00:00 2001 From: Ben Ye Date: Tue, 26 Mar 2024 04:16:27 -0700 Subject: [PATCH 073/127] [ENHANCEMENT] TSDB: Log more statistics during startup (#13838) * log chunk snapshot and mmap chunks replay duration together with total replay duration Signed-off-by: Ben Ye --- tsdb/head.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tsdb/head.go b/tsdb/head.go index 7e6fda9c7e..820fba9e2c 100644 --- a/tsdb/head.go +++ b/tsdb/head.go @@ -620,6 +620,7 @@ func (h *Head) Init(minValidTime int64) error { refSeries := make(map[chunks.HeadSeriesRef]*memSeries) snapshotLoaded := false + var chunkSnapshotLoadDuration time.Duration if h.opts.EnableMemorySnapshotOnShutdown { level.Info(h.logger).Log("msg", "Chunk snapshot is enabled, replaying from the snapshot") // If there are any WAL files, there should be at least one WAL file with an index that is current or newer @@ -650,7 +651,8 @@ func (h *Head) Init(minValidTime int64) error { snapIdx, snapOffset, refSeries, err = h.loadChunkSnapshot() if err == nil { snapshotLoaded = true - level.Info(h.logger).Log("msg", "Chunk snapshot loading time", "duration", time.Since(start).String()) + chunkSnapshotLoadDuration = time.Since(start) + level.Info(h.logger).Log("msg", "Chunk snapshot loading time", "duration", chunkSnapshotLoadDuration.String()) } if err != nil { snapIdx, snapOffset = -1, 0 @@ -672,6 +674,8 @@ func (h *Head) Init(minValidTime int64) error { oooMmappedChunks map[chunks.HeadSeriesRef][]*mmappedChunk lastMmapRef chunks.ChunkDiskMapperRef err error + + mmapChunkReplayDuration time.Duration ) if snapshotLoaded || h.wal != nil { // If snapshot was not loaded and if there is no WAL, then m-map chunks will be discarded @@ -695,7 +699,8 @@ func (h *Head) Init(minValidTime int64) error { return err } } - level.Info(h.logger).Log("msg", "On-disk memory mappable chunks replay completed", "duration", time.Since(mmapChunkReplayStart).String()) + mmapChunkReplayDuration = time.Since(mmapChunkReplayStart) + level.Info(h.logger).Log("msg", "On-disk memory mappable chunks replay completed", "duration", mmapChunkReplayDuration.String()) } if h.wal == nil { @@ -817,6 +822,8 @@ func (h *Head) Init(minValidTime int64) error { "checkpoint_replay_duration", checkpointReplayDuration.String(), "wal_replay_duration", walReplayDuration.String(), "wbl_replay_duration", wblReplayDuration.String(), + "chunk_snapshot_load_duration", chunkSnapshotLoadDuration.String(), + "mmap_chunk_replay_duration", mmapChunkReplayDuration.String(), "total_replay_duration", totalReplayDuration.String(), ) From 481f14e1c0eabd57c5570b1b3f87aa9d8d3b9a81 Mon Sep 17 00:00:00 2001 From: Nick Pillitteri <56quarters@users.noreply.github.com> Date: Tue, 26 Mar 2024 07:17:38 -0400 Subject: [PATCH 074/127] TSDB: Don't rely on integer overflow in head compaction check (#13755) * TSDB: Don't compact the head block when empty Don't compact the Head block if there have not yet been any samples appended. Previously, the logic for determining if the head should be compacted relied on the default values for min and max time and integer overflow when they were checked in `Head.compactable()`. The check in `Head.compactable()` effectively did `math.MinInt64 - math.MaxInt64` which overflowed and wrapped to `1`. Since `1` is less than `1.5` times the chunk range, compaction did not happen. This was the correct behavior but relying on overflow wrapping is surprising. This change add a method for checking if the min and max time for the head is unset and uses it to short-circuit compaction in that case. It also replaces several explicit checks for the default value to determine if the head has not yet had any samples added. Signed-off-by: Nick Pillitteri --- tsdb/db_test.go | 5 +++++ tsdb/head.go | 21 +++++++++++++++------ tsdb/head_append.go | 4 ++-- tsdb/head_test.go | 13 +++++++++++++ 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 498e26c588..e98f60299e 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -1994,6 +1994,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { // Should be set to init values if no WAL or blocks exist so far. require.Equal(t, int64(math.MaxInt64), db.head.MinTime()) require.Equal(t, int64(math.MinInt64), db.head.MaxTime()) + require.False(t, db.head.initialized()) // First added sample initializes the writable range. ctx := context.Background() @@ -2003,6 +2004,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { require.Equal(t, int64(1000), db.head.MinTime()) require.Equal(t, int64(1000), db.head.MaxTime()) + require.True(t, db.head.initialized()) }) t.Run("wal-only", func(t *testing.T) { dir := t.TempDir() @@ -2031,6 +2033,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { require.Equal(t, int64(5000), db.head.MinTime()) require.Equal(t, int64(15000), db.head.MaxTime()) + require.True(t, db.head.initialized()) }) t.Run("existing-block", func(t *testing.T) { dir := t.TempDir() @@ -2043,6 +2046,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { require.Equal(t, int64(2000), db.head.MinTime()) require.Equal(t, int64(2000), db.head.MaxTime()) + require.True(t, db.head.initialized()) }) t.Run("existing-block-and-wal", func(t *testing.T) { dir := t.TempDir() @@ -2075,6 +2079,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { require.Equal(t, int64(6000), db.head.MinTime()) require.Equal(t, int64(15000), db.head.MaxTime()) + require.True(t, db.head.initialized()) // Check that old series has been GCed. require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.series)) }) diff --git a/tsdb/head.go b/tsdb/head.go index 820fba9e2c..8b3d9787ca 100644 --- a/tsdb/head.go +++ b/tsdb/head.go @@ -1081,11 +1081,11 @@ func (h *Head) SetMinValidTime(minValidTime int64) { // Truncate removes old data before mint from the head and WAL. func (h *Head) Truncate(mint int64) (err error) { - initialize := h.MinTime() == math.MaxInt64 + initialized := h.initialized() if err := h.truncateMemory(mint); err != nil { return err } - if initialize { + if !initialized { return nil } return h.truncateWAL(mint) @@ -1107,9 +1107,9 @@ func (h *Head) truncateMemory(mint int64) (err error) { } }() - initialize := h.MinTime() == math.MaxInt64 + initialized := h.initialized() - if h.MinTime() >= mint && !initialize { + if h.MinTime() >= mint && initialized { return nil } @@ -1120,7 +1120,7 @@ func (h *Head) truncateMemory(mint int64) (err error) { defer h.memTruncationInProcess.Store(false) // We wait for pending queries to end that overlap with this truncation. - if !initialize { + if initialized { h.WaitForPendingReadersInTimeRange(h.MinTime(), mint) } @@ -1134,7 +1134,7 @@ func (h *Head) truncateMemory(mint int64) (err error) { // This was an initial call to Truncate after loading blocks on startup. // We haven't read back the WAL yet, so do not attempt to truncate it. - if initialize { + if !initialized { return nil } @@ -1622,10 +1622,19 @@ func (h *Head) MaxOOOTime() int64 { return h.maxOOOTime.Load() } +// initialized returns true if the head has a MinTime set, false otherwise. +func (h *Head) initialized() bool { + return h.MinTime() != math.MaxInt64 +} + // compactable returns whether the head has a compactable range. // The head has a compactable range when the head time range is 1.5 times the chunk range. // The 0.5 acts as a buffer of the appendable window. func (h *Head) compactable() bool { + if !h.initialized() { + return false + } + return h.MaxTime()-h.MinTime() > h.chunkRange.Load()/2*3 } diff --git a/tsdb/head_append.go b/tsdb/head_append.go index 6342a19d46..58d24ceb94 100644 --- a/tsdb/head_append.go +++ b/tsdb/head_append.go @@ -138,7 +138,7 @@ func (h *Head) Appender(_ context.Context) storage.Appender { // The head cache might not have a starting point yet. The init appender // picks up the first appended timestamp as the base. - if h.MinTime() == math.MaxInt64 { + if !h.initialized() { return &initAppender{ head: h, } @@ -191,7 +191,7 @@ func (h *Head) appendableMinValidTime() int64 { // AppendableMinValidTime returns the minimum valid time for samples to be appended to the Head. // Returns false if Head hasn't been initialized yet and the minimum time isn't known yet. func (h *Head) AppendableMinValidTime() (int64, bool) { - if h.MinTime() == math.MaxInt64 { + if !h.initialized() { return 0, false } diff --git a/tsdb/head_test.go b/tsdb/head_test.go index 41c2e062f2..750c8a11e5 100644 --- a/tsdb/head_test.go +++ b/tsdb/head_test.go @@ -5819,3 +5819,16 @@ func TestHeadAppender_AppendCTZeroSample(t *testing.T) { require.Equal(t, chunkenc.ValNone, it.Next()) } } + +func TestHeadCompactableDoesNotCompactEmptyHead(t *testing.T) { + // Use a chunk range of 1 here so that if we attempted to determine if the head + // was compactable using default values for min and max times, `Head.compactable()` + // would return true which is incorrect. This test verifies that we short-circuit + // the check when the head has not yet had any samples added. + head, _ := newTestHead(t, 1, wlog.CompressionNone, false) + defer func() { + require.NoError(t, head.Close()) + }() + + require.False(t, head.compactable()) +} From 5cc97a1820773869bf898b8c2533a4d3bf5d9aba Mon Sep 17 00:00:00 2001 From: Charles Korn Date: Tue, 26 Mar 2024 22:22:22 +1100 Subject: [PATCH 075/127] [tests]: extend test scripting language to support range queries (#13825) * Extract method to make it easier to test. Signed-off-by: Charles Korn * Remove superfluous interface definition. Signed-off-by: Charles Korn * Add test cases for existing instant query functionality. Signed-off-by: Charles Korn * Add support for testing range queries Signed-off-by: Charles Korn * Expand test coverage for instant queries and clarify error when a float is returned but a histogram is expected (or vice versa) Signed-off-by: Charles Korn * Improve error message formatting Signed-off-by: Charles Korn * Add test case for instant query command with invalid timestamp Signed-off-by: Charles Korn * Fix linting warning. Signed-off-by: Charles Korn * Remove superfluous print statement and expected result Signed-off-by: Charles Korn * Fix linting warning. Signed-off-by: Charles Korn * Add note about ordered range eval commands. Signed-off-by: Charles Korn * Check that matrix results are always sorted by labels. Signed-off-by: Charles Korn --------- Signed-off-by: Charles Korn --- promql/test.go | 431 +++++++++++++++++++++++++++++++++----------- promql/test_test.go | 351 ++++++++++++++++++++++++++++++++++++ 2 files changed, 681 insertions(+), 101 deletions(-) diff --git a/promql/test.go b/promql/test.go index c45cfd8a51..296b3d3cad 100644 --- a/promql/test.go +++ b/promql/test.go @@ -46,6 +46,7 @@ var ( patSpace = regexp.MustCompile("[\t ]+") patLoad = regexp.MustCompile(`^load\s+(.+?)$`) patEvalInstant = regexp.MustCompile(`^eval(?:_(fail|ordered))?\s+instant\s+(?:at\s+(.+?))?\s+(.+)$`) + patEvalRange = regexp.MustCompile(`^eval(?:_(fail))?\s+range\s+from\s+(.+)\s+to\s+(.+)\s+step\s+(.+?)\s+(.+)$`) ) const ( @@ -72,7 +73,7 @@ func LoadedStorage(t testutil.T, input string) *teststorage.TestStorage { } // RunBuiltinTests runs an acceptance test suite against the provided engine. -func RunBuiltinTests(t *testing.T, engine engineQuerier) { +func RunBuiltinTests(t *testing.T, engine QueryEngine) { t.Cleanup(func() { parser.EnableExperimentalFunctions = false }) parser.EnableExperimentalFunctions = true @@ -89,11 +90,19 @@ func RunBuiltinTests(t *testing.T, engine engineQuerier) { } // RunTest parses and runs the test against the provided engine. -func RunTest(t testutil.T, input string, engine engineQuerier) { - test, err := newTest(t, input) - require.NoError(t, err) +func RunTest(t testutil.T, input string, engine QueryEngine) { + require.NoError(t, runTest(t, input, engine)) +} +func runTest(t testutil.T, input string, engine QueryEngine) error { + test, err := newTest(t, input) + + // Why do this before checking err? newTest() can create the test storage and then return an error, + // and we want to make sure to clean that up to avoid leaking goroutines. defer func() { + if test == nil { + return + } if test.storage != nil { test.storage.Close() } @@ -102,11 +111,19 @@ func RunTest(t testutil.T, input string, engine engineQuerier) { } }() - for _, cmd := range test.cmds { - // TODO(fabxc): aggregate command errors, yield diffs for result - // comparison errors. - require.NoError(t, test.exec(cmd, engine)) + if err != nil { + return err } + + for _, cmd := range test.cmds { + if err := test.exec(cmd, engine); err != nil { + // TODO(fabxc): aggregate command errors, yield diffs for result + // comparison errors. + return err + } + } + + return nil } // test is a sequence of read and write commands that are run @@ -137,11 +154,6 @@ func newTest(t testutil.T, input string) (*test, error) { //go:embed testdata var testsFs embed.FS -type engineQuerier interface { - NewRangeQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, start, end time.Time, interval time.Duration) (Query, error) - NewInstantQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, ts time.Time) (Query, error) -} - func raise(line int, format string, v ...interface{}) error { return &parser.ParseErr{ LineOffset: line, @@ -188,15 +200,26 @@ func parseSeries(defLine string, line int) (labels.Labels, []parser.SequenceValu } func (t *test) parseEval(lines []string, i int) (int, *evalCmd, error) { - if !patEvalInstant.MatchString(lines[i]) { - return i, nil, raise(i, "invalid evaluation command. (eval[_fail|_ordered] instant [at ] ") + instantParts := patEvalInstant.FindStringSubmatch(lines[i]) + rangeParts := patEvalRange.FindStringSubmatch(lines[i]) + + if instantParts == nil && rangeParts == nil { + return i, nil, raise(i, "invalid evaluation command. Must be either 'eval[_fail|_ordered] instant [at ] ' or 'eval[_fail] range from to step '") } - parts := patEvalInstant.FindStringSubmatch(lines[i]) - var ( - mod = parts[1] - at = parts[2] - expr = parts[3] - ) + + isInstant := instantParts != nil + + var mod string + var expr string + + if isInstant { + mod = instantParts[1] + expr = instantParts[3] + } else { + mod = rangeParts[1] + expr = rangeParts[5] + } + _, err := parser.ParseExpr(expr) if err != nil { parser.EnrichParseError(err, func(parseErr *parser.ParseErr) { @@ -209,15 +232,54 @@ func (t *test) parseEval(lines []string, i int) (int, *evalCmd, error) { return i, nil, err } - offset, err := model.ParseDuration(at) - if err != nil { - return i, nil, raise(i, "invalid step definition %q: %s", parts[1], err) - } - ts := testStartTime.Add(time.Duration(offset)) + formatErr := func(format string, args ...any) error { + combinedArgs := []any{expr, i + 1} + + combinedArgs = append(combinedArgs, args...) + return fmt.Errorf("error in eval %s (line %v): "+format, combinedArgs...) + } + + var cmd *evalCmd + + if isInstant { + at := instantParts[2] + offset, err := model.ParseDuration(at) + if err != nil { + return i, nil, formatErr("invalid timestamp definition %q: %s", at, err) + } + ts := testStartTime.Add(time.Duration(offset)) + cmd = newInstantEvalCmd(expr, ts, i+1) + } else { + from := rangeParts[2] + to := rangeParts[3] + step := rangeParts[4] + + parsedFrom, err := model.ParseDuration(from) + if err != nil { + return i, nil, formatErr("invalid start timestamp definition %q: %s", from, err) + } + + parsedTo, err := model.ParseDuration(to) + if err != nil { + return i, nil, formatErr("invalid end timestamp definition %q: %s", to, err) + } + + if parsedTo < parsedFrom { + return i, nil, formatErr("invalid test definition, end timestamp (%s) is before start timestamp (%s)", to, from) + } + + parsedStep, err := model.ParseDuration(step) + if err != nil { + return i, nil, formatErr("invalid step definition %q: %s", step, err) + } + + cmd = newRangeEvalCmd(expr, testStartTime.Add(time.Duration(parsedFrom)), testStartTime.Add(time.Duration(parsedTo)), time.Duration(parsedStep), i+1) + } - cmd := newEvalCmd(expr, ts, i+1) switch mod { case "ordered": + // Ordered results are not supported for range queries, but the regex for range query commands does not allow + // asserting an ordered result, so we don't need to do any error checking here. cmd.ordered = true case "fail": cmd.fail = true @@ -240,8 +302,8 @@ func (t *test) parseEval(lines []string, i int) (int, *evalCmd, error) { } // Currently, we are not expecting any matrices. - if len(vals) > 1 { - return i, nil, raise(i, "expecting multiple values in instant evaluation not allowed") + if len(vals) > 1 && isInstant { + return i, nil, formatErr("expecting multiple values in instant evaluation not allowed") } cmd.expectMetric(j, metric, vals...) } @@ -375,8 +437,11 @@ func appendSample(a storage.Appender, s Sample, m labels.Labels) error { type evalCmd struct { expr string start time.Time + end time.Time + step time.Duration line int + isRange bool // if false, instant query fail, ordered bool metrics map[uint64]labels.Labels @@ -392,7 +457,7 @@ func (e entry) String() string { return fmt.Sprintf("%d: %s", e.pos, e.vals) } -func newEvalCmd(expr string, start time.Time, line int) *evalCmd { +func newInstantEvalCmd(expr string, start time.Time, line int) *evalCmd { return &evalCmd{ expr: expr, start: start, @@ -403,6 +468,20 @@ func newEvalCmd(expr string, start time.Time, line int) *evalCmd { } } +func newRangeEvalCmd(expr string, start, end time.Time, step time.Duration, line int) *evalCmd { + return &evalCmd{ + expr: expr, + start: start, + end: end, + step: step, + line: line, + isRange: true, + + metrics: map[uint64]labels.Labels{}, + expected: map[uint64]entry{}, + } +} + func (ev *evalCmd) String() string { return "eval" } @@ -425,7 +504,77 @@ func (ev *evalCmd) expectMetric(pos int, m labels.Labels, vals ...parser.Sequenc func (ev *evalCmd) compareResult(result parser.Value) error { switch val := result.(type) { case Matrix: - return errors.New("received range result on instant evaluation") + if ev.ordered { + return fmt.Errorf("expected ordered result, but query returned a matrix") + } + + if err := assertMatrixSorted(val); err != nil { + return err + } + + seen := map[uint64]bool{} + for _, s := range val { + hash := s.Metric.Hash() + if _, ok := ev.metrics[hash]; !ok { + return fmt.Errorf("unexpected metric %s in result", s.Metric) + } + seen[hash] = true + exp := ev.expected[hash] + + var expectedFloats []FPoint + var expectedHistograms []HPoint + + for i, e := range exp.vals { + ts := ev.start.Add(time.Duration(i) * ev.step) + + if ts.After(ev.end) { + return fmt.Errorf("expected %v points for %s, but query time range cannot return this many points", len(exp.vals), ev.metrics[hash]) + } + + t := ts.UnixNano() / int64(time.Millisecond/time.Nanosecond) + + if e.Histogram != nil { + expectedHistograms = append(expectedHistograms, HPoint{T: t, H: e.Histogram}) + } else if !e.Omitted { + expectedFloats = append(expectedFloats, FPoint{T: t, F: e.Value}) + } + } + + if len(expectedFloats) != len(s.Floats) || len(expectedHistograms) != len(s.Histograms) { + return fmt.Errorf("expected %v float points and %v histogram points for %s, but got %s", len(expectedFloats), len(expectedHistograms), ev.metrics[hash], formatSeriesResult(s)) + } + + for i, expected := range expectedFloats { + actual := s.Floats[i] + + if expected.T != actual.T { + return fmt.Errorf("expected float value at index %v for %s to have timestamp %v, but it had timestamp %v (result has %s)", i, ev.metrics[hash], expected.T, actual.T, formatSeriesResult(s)) + } + + if !almostEqual(actual.F, expected.F, defaultEpsilon) { + return fmt.Errorf("expected float value at index %v (t=%v) for %s to be %v, but got %v (result has %s)", i, actual.T, ev.metrics[hash], expected.F, actual.F, formatSeriesResult(s)) + } + } + + for i, expected := range expectedHistograms { + actual := s.Histograms[i] + + if expected.T != actual.T { + return fmt.Errorf("expected histogram value at index %v for %s to have timestamp %v, but it had timestamp %v (result has %s)", i, ev.metrics[hash], expected.T, actual.T, formatSeriesResult(s)) + } + + if !actual.H.Equals(expected.H) { + return fmt.Errorf("expected histogram value at index %v (t=%v) for %s to be %v, but got %v (result has %s)", i, actual.T, ev.metrics[hash], expected.H, actual.H, formatSeriesResult(s)) + } + } + + } + + for hash := range ev.expected { + if !seen[hash] { + return fmt.Errorf("expected metric %s not found", ev.metrics[hash]) + } + } case Vector: seen := map[uint64]bool{} @@ -440,7 +589,13 @@ func (ev *evalCmd) compareResult(result parser.Value) error { } exp0 := exp.vals[0] expH := exp0.Histogram - if (expH == nil) != (v.H == nil) || (expH != nil && !expH.Equals(v.H)) { + if expH == nil && v.H != nil { + return fmt.Errorf("expected float value %v for %s but got histogram %s", exp0, v.Metric, HistogramTestExpression(v.H)) + } + if expH != nil && v.H == nil { + return fmt.Errorf("expected histogram %s for %s but got float value %v", HistogramTestExpression(expH), v.Metric, v.F) + } + if expH != nil && !expH.Equals(v.H) { return fmt.Errorf("expected %v for %s but got %s", HistogramTestExpression(expH), v.Metric, HistogramTestExpression(v.H)) } if !almostEqual(exp0.Value, v.F, defaultEpsilon) { @@ -477,6 +632,21 @@ func (ev *evalCmd) compareResult(result parser.Value) error { return nil } +func formatSeriesResult(s Series) string { + floatPlural := "s" + histogramPlural := "s" + + if len(s.Floats) == 1 { + floatPlural = "" + } + + if len(s.Histograms) == 1 { + histogramPlural = "" + } + + return fmt.Sprintf("%v float point%s %v and %v histogram point%s %v", len(s.Floats), floatPlural, s.Floats, len(s.Histograms), histogramPlural, s.Histograms) +} + // HistogramTestExpression returns TestExpression() for the given histogram or "" if the histogram is nil. func HistogramTestExpression(h *histogram.FloatHistogram) string { if h != nil { @@ -561,7 +731,7 @@ func atModifierTestCases(exprStr string, evalTime time.Time) ([]atModifierTestCa } // exec processes a single step of the test. -func (t *test) exec(tc testCommand, engine engineQuerier) error { +func (t *test) exec(tc testCommand, engine QueryEngine) error { switch cmd := tc.(type) { case *clearCmd: t.clear() @@ -578,74 +748,7 @@ func (t *test) exec(tc testCommand, engine engineQuerier) error { } case *evalCmd: - queries, err := atModifierTestCases(cmd.expr, cmd.start) - if err != nil { - return err - } - queries = append([]atModifierTestCase{{expr: cmd.expr, evalTime: cmd.start}}, queries...) - for _, iq := range queries { - q, err := engine.NewInstantQuery(t.context, t.storage, nil, iq.expr, iq.evalTime) - if err != nil { - return err - } - defer q.Close() - res := q.Exec(t.context) - if res.Err != nil { - if cmd.fail { - continue - } - return fmt.Errorf("error evaluating query %q (line %d): %w", iq.expr, cmd.line, res.Err) - } - if res.Err == nil && cmd.fail { - return fmt.Errorf("expected error evaluating query %q (line %d) but got none", iq.expr, cmd.line) - } - err = cmd.compareResult(res.Value) - if err != nil { - return fmt.Errorf("error in %s %s (line %d): %w", cmd, iq.expr, cmd.line, err) - } - - // Check query returns same result in range mode, - // by checking against the middle step. - q, err = engine.NewRangeQuery(t.context, t.storage, nil, iq.expr, iq.evalTime.Add(-time.Minute), iq.evalTime.Add(time.Minute), time.Minute) - if err != nil { - return err - } - rangeRes := q.Exec(t.context) - if rangeRes.Err != nil { - return fmt.Errorf("error evaluating query %q (line %d) in range mode: %w", iq.expr, cmd.line, rangeRes.Err) - } - defer q.Close() - if cmd.ordered { - // Ordering isn't defined for range queries. - continue - } - mat := rangeRes.Value.(Matrix) - vec := make(Vector, 0, len(mat)) - for _, series := range mat { - // We expect either Floats or Histograms. - for _, point := range series.Floats { - if point.T == timeMilliseconds(iq.evalTime) { - vec = append(vec, Sample{Metric: series.Metric, T: point.T, F: point.F}) - break - } - } - for _, point := range series.Histograms { - if point.T == timeMilliseconds(iq.evalTime) { - vec = append(vec, Sample{Metric: series.Metric, T: point.T, H: point.H}) - break - } - } - } - if _, ok := res.Value.(Scalar); ok { - err = cmd.compareResult(Scalar{V: vec[0].F}) - } else { - err = cmd.compareResult(vec) - } - if err != nil { - return fmt.Errorf("error in %s %s (line %d) range mode: %w", cmd, iq.expr, cmd.line, err) - } - - } + return t.execEval(cmd, engine) default: panic("promql.Test.exec: unknown test command type") @@ -653,6 +756,132 @@ func (t *test) exec(tc testCommand, engine engineQuerier) error { return nil } +func (t *test) execEval(cmd *evalCmd, engine QueryEngine) error { + if cmd.isRange { + return t.execRangeEval(cmd, engine) + } + + return t.execInstantEval(cmd, engine) +} + +func (t *test) execRangeEval(cmd *evalCmd, engine QueryEngine) error { + q, err := engine.NewRangeQuery(t.context, t.storage, nil, cmd.expr, cmd.start, cmd.end, cmd.step) + if err != nil { + return err + } + res := q.Exec(t.context) + if res.Err != nil { + if cmd.fail { + return nil + } + + return fmt.Errorf("error evaluating query %q (line %d): %w", cmd.expr, cmd.line, res.Err) + } + if res.Err == nil && cmd.fail { + return fmt.Errorf("expected error evaluating query %q (line %d) but got none", cmd.expr, cmd.line) + } + defer q.Close() + + if err := cmd.compareResult(res.Value); err != nil { + return fmt.Errorf("error in %s %s (line %d): %w", cmd, cmd.expr, cmd.line, err) + } + + return nil +} + +func (t *test) execInstantEval(cmd *evalCmd, engine QueryEngine) error { + queries, err := atModifierTestCases(cmd.expr, cmd.start) + if err != nil { + return err + } + queries = append([]atModifierTestCase{{expr: cmd.expr, evalTime: cmd.start}}, queries...) + for _, iq := range queries { + q, err := engine.NewInstantQuery(t.context, t.storage, nil, iq.expr, iq.evalTime) + if err != nil { + return err + } + defer q.Close() + res := q.Exec(t.context) + if res.Err != nil { + if cmd.fail { + continue + } + return fmt.Errorf("error evaluating query %q (line %d): %w", iq.expr, cmd.line, res.Err) + } + if res.Err == nil && cmd.fail { + return fmt.Errorf("expected error evaluating query %q (line %d) but got none", iq.expr, cmd.line) + } + err = cmd.compareResult(res.Value) + if err != nil { + return fmt.Errorf("error in %s %s (line %d): %w", cmd, iq.expr, cmd.line, err) + } + + // Check query returns same result in range mode, + // by checking against the middle step. + q, err = engine.NewRangeQuery(t.context, t.storage, nil, iq.expr, iq.evalTime.Add(-time.Minute), iq.evalTime.Add(time.Minute), time.Minute) + if err != nil { + return err + } + rangeRes := q.Exec(t.context) + if rangeRes.Err != nil { + return fmt.Errorf("error evaluating query %q (line %d) in range mode: %w", iq.expr, cmd.line, rangeRes.Err) + } + defer q.Close() + if cmd.ordered { + // Range queries are always sorted by labels, so skip this test case that expects results in a particular order. + continue + } + mat := rangeRes.Value.(Matrix) + if err := assertMatrixSorted(mat); err != nil { + return err + } + + vec := make(Vector, 0, len(mat)) + for _, series := range mat { + // We expect either Floats or Histograms. + for _, point := range series.Floats { + if point.T == timeMilliseconds(iq.evalTime) { + vec = append(vec, Sample{Metric: series.Metric, T: point.T, F: point.F}) + break + } + } + for _, point := range series.Histograms { + if point.T == timeMilliseconds(iq.evalTime) { + vec = append(vec, Sample{Metric: series.Metric, T: point.T, H: point.H}) + break + } + } + } + if _, ok := res.Value.(Scalar); ok { + err = cmd.compareResult(Scalar{V: vec[0].F}) + } else { + err = cmd.compareResult(vec) + } + if err != nil { + return fmt.Errorf("error in %s %s (line %d) range mode: %w", cmd, iq.expr, cmd.line, err) + } + } + + return nil +} + +func assertMatrixSorted(m Matrix) error { + if len(m) <= 1 { + return nil + } + + for i, s := range m[:len(m)-1] { + nextIndex := i + 1 + nextMetric := m[nextIndex].Metric + + if labels.Compare(s.Metric, nextMetric) > 0 { + return fmt.Errorf("matrix results should always be sorted by labels, but matrix is not sorted: series at index %v with labels %s sorts before series at index %v with labels %s", nextIndex, nextMetric, i, s.Metric) + } + } + + return nil +} + // clear the current test storage of all inserted samples. func (t *test) clear() { if t.storage != nil { diff --git a/promql/test_test.go b/promql/test_test.go index 316c177d77..0130a789d7 100644 --- a/promql/test_test.go +++ b/promql/test_test.go @@ -156,3 +156,354 @@ func TestLazyLoader_WithSamplesTill(t *testing.T) { } } } + +func TestRunTest(t *testing.T) { + testData := ` +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 +` + + testCases := map[string]struct { + input string + expectedError string + }{ + "instant query with expected float result": { + input: testData + ` +eval instant at 5m sum by (group) (http_requests) + {group="production"} 30 + {group="canary"} 70 +`, + }, + "instant query with unexpected float result": { + input: testData + ` +eval instant at 5m sum by (group) (http_requests) + {group="production"} 30 + {group="canary"} 80 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): expected 80 for {group="canary"} but got 70`, + }, + "instant query with expected histogram result": { + input: ` +load 5m + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} + +eval instant at 0 testmetric + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} +`, + }, + "instant query with unexpected histogram result": { + input: ` +load 5m + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} + +eval instant at 0 testmetric + testmetric {{schema:-1 sum:6 count:1 buckets:[1] offset:1}} +`, + expectedError: `error in eval testmetric (line 5): expected {{schema:-1 count:1 sum:6 offset:1 buckets:[1]}} for {__name__="testmetric"} but got {{schema:-1 count:1 sum:4 offset:1 buckets:[1]}}`, + }, + "instant query with float value returned when histogram expected": { + input: ` +load 5m + testmetric 2 + +eval instant at 0 testmetric + testmetric {{}} +`, + expectedError: `error in eval testmetric (line 5): expected histogram {{}} for {__name__="testmetric"} but got float value 2`, + }, + "instant query with histogram returned when float expected": { + input: ` +load 5m + testmetric {{}} + +eval instant at 0 testmetric + testmetric 2 +`, + expectedError: `error in eval testmetric (line 5): expected float value 2.000000 for {__name__="testmetric"} but got histogram {{}}`, + }, + "instant query, but result has an unexpected series": { + input: testData + ` +eval instant at 5m sum by (group) (http_requests) + {group="production"} 30 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): unexpected metric {group="canary"} in result`, + }, + "instant query, but result is missing a series": { + input: testData + ` +eval instant at 5m sum by (group) (http_requests) + {group="production"} 30 + {group="canary"} 70 + {group="test"} 100 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): expected metric {group="test"} with 3: [100.000000] not found`, + }, + "instant query expected to fail, and query fails": { + input: ` +load 5m + testmetric1{src="a",dst="b"} 0 + testmetric2{src="a",dst="b"} 1 + +eval_fail instant at 0m ceil({__name__=~'testmetric1|testmetric2'}) +`, + }, + "instant query expected to fail, but query succeeds": { + input: `eval_fail instant at 0s vector(0)`, + expectedError: `expected error evaluating query "vector(0)" (line 1) but got none`, + }, + "instant query with results expected to match provided order, and result is in expected order": { + input: testData + ` +eval_ordered instant at 50m sort(http_requests) + http_requests{group="production", instance="0", job="api-server"} 100 + http_requests{group="production", instance="1", job="api-server"} 200 + http_requests{group="canary", instance="0", job="api-server"} 300 + http_requests{group="canary", instance="1", job="api-server"} 400 +`, + }, + "instant query with results expected to match provided order, but result is out of order": { + input: testData + ` +eval_ordered instant at 50m sort(http_requests) + http_requests{group="production", instance="0", job="api-server"} 100 + http_requests{group="production", instance="1", job="api-server"} 200 + http_requests{group="canary", instance="1", job="api-server"} 400 + http_requests{group="canary", instance="0", job="api-server"} 300 +`, + expectedError: `error in eval sort(http_requests) (line 8): expected metric {__name__="http_requests", group="canary", instance="0", job="api-server"} with [300.000000] at position 4 but was at 3`, + }, + "instant query with results expected to match provided order, but result has an unexpected series": { + input: testData + ` +eval_ordered instant at 50m sort(http_requests) + http_requests{group="production", instance="0", job="api-server"} 100 + http_requests{group="production", instance="1", job="api-server"} 200 + http_requests{group="canary", instance="0", job="api-server"} 300 +`, + expectedError: `error in eval sort(http_requests) (line 8): unexpected metric {__name__="http_requests", group="canary", instance="1", job="api-server"} in result`, + }, + "instant query with invalid timestamp": { + input: `eval instant at abc123 vector(0)`, + expectedError: `error in eval vector(0) (line 1): invalid timestamp definition "abc123": not a valid duration string: "abc123"`, + }, + "range query with expected result": { + input: testData + ` +eval range from 0 to 10m step 5m sum by (group) (http_requests) + {group="production"} 0 30 60 + {group="canary"} 0 70 140 +`, + }, + "range query with unexpected float value": { + input: testData + ` +eval range from 0 to 10m step 5m sum by (group) (http_requests) + {group="production"} 0 30 60 + {group="canary"} 0 80 140 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): expected float value at index 1 (t=300000) for {group="canary"} to be 80, but got 70 (result has 3 float points [0 @[0] 70 @[300000] 140 @[600000]] and 0 histogram points [])`, + }, + "range query with expected histogram values": { + input: ` +load 5m + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} {{schema:-1 sum:5 count:1 buckets:[1] offset:1}} {{schema:-1 sum:6 count:1 buckets:[1] offset:1}} + +eval range from 0 to 10m step 5m testmetric + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} {{schema:-1 sum:5 count:1 buckets:[1] offset:1}} {{schema:-1 sum:6 count:1 buckets:[1] offset:1}} +`, + }, + "range query with unexpected histogram value": { + input: ` +load 5m + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} {{schema:-1 sum:5 count:1 buckets:[1] offset:1}} {{schema:-1 sum:6 count:1 buckets:[1] offset:1}} + +eval range from 0 to 10m step 5m testmetric + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} {{schema:-1 sum:7 count:1 buckets:[1] offset:1}} {{schema:-1 sum:8 count:1 buckets:[1] offset:1}} +`, + expectedError: `error in eval testmetric (line 5): expected histogram value at index 1 (t=300000) for {__name__="testmetric"} to be {count:1, sum:7, (1,4]:1}, but got {count:1, sum:5, (1,4]:1} (result has 0 float points [] and 3 histogram points [{count:1, sum:4, (1,4]:1} @[0] {count:1, sum:5, (1,4]:1} @[300000] {count:1, sum:6, (1,4]:1} @[600000]])`, + }, + "range query with too many points for query time range": { + input: testData + ` +eval range from 0 to 10m step 5m sum by (group) (http_requests) + {group="production"} 0 30 60 90 + {group="canary"} 0 70 140 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): expected 4 points for {group="production"}, but query time range cannot return this many points`, + }, + "range query with missing point in result": { + input: ` +load 5m + testmetric 5 + +eval range from 0 to 6m step 6m testmetric + testmetric 5 10 +`, + expectedError: `error in eval testmetric (line 5): expected 2 float points and 0 histogram points for {__name__="testmetric"}, but got 1 float point [5 @[0]] and 0 histogram points []`, + }, + "range query with extra point in result": { + input: testData + ` +eval range from 0 to 10m step 5m sum by (group) (http_requests) + {group="production"} 0 30 + {group="canary"} 0 70 140 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): expected 2 float points and 0 histogram points for {group="production"}, but got 3 float points [0 @[0] 30 @[300000] 60 @[600000]] and 0 histogram points []`, + }, + "range query, but result has an unexpected series": { + input: testData + ` +eval range from 0 to 10m step 5m sum by (group) (http_requests) + {group="production"} 0 30 60 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): unexpected metric {group="canary"} in result`, + }, + "range query, but result is missing a series": { + input: testData + ` +eval range from 0 to 10m step 5m sum by (group) (http_requests) + {group="production"} 0 30 60 + {group="canary"} 0 70 140 + {group="test"} 0 100 200 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): expected metric {group="test"} not found`, + }, + "range query expected to fail, and query fails": { + input: ` +load 5m + testmetric1{src="a",dst="b"} 0 + testmetric2{src="a",dst="b"} 1 + +eval_fail range from 0 to 10m step 5m ceil({__name__=~'testmetric1|testmetric2'}) +`, + }, + "range query expected to fail, but query succeeds": { + input: `eval_fail range from 0 to 10m step 5m vector(0)`, + expectedError: `expected error evaluating query "vector(0)" (line 1) but got none`, + }, + "range query with from and to timestamps in wrong order": { + input: `eval range from 10m to 9m step 5m vector(0)`, + expectedError: `error in eval vector(0) (line 1): invalid test definition, end timestamp (9m) is before start timestamp (10m)`, + }, + "range query with sparse output": { + input: ` +load 6m + testmetric 1 _ 3 + +eval range from 0 to 18m step 6m testmetric + testmetric 1 _ 3 +`, + }, + "range query with float value returned when no value expected": { + input: ` +load 6m + testmetric 1 2 3 + +eval range from 0 to 18m step 6m testmetric + testmetric 1 _ 3 +`, + expectedError: `error in eval testmetric (line 5): expected 2 float points and 0 histogram points for {__name__="testmetric"}, but got 3 float points [1 @[0] 2 @[360000] 3 @[720000]] and 0 histogram points []`, + }, + "range query with float value returned when histogram expected": { + input: ` +load 5m + testmetric 2 3 + +eval range from 0 to 5m step 5m testmetric + testmetric {{}} {{}} +`, + expectedError: `error in eval testmetric (line 5): expected 0 float points and 2 histogram points for {__name__="testmetric"}, but got 2 float points [2 @[0] 3 @[300000]] and 0 histogram points []`, + }, + "range query with histogram returned when float expected": { + input: ` +load 5m + testmetric {{}} {{}} + +eval range from 0 to 5m step 5m testmetric + testmetric 2 3 +`, + expectedError: `error in eval testmetric (line 5): expected 2 float points and 0 histogram points for {__name__="testmetric"}, but got 0 float points [] and 2 histogram points [{count:0, sum:0} @[0] {count:0, sum:0} @[300000]]`, + }, + "range query with expected mixed results": { + input: ` +load 6m + testmetric{group="a"} {{}} _ _ + testmetric{group="b"} _ _ 3 + +eval range from 0 to 12m step 6m sum(testmetric) + {} {{}} _ 3 +`, + }, + "range query with mixed results and incorrect values": { + input: ` +load 5m + testmetric 3 {{}} + +eval range from 0 to 5m step 5m testmetric + testmetric {{}} 3 +`, + expectedError: `error in eval testmetric (line 5): expected float value at index 0 for {__name__="testmetric"} to have timestamp 300000, but it had timestamp 0 (result has 1 float point [3 @[0]] and 1 histogram point [{count:0, sum:0} @[300000]])`, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + err := runTest(t, testCase.input, newTestEngine()) + + if testCase.expectedError == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, testCase.expectedError) + } + }) + } +} + +func TestAssertMatrixSorted(t *testing.T) { + testCases := map[string]struct { + matrix Matrix + expectedError string + }{ + "empty matrix": { + matrix: Matrix{}, + }, + "matrix with one series": { + matrix: Matrix{ + Series{Metric: labels.FromStrings("the_label", "value_1")}, + }, + }, + "matrix with two series, series in sorted order": { + matrix: Matrix{ + Series{Metric: labels.FromStrings("the_label", "value_1")}, + Series{Metric: labels.FromStrings("the_label", "value_2")}, + }, + }, + "matrix with two series, series in reverse order": { + matrix: Matrix{ + Series{Metric: labels.FromStrings("the_label", "value_2")}, + Series{Metric: labels.FromStrings("the_label", "value_1")}, + }, + expectedError: `matrix results should always be sorted by labels, but matrix is not sorted: series at index 1 with labels {the_label="value_1"} sorts before series at index 0 with labels {the_label="value_2"}`, + }, + "matrix with three series, series in sorted order": { + matrix: Matrix{ + Series{Metric: labels.FromStrings("the_label", "value_1")}, + Series{Metric: labels.FromStrings("the_label", "value_2")}, + Series{Metric: labels.FromStrings("the_label", "value_3")}, + }, + }, + "matrix with three series, series not in sorted order": { + matrix: Matrix{ + Series{Metric: labels.FromStrings("the_label", "value_1")}, + Series{Metric: labels.FromStrings("the_label", "value_3")}, + Series{Metric: labels.FromStrings("the_label", "value_2")}, + }, + expectedError: `matrix results should always be sorted by labels, but matrix is not sorted: series at index 2 with labels {the_label="value_2"} sorts before series at index 1 with labels {the_label="value_3"}`, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + err := assertMatrixSorted(testCase.matrix) + + if testCase.expectedError == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, testCase.expectedError) + } + }) + } +} From 35aab01de02528714f75f8cdd197b633cbadf031 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Tue, 26 Mar 2024 15:16:45 +0100 Subject: [PATCH 076/127] tsdb/wlog.Checkpoint: Handle also float histograms Signed-off-by: Arve Knudsen --- tsdb/wlog/checkpoint.go | 43 ++++++++++++++++++++++++++---------- tsdb/wlog/checkpoint_test.go | 38 ++++++++++++++++++++++++++++--- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/tsdb/wlog/checkpoint.go b/tsdb/wlog/checkpoint.go index 5491d9718e..a16cd5fc74 100644 --- a/tsdb/wlog/checkpoint.go +++ b/tsdb/wlog/checkpoint.go @@ -149,22 +149,23 @@ func Checkpoint(logger log.Logger, w *WL, from, to int, keep func(id chunks.Head r := NewReader(sgmReader) var ( - series []record.RefSeries - samples []record.RefSample - histogramSamples []record.RefHistogramSample - tstones []tombstones.Stone - exemplars []record.RefExemplar - metadata []record.RefMetadata - st = labels.NewSymbolTable() // Needed for decoding; labels do not outlive this function. - dec = record.NewDecoder(st) - enc record.Encoder - buf []byte - recs [][]byte + series []record.RefSeries + samples []record.RefSample + histogramSamples []record.RefHistogramSample + floatHistogramSamples []record.RefFloatHistogramSample + tstones []tombstones.Stone + exemplars []record.RefExemplar + metadata []record.RefMetadata + st = labels.NewSymbolTable() // Needed for decoding; labels do not outlive this function. + dec = record.NewDecoder(st) + enc record.Encoder + buf []byte + recs [][]byte latestMetadataMap = make(map[chunks.HeadSeriesRef]record.RefMetadata) ) for r.Next() { - series, samples, histogramSamples, tstones, exemplars, metadata = series[:0], samples[:0], histogramSamples[:0], tstones[:0], exemplars[:0], metadata[:0] + series, samples, histogramSamples, floatHistogramSamples, tstones, exemplars, metadata = series[:0], samples[:0], histogramSamples[:0], floatHistogramSamples[:0], tstones[:0], exemplars[:0], metadata[:0] // We don't reset the buffer since we batch up multiple records // before writing them to the checkpoint. @@ -227,6 +228,24 @@ func Checkpoint(logger log.Logger, w *WL, from, to int, keep func(id chunks.Head stats.TotalSamples += len(histogramSamples) stats.DroppedSamples += len(histogramSamples) - len(repl) + case record.FloatHistogramSamples: + floatHistogramSamples, err = dec.FloatHistogramSamples(rec, floatHistogramSamples) + if err != nil { + return nil, fmt.Errorf("decode float histogram samples: %w", err) + } + // Drop irrelevant floatHistogramSamples in place. + repl := floatHistogramSamples[:0] + for _, fh := range floatHistogramSamples { + if fh.T >= mint { + repl = append(repl, fh) + } + } + if len(repl) > 0 { + buf = enc.FloatHistogramSamples(repl, buf) + } + stats.TotalSamples += len(floatHistogramSamples) + stats.DroppedSamples += len(floatHistogramSamples) - len(repl) + case record.Tombstones: tstones, err = dec.Tombstones(rec, tstones) if err != nil { diff --git a/tsdb/wlog/checkpoint_test.go b/tsdb/wlog/checkpoint_test.go index 0d22171768..279f7c4356 100644 --- a/tsdb/wlog/checkpoint_test.go +++ b/tsdb/wlog/checkpoint_test.go @@ -125,6 +125,20 @@ func TestCheckpoint(t *testing.T) { PositiveBuckets: []int64{int64(i + 1), 1, -1, 0}, } } + makeFloatHistogram := func(i int) *histogram.FloatHistogram { + return &histogram.FloatHistogram{ + Count: 5 + float64(i*4), + ZeroCount: 2 + float64(i), + ZeroThreshold: 0.001, + Sum: 18.4 * float64(i+1), + Schema: 1, + PositiveSpans: []histogram.Span{ + {Offset: 0, Length: 2}, + {Offset: 1, Length: 2}, + }, + PositiveBuckets: []float64{float64(i + 1), 1, -1, 0}, + } + } for _, compress := range []CompressionType{CompressionNone, CompressionSnappy, CompressionZstd} { t.Run(fmt.Sprintf("compress=%s", compress), func(t *testing.T) { @@ -154,7 +168,7 @@ func TestCheckpoint(t *testing.T) { w, err = NewSize(nil, nil, dir, 64*1024, compress) require.NoError(t, err) - samplesInWAL, histogramsInWAL := 0, 0 + samplesInWAL, histogramsInWAL, floatHistogramsInWAL := 0, 0, 0 var last int64 for i := 0; ; i++ { _, n, err := Segments(w.Dir()) @@ -200,6 +214,15 @@ func TestCheckpoint(t *testing.T) { }, nil) require.NoError(t, w.Log(b)) histogramsInWAL += 4 + fh := makeFloatHistogram(i) + b = enc.FloatHistogramSamples([]record.RefFloatHistogramSample{ + {Ref: 0, T: last, FH: fh}, + {Ref: 1, T: last + 10000, FH: fh}, + {Ref: 2, T: last + 20000, FH: fh}, + {Ref: 3, T: last + 30000, FH: fh}, + }, nil) + require.NoError(t, w.Log(b)) + floatHistogramsInWAL += 4 b = enc.Exemplars([]record.RefExemplar{ {Ref: 1, T: last, V: float64(i), Labels: labels.FromStrings("trace_id", fmt.Sprintf("trace-%d", i))}, @@ -226,7 +249,7 @@ func TestCheckpoint(t *testing.T) { require.NoError(t, err) require.NoError(t, w.Truncate(107)) require.NoError(t, DeleteCheckpoints(w.Dir(), 106)) - require.Equal(t, histogramsInWAL+samplesInWAL, stats.TotalSamples) + require.Equal(t, histogramsInWAL+floatHistogramsInWAL+samplesInWAL, stats.TotalSamples) require.Greater(t, stats.DroppedSamples, 0) // Only the new checkpoint should be left. @@ -244,7 +267,7 @@ func TestCheckpoint(t *testing.T) { var metadata []record.RefMetadata r := NewReader(sr) - samplesInCheckpoint, histogramsInCheckpoint := 0, 0 + samplesInCheckpoint, histogramsInCheckpoint, floatHistogramsInCheckpoint := 0, 0, 0 for r.Next() { rec := r.Record() @@ -266,6 +289,13 @@ func TestCheckpoint(t *testing.T) { require.GreaterOrEqual(t, h.T, last/2, "histogram with wrong timestamp") } histogramsInCheckpoint += len(histograms) + case record.FloatHistogramSamples: + floatHistograms, err := dec.FloatHistogramSamples(rec, nil) + require.NoError(t, err) + for _, h := range floatHistograms { + require.GreaterOrEqual(t, h.T, last/2, "float histogram with wrong timestamp") + } + floatHistogramsInCheckpoint += len(floatHistograms) case record.Exemplars: exemplars, err := dec.Exemplars(rec, nil) require.NoError(t, err) @@ -283,6 +313,8 @@ func TestCheckpoint(t *testing.T) { require.Less(t, float64(samplesInCheckpoint)/float64(samplesInWAL), 0.8) require.Greater(t, float64(histogramsInCheckpoint)/float64(histogramsInWAL), 0.5) require.Less(t, float64(histogramsInCheckpoint)/float64(histogramsInWAL), 0.8) + require.Greater(t, float64(floatHistogramsInCheckpoint)/float64(floatHistogramsInWAL), 0.5) + require.Less(t, float64(floatHistogramsInCheckpoint)/float64(floatHistogramsInWAL), 0.8) expectedRefSeries := []record.RefSeries{ {Ref: 0, Labels: labels.FromStrings("a", "b", "c", "0")}, From 435f330d0b139111c1c6e9a3ab5854c7077e4056 Mon Sep 17 00:00:00 2001 From: Domantas Date: Wed, 27 Mar 2024 12:35:17 +0200 Subject: [PATCH 077/127] [BUGFIX] labels: don't modify original labels in DropMetricName (#13845) Restrict the capacity of first argument to `append()` to force an allocation. This is for the slice implementation only. Signed-off-by: Domantas Jadenkus --- model/labels/labels.go | 4 +++- model/labels/labels_test.go | 6 +++++- promql/engine_test.go | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/model/labels/labels.go b/model/labels/labels.go index e998248269..01514abf38 100644 --- a/model/labels/labels.go +++ b/model/labels/labels.go @@ -349,7 +349,9 @@ func (ls Labels) DropMetricName() Labels { if i == 0 { // Make common case fast with no allocations. return ls[1:] } - return append(ls[:i], ls[i+1:]...) + // Avoid modifying original Labels - use [:i:i] so that left slice would not + // have any spare capacity and append would have to allocate a new slice for the result. + return append(ls[:i:i], ls[i+1:]...) } } return ls diff --git a/model/labels/labels_test.go b/model/labels/labels_test.go index cedeb95a6c..90ae41ccea 100644 --- a/model/labels/labels_test.go +++ b/model/labels/labels_test.go @@ -457,7 +457,11 @@ func TestLabels_Get(t *testing.T) { func TestLabels_DropMetricName(t *testing.T) { require.True(t, Equal(FromStrings("aaa", "111", "bbb", "222"), FromStrings("aaa", "111", "bbb", "222").DropMetricName())) require.True(t, Equal(FromStrings("aaa", "111"), FromStrings(MetricName, "myname", "aaa", "111").DropMetricName())) - require.True(t, Equal(FromStrings("__aaa__", "111", "bbb", "222"), FromStrings("__aaa__", "111", MetricName, "myname", "bbb", "222").DropMetricName())) + + original := FromStrings("__aaa__", "111", MetricName, "myname", "bbb", "222") + check := FromStrings("__aaa__", "111", MetricName, "myname", "bbb", "222") + require.True(t, Equal(FromStrings("__aaa__", "111", "bbb", "222"), check.DropMetricName())) + require.True(t, Equal(original, check)) } // BenchmarkLabels_Get was written to check whether a binary search can improve the performance vs the linear search implementation diff --git a/promql/engine_test.go b/promql/engine_test.go index cc5d0ee780..3f6727d849 100644 --- a/promql/engine_test.go +++ b/promql/engine_test.go @@ -3212,6 +3212,24 @@ func TestRangeQuery(t *testing.T) { End: time.Unix(120, 0), Interval: 1 * time.Minute, }, + { + Name: "drop-metric-name", + Load: `load 30s + requests{job="1", __address__="bar"} 100`, + Query: `requests * 2`, + Result: Matrix{ + Series{ + Floats: []FPoint{{F: 200, T: 0}, {F: 200, T: 60000}, {F: 200, T: 120000}}, + Metric: labels.FromStrings( + "__address__", "bar", + "job", "1", + ), + }, + }, + Start: time.Unix(0, 0), + End: time.Unix(120, 0), + Interval: 1 * time.Minute, + }, } for _, c := range cases { t.Run(c.Name, func(t *testing.T) { From 64dfd8a158fe8de72d8019c54ea4242956e4d652 Mon Sep 17 00:00:00 2001 From: Ziqi Zhao Date: Wed, 27 Mar 2024 23:32:37 +0800 Subject: [PATCH 078/127] fix the bug of setting native histogram min bucket factor (#13846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix the bug of setting native histogram min bucket factor Signed-off-by: Ziqi Zhao * Add unit test for checking that min_bucket_factor is correctly applied Signed-off-by: György Krajcsovits --------- Signed-off-by: Ziqi Zhao Signed-off-by: György Krajcsovits Co-authored-by: György Krajcsovits --- scrape/scrape.go | 2 + scrape/scrape_test.go | 135 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/scrape/scrape.go b/scrape/scrape.go index 064b0b67bc..734c248130 100644 --- a/scrape/scrape.go +++ b/scrape/scrape.go @@ -427,6 +427,7 @@ func (sp *scrapePool) sync(targets []*Target) { bodySizeLimit = int64(sp.config.BodySizeLimit) sampleLimit = int(sp.config.SampleLimit) bucketLimit = int(sp.config.NativeHistogramBucketLimit) + maxSchema = pickSchema(sp.config.NativeHistogramMinBucketFactor) labelLimits = &labelLimits{ labelLimit: int(sp.config.LabelLimit), labelNameLengthLimit: int(sp.config.LabelNameLengthLimit), @@ -464,6 +465,7 @@ func (sp *scrapePool) sync(targets []*Target) { scraper: s, sampleLimit: sampleLimit, bucketLimit: bucketLimit, + maxSchema: maxSchema, labelLimits: labelLimits, honorLabels: honorLabels, honorTimestamps: honorTimestamps, diff --git a/scrape/scrape_test.go b/scrape/scrape_test.go index 0dfca538d8..20b21936b9 100644 --- a/scrape/scrape_test.go +++ b/scrape/scrape_test.go @@ -41,6 +41,7 @@ import ( "github.com/stretchr/testify/require" "github.com/prometheus/prometheus/config" + "github.com/prometheus/prometheus/discovery" "github.com/prometheus/prometheus/discovery/targetgroup" "github.com/prometheus/prometheus/model/exemplar" "github.com/prometheus/prometheus/model/histogram" @@ -3631,3 +3632,137 @@ func TestScrapeLoopSeriesAddedDuplicates(t *testing.T) { value := metric.GetCounter().GetValue() require.Equal(t, 4.0, value) } + +// This tests running a full scrape loop and checking that the scrape option +// `native_histogram_min_bucket_factor` is used correctly. +func TestNativeHistogramMaxSchemaSet(t *testing.T) { + testcases := map[string]struct { + minBucketFactor string + expectedSchema int32 + }{ + "min factor not specified": { + minBucketFactor: "", + expectedSchema: 3, // Factor 1.09. + }, + "min factor 1": { + minBucketFactor: "native_histogram_min_bucket_factor: 1", + expectedSchema: 3, // Factor 1.09. + }, + "min factor 2": { + minBucketFactor: "native_histogram_min_bucket_factor: 2", + expectedSchema: 0, // Factor 2.00. + }, + } + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + testNativeHistogramMaxSchemaSet(t, tc.minBucketFactor, tc.expectedSchema) + }) + } +} + +func testNativeHistogramMaxSchemaSet(t *testing.T, minBucketFactor string, expectedSchema int32) { + // Create a ProtoBuf message to serve as a Prometheus metric. + nativeHistogram := prometheus.NewHistogram( + prometheus.HistogramOpts{ + Namespace: "testing", + Name: "example_native_histogram", + Help: "This is used for testing", + NativeHistogramBucketFactor: 1.1, + NativeHistogramMaxBucketNumber: 100, + }, + ) + registry := prometheus.NewRegistry() + registry.Register(nativeHistogram) + nativeHistogram.Observe(1.0) + nativeHistogram.Observe(1.0) + nativeHistogram.Observe(1.0) + nativeHistogram.Observe(10.0) // in different bucket since > 1*1.1. + nativeHistogram.Observe(10.0) + + gathered, err := registry.Gather() + require.NoError(t, err) + require.NotEmpty(t, gathered) + + histogramMetricFamily := gathered[0] + buffer := protoMarshalDelimited(t, histogramMetricFamily) + + // Create a HTTP server to serve /metrics via ProtoBuf + metricsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`) + w.Write(buffer) + })) + defer metricsServer.Close() + + // Create a scrape loop with the HTTP server as the target. + configStr := fmt.Sprintf(` +global: + scrape_interval: 1s + scrape_timeout: 1s +scrape_configs: + - job_name: test + %s + static_configs: + - targets: [%s] +`, minBucketFactor, strings.ReplaceAll(metricsServer.URL, "http://", "")) + + s := teststorage.New(t) + defer s.Close() + s.DB.EnableNativeHistograms() + reg := prometheus.NewRegistry() + + mng, err := NewManager(nil, nil, s, reg) + require.NoError(t, err) + cfg, err := config.Load(configStr, false, log.NewNopLogger()) + require.NoError(t, err) + mng.ApplyConfig(cfg) + tsets := make(chan map[string][]*targetgroup.Group) + go func() { + err = mng.Run(tsets) + require.NoError(t, err) + }() + defer mng.Stop() + + // Get the static targets and apply them to the scrape manager. + require.Len(t, cfg.ScrapeConfigs, 1) + scrapeCfg := cfg.ScrapeConfigs[0] + require.Len(t, scrapeCfg.ServiceDiscoveryConfigs, 1) + staticDiscovery, ok := scrapeCfg.ServiceDiscoveryConfigs[0].(discovery.StaticConfig) + require.True(t, ok) + require.Len(t, staticDiscovery, 1) + tsets <- map[string][]*targetgroup.Group{"test": staticDiscovery} + + // Wait for the scrape loop to scrape the target. + require.Eventually(t, func() bool { + q, err := s.Querier(0, math.MaxInt64) + require.NoError(t, err) + seriesS := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "__name__", "testing_example_native_histogram")) + countSeries := 0 + for seriesS.Next() { + countSeries++ + } + return countSeries > 0 + }, 15*time.Second, 100*time.Millisecond) + + // Check that native histogram schema is as expected. + q, err := s.Querier(0, math.MaxInt64) + require.NoError(t, err) + seriesS := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "__name__", "testing_example_native_histogram")) + histogramSamples := []*histogram.Histogram{} + for seriesS.Next() { + series := seriesS.At() + it := series.Iterator(nil) + for vt := it.Next(); vt != chunkenc.ValNone; vt = it.Next() { + if vt != chunkenc.ValHistogram { + // don't care about other samples + continue + } + _, h := it.AtHistogram(nil) + histogramSamples = append(histogramSamples, h) + } + } + require.NoError(t, seriesS.Err()) + require.NotEmpty(t, histogramSamples) + for _, h := range histogramSamples { + require.Equal(t, expectedSchema, h.Schema) + } +} From 22d0f4f114b9143c66d7805ed92c5f692e74c066 Mon Sep 17 00:00:00 2001 From: Jeanette Tan Date: Fri, 22 Mar 2024 03:42:50 +0800 Subject: [PATCH 079/127] improve handling of negative bounds in histogram std dev/var Signed-off-by: Jeanette Tan --- promql/engine_test.go | 6 +++--- promql/functions.go | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/promql/engine_test.go b/promql/engine_test.go index 3f5f2dc132..9d43efdb90 100644 --- a/promql/engine_test.go +++ b/promql/engine_test.go @@ -3496,7 +3496,7 @@ func TestNativeHistogram_HistogramStdDevVar(t *testing.T) { }, NegativeBuckets: []int64{1, 0}, }, - stdVar: 1544.8582535368798, // actual variance: 1738.4082 + stdVar: 1844.4651144196398, // actual variance: 1738.4082 }, { name: "-100000, -10000, -1000, -888, -888, -100, -50, -9, -8, -3", @@ -3514,7 +3514,7 @@ func TestNativeHistogram_HistogramStdDevVar(t *testing.T) { }, NegativeBuckets: []int64{1, 0, 0, 0, 0, 2, -2, 0}, }, - stdVar: 1240930974.5260057, // actual variance: 882690990 + stdVar: 759352122.1939945, // actual variance: 882690990 }, { name: "-10 x10", @@ -3528,7 +3528,7 @@ func TestNativeHistogram_HistogramStdDevVar(t *testing.T) { }, NegativeBuckets: []int64{10}, }, - stdVar: 454.2741699796952, // actual variance: 0 + stdVar: 1.725830020304794, // actual variance: 0 }, { name: "-50, -8, 0, 3, 8, 9, 100, NaN", diff --git a/promql/functions.go b/promql/functions.go index da66af2f02..e0b811a09d 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -1116,6 +1116,9 @@ func funcHistogramStdDev(vals []parser.Value, args parser.Expressions, enh *Eval val = 0 } else { val = math.Sqrt(bucket.Upper * bucket.Lower) + if bucket.Upper < 0 { + val = -val + } } delta := val - mean variance, cVariance = kahanSumInc(bucket.Count*delta*delta, variance, cVariance) @@ -1149,6 +1152,9 @@ func funcHistogramStdVar(vals []parser.Value, args parser.Expressions, enh *Eval val = 0 } else { val = math.Sqrt(bucket.Upper * bucket.Lower) + if bucket.Upper < 0 { + val = -val + } } delta := val - mean variance, cVariance = kahanSumInc(bucket.Count*delta*delta, variance, cVariance) From 4f2df329bd8d49f69eff060da04edf91756a36fb Mon Sep 17 00:00:00 2001 From: Jeanette Tan Date: Fri, 22 Mar 2024 03:42:50 +0800 Subject: [PATCH 080/127] improve handling of empty buckets with infinite bounds in histogram std dev/var Signed-off-by: Jeanette Tan --- promql/functions.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/promql/functions.go b/promql/functions.go index e0b811a09d..6853307d0e 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -1111,6 +1111,9 @@ func funcHistogramStdDev(vals []parser.Value, args parser.Expressions, enh *Eval it := sample.H.AllBucketIterator() for it.Next() { bucket := it.At() + if bucket.Count == 0 { + continue + } var val float64 if bucket.Lower <= 0 && 0 <= bucket.Upper { val = 0 @@ -1147,6 +1150,9 @@ func funcHistogramStdVar(vals []parser.Value, args parser.Expressions, enh *Eval it := sample.H.AllBucketIterator() for it.Next() { bucket := it.At() + if bucket.Count == 0 { + continue + } var val float64 if bucket.Lower <= 0 && 0 <= bucket.Upper { val = 0 From 2c1f9558b26341c1842150dd235e0d01ef939b96 Mon Sep 17 00:00:00 2001 From: beorn7 Date: Wed, 27 Mar 2024 19:00:16 +0100 Subject: [PATCH 081/127] promql: Fix histogram comparison in test framework The definition of histograms in the test framework may create histograms in a non-compact form. Since histogram comparison relies on exact equality of the bucket layout, we have to compact the histograms created by the test framework language before comparing them to histograms returned from the PromQL engine. Signed-off-by: beorn7 --- promql/test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/promql/test.go b/promql/test.go index 296b3d3cad..a7b5b8b99f 100644 --- a/promql/test.go +++ b/promql/test.go @@ -563,7 +563,7 @@ func (ev *evalCmd) compareResult(result parser.Value) error { return fmt.Errorf("expected histogram value at index %v for %s to have timestamp %v, but it had timestamp %v (result has %s)", i, ev.metrics[hash], expected.T, actual.T, formatSeriesResult(s)) } - if !actual.H.Equals(expected.H) { + if !actual.H.Equals(expected.H.Compact(0)) { return fmt.Errorf("expected histogram value at index %v (t=%v) for %s to be %v, but got %v (result has %s)", i, actual.T, ev.metrics[hash], expected.H, actual.H, formatSeriesResult(s)) } } @@ -595,7 +595,7 @@ func (ev *evalCmd) compareResult(result parser.Value) error { if expH != nil && v.H == nil { return fmt.Errorf("expected histogram %s for %s but got float value %v", HistogramTestExpression(expH), v.Metric, v.F) } - if expH != nil && !expH.Equals(v.H) { + if expH != nil && !expH.Compact(0).Equals(v.H) { return fmt.Errorf("expected %v for %s but got %s", HistogramTestExpression(expH), v.Metric, HistogramTestExpression(v.H)) } if !almostEqual(exp0.Value, v.F, defaultEpsilon) { From 65b4696b88521197668c09dd08dd5e46777c6845 Mon Sep 17 00:00:00 2001 From: beorn7 Date: Wed, 27 Mar 2024 19:02:27 +0100 Subject: [PATCH 082/127] promql: Remove leftover debug output Signed-off-by: beorn7 --- promql/test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/promql/test.go b/promql/test.go index a7b5b8b99f..ba716d1eea 100644 --- a/promql/test.go +++ b/promql/test.go @@ -606,10 +606,6 @@ func (ev *evalCmd) compareResult(result parser.Value) error { } for fp, expVals := range ev.expected { if !seen[fp] { - fmt.Println("vector result", len(val), ev.expr) - for _, ss := range val { - fmt.Println(" ", ss.Metric, ss.T, ss.F) - } return fmt.Errorf("expected metric %s with %v not found", ev.metrics[fp], expVals) } } From 255098e0538fdc2ae51390ea386f0edc737c3a09 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Thu, 28 Mar 2024 10:05:10 +0000 Subject: [PATCH 083/127] CI: Publish step should require all Go tests to pass This was an unintentional effect of splitting out Go tests into multiple parallel blocks. Signed-off-by: Bryan Boreham --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 498db12b6f..8866384dba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,7 +172,7 @@ jobs: publish_main: name: Publish main branch artifacts runs-on: ubuntu-latest - needs: [test_ui, test_go, test_windows, golangci, codeql, build_all] + needs: [test_ui, test_go, test_go_more, test_go_oldest, test_windows, golangci, codeql, build_all] if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 @@ -186,7 +186,7 @@ jobs: publish_release: name: Publish release artefacts runs-on: ubuntu-latest - needs: [test_ui, test_go, test_windows, golangci, codeql, build_all] + needs: [test_ui, test_go, test_go_more, test_go_oldest, test_windows, golangci, codeql, build_all] if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v2.') steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 From fc3ad66539a3fc371bcd640ea20bfe079309fd74 Mon Sep 17 00:00:00 2001 From: beorn7 Date: Thu, 28 Mar 2024 18:33:39 +0100 Subject: [PATCH 084/127] Appoint release shepherds for v2.52 and v2.53 Note that we have delayed v2.52 by a week to avoid collisions with events and travels. Signed-off-by: beorn7 --- RELEASE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index c2f98ab2ce..f313c4172d 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -56,6 +56,8 @@ Release cadence of first pre-releases being cut is 6 weeks. | v2.49 | 2023-12-05 | Bartek Plotka (GitHub: @bwplotka) | | v2.50 | 2024-01-16 | Augustin Husson (GitHub: @nexucis) | | v2.51 | 2024-03-07 | Bryan Boreham (GitHub: @bboreham) | +| v2.52 | 2024-04-22 | Arthur Silva Sens (GitHub: @ArthurSens) | +| v2.53 | 2024-06-03 | George Krajcsovits (GitHub: @krajorama) | If you are interested in volunteering please create a pull request against the [prometheus/prometheus](https://github.com/prometheus/prometheus) repository and propose yourself for the release series of your choice. From 0b762db15434e9f85613a30cbe746023a849da26 Mon Sep 17 00:00:00 2001 From: Nicolas Takashi Date: Fri, 29 Mar 2024 23:33:15 +0000 Subject: [PATCH 085/127] [refactor] moving mergedOOOChunks to ooo_head_read Signed-off-by: Nicolas Takashi --- RELEASE.md | 2 ++ promql/test.go | 8 ++------ tsdb/head_read.go | 7 ------- tsdb/ooo_head_read.go | 7 +++++++ 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index c2f98ab2ce..f313c4172d 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -56,6 +56,8 @@ Release cadence of first pre-releases being cut is 6 weeks. | v2.49 | 2023-12-05 | Bartek Plotka (GitHub: @bwplotka) | | v2.50 | 2024-01-16 | Augustin Husson (GitHub: @nexucis) | | v2.51 | 2024-03-07 | Bryan Boreham (GitHub: @bboreham) | +| v2.52 | 2024-04-22 | Arthur Silva Sens (GitHub: @ArthurSens) | +| v2.53 | 2024-06-03 | George Krajcsovits (GitHub: @krajorama) | If you are interested in volunteering please create a pull request against the [prometheus/prometheus](https://github.com/prometheus/prometheus) repository and propose yourself for the release series of your choice. diff --git a/promql/test.go b/promql/test.go index 296b3d3cad..ba716d1eea 100644 --- a/promql/test.go +++ b/promql/test.go @@ -563,7 +563,7 @@ func (ev *evalCmd) compareResult(result parser.Value) error { return fmt.Errorf("expected histogram value at index %v for %s to have timestamp %v, but it had timestamp %v (result has %s)", i, ev.metrics[hash], expected.T, actual.T, formatSeriesResult(s)) } - if !actual.H.Equals(expected.H) { + if !actual.H.Equals(expected.H.Compact(0)) { return fmt.Errorf("expected histogram value at index %v (t=%v) for %s to be %v, but got %v (result has %s)", i, actual.T, ev.metrics[hash], expected.H, actual.H, formatSeriesResult(s)) } } @@ -595,7 +595,7 @@ func (ev *evalCmd) compareResult(result parser.Value) error { if expH != nil && v.H == nil { return fmt.Errorf("expected histogram %s for %s but got float value %v", HistogramTestExpression(expH), v.Metric, v.F) } - if expH != nil && !expH.Equals(v.H) { + if expH != nil && !expH.Compact(0).Equals(v.H) { return fmt.Errorf("expected %v for %s but got %s", HistogramTestExpression(expH), v.Metric, HistogramTestExpression(v.H)) } if !almostEqual(exp0.Value, v.F, defaultEpsilon) { @@ -606,10 +606,6 @@ func (ev *evalCmd) compareResult(result parser.Value) error { } for fp, expVals := range ev.expected { if !seen[fp] { - fmt.Println("vector result", len(val), ev.expr) - for _, ss := range val { - fmt.Println(" ", ss.Metric, ss.T, ss.F) - } return fmt.Errorf("expected metric %s with %v not found", ev.metrics[fp], expVals) } } diff --git a/tsdb/head_read.go b/tsdb/head_read.go index 10a4623924..0ad885da20 100644 --- a/tsdb/head_read.go +++ b/tsdb/head_read.go @@ -582,13 +582,6 @@ func (s *memSeries) oooMergedChunks(meta chunks.Meta, cdm *chunks.ChunkDiskMappe return mc, nil } -var _ chunkenc.Iterable = &mergedOOOChunks{} - -// mergedOOOChunks holds the list of iterables for overlapping chunks. -type mergedOOOChunks struct { - chunkIterables []chunkenc.Iterable -} - func (o mergedOOOChunks) Iterator(iterator chunkenc.Iterator) chunkenc.Iterator { return storage.ChainSampleIteratorFromIterables(iterator, o.chunkIterables) } diff --git a/tsdb/ooo_head_read.go b/tsdb/ooo_head_read.go index c9fe5cd580..4985ebb987 100644 --- a/tsdb/ooo_head_read.go +++ b/tsdb/ooo_head_read.go @@ -42,6 +42,13 @@ type OOOHeadIndexReader struct { lastGarbageCollectedMmapRef chunks.ChunkDiskMapperRef } +var _ chunkenc.Iterable = &mergedOOOChunks{} + +// mergedOOOChunks holds the list of iterables for overlapping chunks. +type mergedOOOChunks struct { + chunkIterables []chunkenc.Iterable +} + func NewOOOHeadIndexReader(head *Head, mint, maxt int64, lastGarbageCollectedMmapRef chunks.ChunkDiskMapperRef) *OOOHeadIndexReader { hr := &headIndexReader{ head: head, From ded35ef20dd070972ba43b2b3ec81df39f50e69e Mon Sep 17 00:00:00 2001 From: Ben Ye Date: Sun, 31 Mar 2024 15:10:29 -0700 Subject: [PATCH 086/127] expose compactor metrics Signed-off-by: Ben Ye --- tsdb/compact.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tsdb/compact.go b/tsdb/compact.go index 3d8d9130c4..4d345ffd4a 100644 --- a/tsdb/compact.go +++ b/tsdb/compact.go @@ -96,7 +96,8 @@ type CompactorMetrics struct { ChunkRange prometheus.Histogram } -func newCompactorMetrics(r prometheus.Registerer) *CompactorMetrics { +// NewCompactorMetrics initializes metrics for Compactor. +func NewCompactorMetrics(r prometheus.Registerer) *CompactorMetrics { m := &CompactorMetrics{} m.Ran = prometheus.NewCounter(prometheus.CounterOpts{ @@ -203,7 +204,7 @@ func NewLeveledCompactorWithOptions(ctx context.Context, r prometheus.Registerer ranges: ranges, chunkPool: pool, logger: l, - metrics: newCompactorMetrics(r), + metrics: NewCompactorMetrics(r), ctx: ctx, maxBlockChunkSegmentSize: maxBlockChunkSegmentSize, mergeFunc: mergeFunc, From a67266207377f8294dcd4bd3043ecfe6dac4a086 Mon Sep 17 00:00:00 2001 From: carehabit <165479941+carehabit@users.noreply.github.com> Date: Tue, 2 Apr 2024 00:06:05 +0800 Subject: [PATCH 087/127] all: fix some typos (#13863) Signed-off-by: carehabit --- model/labels/labels_test.go | 8 ++++---- model/labels/regexp_test.go | 2 +- model/rulefmt/testdata/bad_field.bad.yaml | 2 +- storage/remote/write_test.go | 2 +- tsdb/db.go | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/model/labels/labels_test.go b/model/labels/labels_test.go index 90ae41ccea..5ec7764ca9 100644 --- a/model/labels/labels_test.go +++ b/model/labels/labels_test.go @@ -25,20 +25,20 @@ import ( func TestLabels_String(t *testing.T) { cases := []struct { - lables Labels + labels Labels expected string }{ { - lables: FromStrings("t1", "t1", "t2", "t2"), + labels: FromStrings("t1", "t1", "t2", "t2"), expected: "{t1=\"t1\", t2=\"t2\"}", }, { - lables: Labels{}, + labels: Labels{}, expected: "{}", }, } for _, c := range cases { - str := c.lables.String() + str := c.labels.String() require.Equal(t, c.expected, str) } } diff --git a/model/labels/regexp_test.go b/model/labels/regexp_test.go index fc21459ed0..ac48bdc5b5 100644 --- a/model/labels/regexp_test.go +++ b/model/labels/regexp_test.go @@ -1045,7 +1045,7 @@ func visitStringMatcher(matcher StringMatcher, callback func(matcher StringMatch visitStringMatcher(entry, callback) } - // No nested matchers for the folling ones. + // No nested matchers for the following ones. case emptyStringMatcher: case *equalStringMatcher: case *equalMultiStringSliceMatcher: diff --git a/model/rulefmt/testdata/bad_field.bad.yaml b/model/rulefmt/testdata/bad_field.bad.yaml index d85eab1e5f..729bbadfbc 100644 --- a/model/rulefmt/testdata/bad_field.bad.yaml +++ b/model/rulefmt/testdata/bad_field.bad.yaml @@ -6,4 +6,4 @@ groups: labels: instance: localhost annotation: - summary: annonations is written without s above + summary: annotations is written without s above diff --git a/storage/remote/write_test.go b/storage/remote/write_test.go index 4a9a3bafc4..c79ac3ab7d 100644 --- a/storage/remote/write_test.go +++ b/storage/remote/write_test.go @@ -409,7 +409,7 @@ func generateOTLPWriteRequest(t *testing.T) pmetricotlp.ExportRequest { // Generate One Counter, One Gauge, One Histogram, One Exponential-Histogram // with resource attributes: service.name="test-service", service.instance.id="test-instance", host.name="test-host" - // with metric attibute: foo.bar="baz" + // with metric attribute: foo.bar="baz" timestamp := time.Now() diff --git a/tsdb/db.go b/tsdb/db.go index 856a9a68a3..293ba646ea 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -42,7 +42,7 @@ import ( "github.com/prometheus/prometheus/tsdb/chunks" tsdb_errors "github.com/prometheus/prometheus/tsdb/errors" "github.com/prometheus/prometheus/tsdb/fileutil" - _ "github.com/prometheus/prometheus/tsdb/goversion" // Load the package into main to make sure minium Go version is met. + _ "github.com/prometheus/prometheus/tsdb/goversion" // Load the package into main to make sure minimum Go version is met. "github.com/prometheus/prometheus/tsdb/tsdbutil" "github.com/prometheus/prometheus/tsdb/wlog" ) From 2e6c1c35a4dfe63350044713107dd0f4c2fcc433 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 23:37:04 +0000 Subject: [PATCH 088/127] build(deps): bump the go-opentelemetry-io group with 3 updates Bumps the go-opentelemetry-io group with 3 updates: [go.opentelemetry.io/collector/featuregate](https://github.com/open-telemetry/opentelemetry-collector), [go.opentelemetry.io/collector/pdata](https://github.com/open-telemetry/opentelemetry-collector) and [go.opentelemetry.io/collector/semconv](https://github.com/open-telemetry/opentelemetry-collector). Updates `go.opentelemetry.io/collector/featuregate` from 1.3.0 to 1.4.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/pdata/v1.3.0...pdata/v1.4.0) Updates `go.opentelemetry.io/collector/pdata` from 1.3.0 to 1.4.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/pdata/v1.3.0...pdata/v1.4.0) Updates `go.opentelemetry.io/collector/semconv` from 0.96.0 to 0.97.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.96.0...v0.97.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/collector/featuregate dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-opentelemetry-io - dependency-name: go.opentelemetry.io/collector/pdata dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-opentelemetry-io - 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] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 5ea1124fa7..8e9824b49d 100644 --- a/go.mod +++ b/go.mod @@ -60,9 +60,9 @@ require ( github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c github.com/stretchr/testify v1.9.0 github.com/vultr/govultr/v2 v2.17.2 - go.opentelemetry.io/collector/featuregate v1.3.0 - go.opentelemetry.io/collector/pdata v1.3.0 - go.opentelemetry.io/collector/semconv v0.96.0 + go.opentelemetry.io/collector/featuregate v1.4.0 + go.opentelemetry.io/collector/pdata v1.4.0 + go.opentelemetry.io/collector/semconv v0.97.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 go.opentelemetry.io/otel v1.24.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 diff --git a/go.sum b/go.sum index 1876425ea5..135e3afafc 100644 --- a/go.sum +++ b/go.sum @@ -720,12 +720,12 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/collector/featuregate v1.3.0 h1:nrFSx+zfjdisjE9oCx25Aep3nJ9RaUjeE1qFL6eovoU= -go.opentelemetry.io/collector/featuregate v1.3.0/go.mod h1:mm8+xyQfgDmqhyegZRNIQmoKsNnDTwWKFLsdMoXAb7A= -go.opentelemetry.io/collector/pdata v1.3.0 h1:JRYN7tVHYFwmtQhIYbxWeiKSa2L1nCohyAs8sYqKFZo= -go.opentelemetry.io/collector/pdata v1.3.0/go.mod h1:t7W0Undtes53HODPdSujPLTnfSR5fzT+WpL+RTaaayo= -go.opentelemetry.io/collector/semconv v0.96.0 h1:DrZy8BpzJDnN2zFxXRj6BhfGYxNlqpFHBqyuS9fVHRY= -go.opentelemetry.io/collector/semconv v0.96.0/go.mod h1:zOm/U3pgMIWcvrcnPbR9Xx2HinoXj46ERMK8PUV9wrs= +go.opentelemetry.io/collector/featuregate v1.4.0 h1:RWE9M659C9iuUQc4GzBsndkGHG1jIzIY+nZJWvcKy1M= +go.opentelemetry.io/collector/featuregate v1.4.0/go.mod h1:w7nUODKxEi3FLf1HslCiE6YWtMtOOrMnSwsDam8Mg9w= +go.opentelemetry.io/collector/pdata v1.4.0 h1:cA6Pr7Z2V7mE+i7FmYpavX7nefzd6H4CICgW0T9aJX0= +go.opentelemetry.io/collector/pdata v1.4.0/go.mod h1:0Ttp4wQinhV5oJTd9MjyvUegmZBO9O0nrlh/+EDLw+Q= +go.opentelemetry.io/collector/semconv v0.97.0 h1:iF3nTfThbiOwz7o5Pocn0dDnDoffd18ijDuf6Mwzi1s= +go.opentelemetry.io/collector/semconv v0.97.0/go.mod h1:8ElcRZ8Cdw5JnvhTOQOdYizkJaQ10Z2fS+R6djOnj6A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= From 9b7de4778732ca2f2ab5028e9d1955109f440c4c Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Tue, 2 Apr 2024 18:45:46 +0200 Subject: [PATCH 089/127] Remove unused Dmn field on EvalNodeHelper (#13877) https://github.com/prometheus/prometheus/pull/13446 removed the last usage of this field, but didn't remove the field. Signed-off-by: Julius Volz --- promql/engine.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index cf60f477af..2f7dcb222e 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1081,8 +1081,6 @@ type EvalNodeHelper struct { Out Vector // Caches. - // label_*. - Dmn map[uint64]labels.Labels // funcHistogramQuantile for classic histograms. signatureToMetricWithBuckets map[string]*metricWithBuckets From db64d2dcdc09f474b158c341f43468b67686b81a Mon Sep 17 00:00:00 2001 From: Arthur Silva Sens Date: Tue, 2 Apr 2024 19:13:39 -0300 Subject: [PATCH 090/127] Update documentation about existing feature-flags Signed-off-by: Arthur Silva Sens --- cmd/prometheus/main.go | 2 +- docs/command-line/prometheus.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go index f64c00e824..0e15d5ca5f 100644 --- a/cmd/prometheus/main.go +++ b/cmd/prometheus/main.go @@ -447,7 +447,7 @@ func main() { a.Flag("scrape.discovery-reload-interval", "Interval used by scrape manager to throttle target groups updates."). Hidden().Default("5s").SetValue(&cfg.scrape.DiscoveryReloadInterval) - a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: agent, auto-gomemlimit, exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-at-modifier, promql-negative-offset, promql-per-step-stats, promql-experimental-functions, remote-write-receiver (DEPRECATED), extra-scrape-metrics, new-service-discovery-manager, auto-gomaxprocs, no-default-scrape-port, native-histograms, otlp-write-receiver. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details."). + a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: agent, auto-gomemlimit, exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-per-step-stats, promql-experimental-functions, remote-write-receiver (DEPRECATED), extra-scrape-metrics, new-service-discovery-manager, auto-gomaxprocs, no-default-scrape-port, native-histograms, otlp-write-receiver, created-timestamp-zero-ingestion, concurrent-rule-eval. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details."). Default("").StringsVar(&cfg.featureList) promlogflag.AddFlags(a, &cfg.promlogConfig) diff --git a/docs/command-line/prometheus.md b/docs/command-line/prometheus.md index 2faea5b15e..93eaf251d0 100644 --- a/docs/command-line/prometheus.md +++ b/docs/command-line/prometheus.md @@ -54,7 +54,7 @@ The Prometheus monitoring server | --query.timeout | Maximum time a query may take before being aborted. Use with server mode only. | `2m` | | --query.max-concurrency | Maximum number of queries executed concurrently. Use with server mode only. | `20` | | --query.max-samples | Maximum number of samples a single query can load into memory. Note that queries will fail if they try to load more samples than this into memory, so this also limits the number of samples a query can return. Use with server mode only. | `50000000` | -| --enable-feature | Comma separated feature names to enable. Valid options: agent, auto-gomemlimit, exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-at-modifier, promql-negative-offset, promql-per-step-stats, promql-experimental-functions, remote-write-receiver (DEPRECATED), extra-scrape-metrics, new-service-discovery-manager, auto-gomaxprocs, no-default-scrape-port, native-histograms, otlp-write-receiver. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details. | | +| --enable-feature | Comma separated feature names to enable. Valid options: agent, auto-gomemlimit, exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-per-step-stats, promql-experimental-functions, remote-write-receiver (DEPRECATED), extra-scrape-metrics, new-service-discovery-manager, auto-gomaxprocs, no-default-scrape-port, native-histograms, otlp-write-receiver, created-timestamp-zero-ingestion, concurrent-rule-eval. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details. | | | --log.level | Only log messages with the given severity or above. One of: [debug, info, warn, error] | `info` | | --log.format | Output format of log messages. One of: [logfmt, json] | `logfmt` | From 812563408683d7c4e5048a6ebcda3d3eb802fe00 Mon Sep 17 00:00:00 2001 From: Nicolas Takashi Date: Wed, 3 Apr 2024 09:14:34 +0100 Subject: [PATCH 091/127] [refactor] moving mergedOOOChunks Iterator (#13881) Signed-off-by: Nicolas Takashi --- tsdb/head_read.go | 4 ---- tsdb/ooo_head_read.go | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tsdb/head_read.go b/tsdb/head_read.go index 0ad885da20..45bbc81f18 100644 --- a/tsdb/head_read.go +++ b/tsdb/head_read.go @@ -582,10 +582,6 @@ func (s *memSeries) oooMergedChunks(meta chunks.Meta, cdm *chunks.ChunkDiskMappe return mc, nil } -func (o mergedOOOChunks) Iterator(iterator chunkenc.Iterator) chunkenc.Iterator { - return storage.ChainSampleIteratorFromIterables(iterator, o.chunkIterables) -} - var _ chunkenc.Iterable = &boundedIterable{} // boundedIterable is an implementation of chunkenc.Iterable that uses a diff --git a/tsdb/ooo_head_read.go b/tsdb/ooo_head_read.go index 4985ebb987..ed0b3fd227 100644 --- a/tsdb/ooo_head_read.go +++ b/tsdb/ooo_head_read.go @@ -49,6 +49,10 @@ type mergedOOOChunks struct { chunkIterables []chunkenc.Iterable } +func (o mergedOOOChunks) Iterator(iterator chunkenc.Iterator) chunkenc.Iterator { + return storage.ChainSampleIteratorFromIterables(iterator, o.chunkIterables) +} + func NewOOOHeadIndexReader(head *Head, mint, maxt int64, lastGarbageCollectedMmapRef chunks.ChunkDiskMapperRef) *OOOHeadIndexReader { hr := &headIndexReader{ head: head, From cd72ebb05f79bdd4ddd96a65e3759e2310b173d8 Mon Sep 17 00:00:00 2001 From: Charles Korn Date: Wed, 3 Apr 2024 19:57:08 +1100 Subject: [PATCH 092/127] promql: include more details in error message when creating test query fails or an unexpected series is returned (#13847) * promql: include more details in error message when creating test query fails Signed-off-by: Charles Korn * Include more details when an unexpected metric is returned Signed-off-by: Charles Korn --------- Signed-off-by: Charles Korn --- promql/test.go | 14 +++++++++----- promql/test_test.go | 17 +++++++++++++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/promql/test.go b/promql/test.go index ba716d1eea..2a9a4a2b81 100644 --- a/promql/test.go +++ b/promql/test.go @@ -516,7 +516,7 @@ func (ev *evalCmd) compareResult(result parser.Value) error { for _, s := range val { hash := s.Metric.Hash() if _, ok := ev.metrics[hash]; !ok { - return fmt.Errorf("unexpected metric %s in result", s.Metric) + return fmt.Errorf("unexpected metric %s in result, has %s", s.Metric, formatSeriesResult(s)) } seen[hash] = true exp := ev.expected[hash] @@ -581,7 +581,11 @@ func (ev *evalCmd) compareResult(result parser.Value) error { for pos, v := range val { fp := v.Metric.Hash() if _, ok := ev.metrics[fp]; !ok { - return fmt.Errorf("unexpected metric %s in result", v.Metric) + if v.H != nil { + return fmt.Errorf("unexpected metric %s in result, has value %v", v.Metric, v.H) + } + + return fmt.Errorf("unexpected metric %s in result, has value %v", v.Metric, v.F) } exp := ev.expected[fp] if ev.ordered && exp.pos != pos+1 { @@ -763,7 +767,7 @@ func (t *test) execEval(cmd *evalCmd, engine QueryEngine) error { func (t *test) execRangeEval(cmd *evalCmd, engine QueryEngine) error { q, err := engine.NewRangeQuery(t.context, t.storage, nil, cmd.expr, cmd.start, cmd.end, cmd.step) if err != nil { - return err + return fmt.Errorf("error creating range query for %q (line %d): %w", cmd.expr, cmd.line, err) } res := q.Exec(t.context) if res.Err != nil { @@ -794,7 +798,7 @@ func (t *test) execInstantEval(cmd *evalCmd, engine QueryEngine) error { for _, iq := range queries { q, err := engine.NewInstantQuery(t.context, t.storage, nil, iq.expr, iq.evalTime) if err != nil { - return err + return fmt.Errorf("error creating instant query for %q (line %d): %w", cmd.expr, cmd.line, err) } defer q.Close() res := q.Exec(t.context) @@ -816,7 +820,7 @@ func (t *test) execInstantEval(cmd *evalCmd, engine QueryEngine) error { // by checking against the middle step. q, err = engine.NewRangeQuery(t.context, t.storage, nil, iq.expr, iq.evalTime.Add(-time.Minute), iq.evalTime.Add(time.Minute), time.Minute) if err != nil { - return err + return fmt.Errorf("error creating range query for %q (line %d): %w", cmd.expr, cmd.line, err) } rangeRes := q.Exec(t.context) if rangeRes.Err != nil { diff --git a/promql/test_test.go b/promql/test_test.go index 0130a789d7..a5b24ac698 100644 --- a/promql/test_test.go +++ b/promql/test_test.go @@ -224,12 +224,21 @@ eval instant at 0 testmetric `, expectedError: `error in eval testmetric (line 5): expected float value 2.000000 for {__name__="testmetric"} but got histogram {{}}`, }, - "instant query, but result has an unexpected series": { + "instant query, but result has an unexpected series with a float value": { input: testData + ` eval instant at 5m sum by (group) (http_requests) {group="production"} 30 `, - expectedError: `error in eval sum by (group) (http_requests) (line 8): unexpected metric {group="canary"} in result`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): unexpected metric {group="canary"} in result, has value 70`, + }, + "instant query, but result has an unexpected series with a histogram value": { + input: ` +load 5m + testmetric {{}} + +eval instant at 5m testmetric +`, + expectedError: `error in eval testmetric (line 5): unexpected metric {__name__="testmetric"} in result, has value {count:0, sum:0}`, }, "instant query, but result is missing a series": { input: testData + ` @@ -279,7 +288,7 @@ eval_ordered instant at 50m sort(http_requests) http_requests{group="production", instance="1", job="api-server"} 200 http_requests{group="canary", instance="0", job="api-server"} 300 `, - expectedError: `error in eval sort(http_requests) (line 8): unexpected metric {__name__="http_requests", group="canary", instance="1", job="api-server"} in result`, + expectedError: `error in eval sort(http_requests) (line 8): unexpected metric {__name__="http_requests", group="canary", instance="1", job="api-server"} in result, has value 400`, }, "instant query with invalid timestamp": { input: `eval instant at abc123 vector(0)`, @@ -350,7 +359,7 @@ eval range from 0 to 10m step 5m sum by (group) (http_requests) eval range from 0 to 10m step 5m sum by (group) (http_requests) {group="production"} 0 30 60 `, - expectedError: `error in eval sum by (group) (http_requests) (line 8): unexpected metric {group="canary"} in result`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): unexpected metric {group="canary"} in result, has 3 float points [0 @[0] 70 @[300000] 140 @[600000]] and 0 histogram points []`, }, "range query, but result is missing a series": { input: testData + ` From 7c2852145153026ca6adbe9280b64fd35c91ad39 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Wed, 3 Apr 2024 10:10:35 +0100 Subject: [PATCH 093/127] [TESTS] Truncate some long test names, for readability The strings produced by these tests can run to thousands of characters, which makes test logs difficult to read. Signed-off-by: Bryan Boreham --- model/labels/regexp_test.go | 10 +++++++++- promql/parser/parse_test.go | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/model/labels/regexp_test.go b/model/labels/regexp_test.go index ac48bdc5b5..3a15b52b40 100644 --- a/model/labels/regexp_test.go +++ b/model/labels/regexp_test.go @@ -100,7 +100,7 @@ func TestFastRegexMatcher_MatchString(t *testing.T) { r := r for _, v := range testValues { v := v - t.Run(r+` on "`+v+`"`, func(t *testing.T) { + t.Run(readable(r)+` on "`+readable(v)+`"`, func(t *testing.T) { t.Parallel() m, err := NewFastRegexMatcher(r) require.NoError(t, err) @@ -111,6 +111,14 @@ func TestFastRegexMatcher_MatchString(t *testing.T) { } } +func readable(s string) string { + const maxReadableStringLen = 40 + if len(s) < maxReadableStringLen { + return s + } + return s[:maxReadableStringLen] + "..." +} + func TestOptimizeConcatRegex(t *testing.T) { cases := []struct { regex string diff --git a/promql/parser/parse_test.go b/promql/parser/parse_test.go index ff6e0b0828..c56d845947 100644 --- a/promql/parser/parse_test.go +++ b/promql/parser/parse_test.go @@ -3696,9 +3696,17 @@ func makeInt64Pointer(val int64) *int64 { return valp } +func readable(s string) string { + const maxReadableStringLen = 40 + if len(s) < maxReadableStringLen { + return s + } + return s[:maxReadableStringLen] + "..." +} + func TestParseExpressions(t *testing.T) { for _, test := range testExpr { - t.Run(test.input, func(t *testing.T) { + t.Run(readable(test.input), func(t *testing.T) { expr, err := ParseExpr(test.input) // Unexpected errors are always caused by a bug. From 0249e080b4e16b9fe78336195b03b08fec5e9e50 Mon Sep 17 00:00:00 2001 From: komisan19 <18901496+komisan19@users.noreply.github.com> Date: Thu, 4 Apr 2024 03:11:16 +0900 Subject: [PATCH 094/127] refactor: utilize standard functions max/min Signed-off-by: komisan19 <18901496+komisan19@users.noreply.github.com> --- cmd/promtool/rules.go | 14 -------------- tsdb/head_append.go | 7 ------- tsdb/wlog/live_reader.go | 7 ------- 3 files changed, 28 deletions(-) diff --git a/cmd/promtool/rules.go b/cmd/promtool/rules.go index d8d6bb83e1..5a18644842 100644 --- a/cmd/promtool/rules.go +++ b/cmd/promtool/rules.go @@ -234,17 +234,3 @@ func (m *multipleAppender) flushAndCommit(ctx context.Context) error { } return nil } - -func max(x, y int64) int64 { - if x > y { - return x - } - return y -} - -func min(x, y int64) int64 { - if x < y { - return x - } - return y -} diff --git a/tsdb/head_append.go b/tsdb/head_append.go index 58d24ceb94..23c2c0fbd7 100644 --- a/tsdb/head_append.go +++ b/tsdb/head_append.go @@ -198,13 +198,6 @@ func (h *Head) AppendableMinValidTime() (int64, bool) { return h.appendableMinValidTime(), true } -func max(a, b int64) int64 { - if a > b { - return a - } - return b -} - func (h *Head) getAppendBuffer() []record.RefSample { b := h.appendPool.Get() if b == nil { diff --git a/tsdb/wlog/live_reader.go b/tsdb/wlog/live_reader.go index 905bbf00d6..6eaef5f396 100644 --- a/tsdb/wlog/live_reader.go +++ b/tsdb/wlog/live_reader.go @@ -327,10 +327,3 @@ func (r *LiveReader) readRecord() ([]byte, int, error) { return rec, length + recordHeaderSize, nil } - -func min(i, j int) int { - if i < j { - return i - } - return j -} From 113938aeb894e60c5706ff9ca993344a990a96e7 Mon Sep 17 00:00:00 2001 From: Jonathan Halterman Date: Thu, 4 Apr 2024 05:26:13 -0700 Subject: [PATCH 095/127] Log out of order when writing a block (#13888) Signed-off-by: Jonathan Halterman --- tsdb/compact.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tsdb/compact.go b/tsdb/compact.go index 3d8d9130c4..b83649564d 100644 --- a/tsdb/compact.go +++ b/tsdb/compact.go @@ -575,6 +575,7 @@ func (c *LeveledCompactor) Write(dest string, b BlockReader, mint, maxt int64, p "maxt", meta.MaxTime, "ulid", meta.ULID, "duration", time.Since(start), + "ooo", meta.Compaction.FromOutOfOrder(), ) return uid, nil } From 38b75bc0d70418945fb58e4ad7bf55f3b9c8ffe2 Mon Sep 17 00:00:00 2001 From: dandrucz <161746538+dandrucz@users.noreply.github.com> Date: Fri, 5 Apr 2024 04:31:59 -0400 Subject: [PATCH 096/127] Linode IPv6 Range support, Optional region filtering, Missing fields in Documentation fixed (#13774) * Add support for discovering Linode IPv6 ranges associated with linodes. * Add optional but recommended region filtering (faster queries, more relevant information). * Added missing fields in configuration.md, updated linode test cases. * Convert to TableDrivenTests as per tjhop request. Signed-off-by: David Andruczyk --- discovery/linode/linode.go | 51 +- discovery/linode/linode_test.go | 367 +++++++++------ discovery/linode/mock_test.go | 439 ++---------------- .../ca-central/v4/account/events.json | 6 + .../ca-central/v4/linode/instances.json | 49 ++ .../ca-central/v4/networking/ips.json | 29 ++ .../ca-central/v4/networking/ipv6/ranges.json | 13 + .../no_region_filter/v4/account/events.json | 6 + .../no_region_filter/v4/linode/instances.json | 180 +++++++ .../no_region_filter/v4/networking/ips.json | 150 ++++++ .../v4/networking/ipv6/ranges.json | 19 + .../testdata/us-east/v4/account/events.json | 6 + .../testdata/us-east/v4/linode/instances.json | 97 ++++ .../testdata/us-east/v4/networking/ips.json | 106 +++++ .../us-east/v4/networking/ipv6/ranges.json | 13 + docs/configuration/configuration.md | 8 + documentation/examples/prometheus-linode.yml | 1 + 17 files changed, 982 insertions(+), 558 deletions(-) create mode 100644 discovery/linode/testdata/ca-central/v4/account/events.json create mode 100644 discovery/linode/testdata/ca-central/v4/linode/instances.json create mode 100644 discovery/linode/testdata/ca-central/v4/networking/ips.json create mode 100644 discovery/linode/testdata/ca-central/v4/networking/ipv6/ranges.json create mode 100644 discovery/linode/testdata/no_region_filter/v4/account/events.json create mode 100644 discovery/linode/testdata/no_region_filter/v4/linode/instances.json create mode 100644 discovery/linode/testdata/no_region_filter/v4/networking/ips.json create mode 100644 discovery/linode/testdata/no_region_filter/v4/networking/ipv6/ranges.json create mode 100644 discovery/linode/testdata/us-east/v4/account/events.json create mode 100644 discovery/linode/testdata/us-east/v4/linode/instances.json create mode 100644 discovery/linode/testdata/us-east/v4/networking/ips.json create mode 100644 discovery/linode/testdata/us-east/v4/networking/ipv6/ranges.json diff --git a/discovery/linode/linode.go b/discovery/linode/linode.go index 94f0a63bbb..2a5475b854 100644 --- a/discovery/linode/linode.go +++ b/discovery/linode/linode.go @@ -59,17 +59,22 @@ const ( linodeLabelSpecsVCPUs = linodeLabel + "specs_vcpus" linodeLabelSpecsTransferBytes = linodeLabel + "specs_transfer_bytes" linodeLabelExtraIPs = linodeLabel + "extra_ips" + linodeLabelIPv6Ranges = linodeLabel + "ipv6_ranges" // This is our events filter; when polling for changes, we care only about // events since our last refresh. - // Docs: https://www.linode.com/docs/api/account/#events-list + // Docs: https://www.linode.com/docs/api/account/#events-list. filterTemplate = `{"created": {"+gte": "%s"}}` + + // Optional region filtering. + regionFilterTemplate = `{"region": "%s"}` ) // DefaultSDConfig is the default Linode SD configuration. var DefaultSDConfig = SDConfig{ TagSeparator: ",", Port: 80, + Region: "", RefreshInterval: model.Duration(60 * time.Second), HTTPClientConfig: config.DefaultHTTPClientConfig, } @@ -85,6 +90,7 @@ type SDConfig struct { RefreshInterval model.Duration `yaml:"refresh_interval"` Port int `yaml:"port"` TagSeparator string `yaml:"tag_separator,omitempty"` + Region string `yaml:"region,omitempty"` } // NewDiscovererMetrics implements discovery.Config. @@ -122,6 +128,7 @@ type Discovery struct { *refresh.Discovery client *linodego.Client port int + region string tagSeparator string lastRefreshTimestamp time.Time pollCount int @@ -139,6 +146,7 @@ func NewDiscovery(conf *SDConfig, logger log.Logger, metrics discovery.Discovere d := &Discovery{ port: conf.Port, + region: conf.Region, tagSeparator: conf.TagSeparator, pollCount: 0, lastRefreshTimestamp: time.Now().UTC(), @@ -224,16 +232,31 @@ func (d *Discovery) refreshData(ctx context.Context) ([]*targetgroup.Group, erro tg := &targetgroup.Group{ Source: "Linode", } + opts := linodego.ListOptions{ + PageSize: 500, + } + + // If region filter provided, use it to constrain results. + if d.region != "" { + opts.Filter = fmt.Sprintf(regionFilterTemplate, d.region) + } // Gather all linode instances. - instances, err := d.client.ListInstances(ctx, &linodego.ListOptions{PageSize: 500}) + instances, err := d.client.ListInstances(ctx, &opts) if err != nil { d.metrics.failuresCount.Inc() return nil, err } // Gather detailed IP address info for all IPs on all linode instances. - detailedIPs, err := d.client.ListIPAddresses(ctx, &linodego.ListOptions{PageSize: 500}) + detailedIPs, err := d.client.ListIPAddresses(ctx, &opts) + if err != nil { + d.metrics.failuresCount.Inc() + return nil, err + } + + // Gather detailed IPv6 Range info for all linode instances. + ipv6RangeList, err := d.client.ListIPv6Ranges(ctx, &opts) if err != nil { d.metrics.failuresCount.Inc() return nil, err @@ -248,7 +271,7 @@ func (d *Discovery) refreshData(ctx context.Context) ([]*targetgroup.Group, erro privateIPv4, publicIPv4, publicIPv6 string privateIPv4RDNS, publicIPv4RDNS, publicIPv6RDNS string backupsStatus string - extraIPs []string + extraIPs, ipv6Ranges []string ) for _, ip := range instance.IPv4 { @@ -276,17 +299,23 @@ func (d *Discovery) refreshData(ctx context.Context) ([]*targetgroup.Group, erro } if instance.IPv6 != "" { + slaac := strings.Split(instance.IPv6, "/")[0] for _, detailedIP := range detailedIPs { - if detailedIP.Address != strings.Split(instance.IPv6, "/")[0] { + if detailedIP.Address != slaac { continue } - publicIPv6 = detailedIP.Address if detailedIP.RDNS != "" && detailedIP.RDNS != "null" { publicIPv6RDNS = detailedIP.RDNS } } + for _, ipv6Range := range ipv6RangeList { + if ipv6Range.RouteTarget != slaac { + continue + } + ipv6Ranges = append(ipv6Ranges, fmt.Sprintf("%s/%d", ipv6Range.Range, ipv6Range.Prefix)) + } } if instance.Backups.Enabled { @@ -330,12 +359,20 @@ func (d *Discovery) refreshData(ctx context.Context) ([]*targetgroup.Group, erro if len(extraIPs) > 0 { // This instance has more than one of at least one type of IP address (public, private, - // IPv4, IPv6, etc. We provide those extra IPs found here just like we do for instance + // IPv4,etc. We provide those extra IPs found here just like we do for instance // tags, we surround a separated list with the tagSeparator config. ips := d.tagSeparator + strings.Join(extraIPs, d.tagSeparator) + d.tagSeparator labels[linodeLabelExtraIPs] = model.LabelValue(ips) } + if len(ipv6Ranges) > 0 { + // This instance has more than one IPv6 Ranges routed to it we provide these + // Ranges found here just like we do for instance tags, we surround a separated + // list with the tagSeparator config. + ips := d.tagSeparator + strings.Join(ipv6Ranges, d.tagSeparator) + d.tagSeparator + labels[linodeLabelIPv6Ranges] = model.LabelValue(ips) + } + tg.Targets = append(tg.Targets, labels) } return []*targetgroup.Group{tg}, nil diff --git a/discovery/linode/linode_test.go b/discovery/linode/linode_test.go index a6a16d82aa..3c10650653 100644 --- a/discovery/linode/linode_test.go +++ b/discovery/linode/linode_test.go @@ -28,159 +28,236 @@ import ( "github.com/prometheus/prometheus/discovery" ) -type LinodeSDTestSuite struct { - Mock *SDMock -} - -func (s *LinodeSDTestSuite) TearDownSuite() { - s.Mock.ShutdownServer() -} - -func (s *LinodeSDTestSuite) SetupTest(t *testing.T) { - s.Mock = NewSDMock(t) - s.Mock.Setup() - - s.Mock.HandleLinodeInstancesList() - s.Mock.HandleLinodeNeworkingIPs() - s.Mock.HandleLinodeAccountEvents() -} - func TestLinodeSDRefresh(t *testing.T) { - sdmock := &LinodeSDTestSuite{} - sdmock.SetupTest(t) - t.Cleanup(sdmock.TearDownSuite) + sdmock := NewSDMock(t) + sdmock.Setup() - cfg := DefaultSDConfig - cfg.HTTPClientConfig.Authorization = &config.Authorization{ - Credentials: tokenID, - Type: "Bearer", + tests := map[string]struct { + region string + targetCount int + want []model.LabelSet + }{ + "no_region": {region: "", targetCount: 4, want: []model.LabelSet{ + { + "__address__": model.LabelValue("45.33.82.151:80"), + "__meta_linode_instance_id": model.LabelValue("26838044"), + "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-1"), + "__meta_linode_image": model.LabelValue("linode/arch"), + "__meta_linode_private_ipv4": model.LabelValue("192.168.170.51"), + "__meta_linode_public_ipv4": model.LabelValue("45.33.82.151"), + "__meta_linode_public_ipv6": model.LabelValue("2600:3c03::f03c:92ff:fe1a:1382"), + "__meta_linode_private_ipv4_rdns": model.LabelValue(""), + "__meta_linode_public_ipv4_rdns": model.LabelValue("li1028-151.members.linode.com"), + "__meta_linode_public_ipv6_rdns": model.LabelValue(""), + "__meta_linode_region": model.LabelValue("us-east"), + "__meta_linode_type": model.LabelValue("g6-standard-2"), + "__meta_linode_status": model.LabelValue("running"), + "__meta_linode_tags": model.LabelValue(",monitoring,"), + "__meta_linode_group": model.LabelValue(""), + "__meta_linode_gpus": model.LabelValue("0"), + "__meta_linode_hypervisor": model.LabelValue("kvm"), + "__meta_linode_backups": model.LabelValue("disabled"), + "__meta_linode_specs_disk_bytes": model.LabelValue("85899345920"), + "__meta_linode_specs_memory_bytes": model.LabelValue("4294967296"), + "__meta_linode_specs_vcpus": model.LabelValue("2"), + "__meta_linode_specs_transfer_bytes": model.LabelValue("4194304000"), + "__meta_linode_extra_ips": model.LabelValue(",96.126.108.16,192.168.201.25,"), + }, + { + "__address__": model.LabelValue("139.162.196.43:80"), + "__meta_linode_instance_id": model.LabelValue("26848419"), + "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-2"), + "__meta_linode_image": model.LabelValue("linode/debian10"), + "__meta_linode_private_ipv4": model.LabelValue(""), + "__meta_linode_public_ipv4": model.LabelValue("139.162.196.43"), + "__meta_linode_public_ipv6": model.LabelValue("2a01:7e00::f03c:92ff:fe1a:9976"), + "__meta_linode_private_ipv4_rdns": model.LabelValue(""), + "__meta_linode_public_ipv4_rdns": model.LabelValue("li1359-43.members.linode.com"), + "__meta_linode_public_ipv6_rdns": model.LabelValue(""), + "__meta_linode_region": model.LabelValue("eu-west"), + "__meta_linode_type": model.LabelValue("g6-standard-2"), + "__meta_linode_status": model.LabelValue("running"), + "__meta_linode_tags": model.LabelValue(",monitoring,"), + "__meta_linode_group": model.LabelValue(""), + "__meta_linode_gpus": model.LabelValue("0"), + "__meta_linode_hypervisor": model.LabelValue("kvm"), + "__meta_linode_backups": model.LabelValue("disabled"), + "__meta_linode_specs_disk_bytes": model.LabelValue("85899345920"), + "__meta_linode_specs_memory_bytes": model.LabelValue("4294967296"), + "__meta_linode_specs_vcpus": model.LabelValue("2"), + "__meta_linode_specs_transfer_bytes": model.LabelValue("4194304000"), + }, + { + "__address__": model.LabelValue("192.53.120.25:80"), + "__meta_linode_instance_id": model.LabelValue("26837938"), + "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-3"), + "__meta_linode_image": model.LabelValue("linode/ubuntu20.04"), + "__meta_linode_private_ipv4": model.LabelValue(""), + "__meta_linode_public_ipv4": model.LabelValue("192.53.120.25"), + "__meta_linode_public_ipv6": model.LabelValue("2600:3c04::f03c:92ff:fe1a:fb68"), + "__meta_linode_private_ipv4_rdns": model.LabelValue(""), + "__meta_linode_public_ipv4_rdns": model.LabelValue("li2216-25.members.linode.com"), + "__meta_linode_public_ipv6_rdns": model.LabelValue(""), + "__meta_linode_region": model.LabelValue("ca-central"), + "__meta_linode_type": model.LabelValue("g6-standard-1"), + "__meta_linode_status": model.LabelValue("running"), + "__meta_linode_tags": model.LabelValue(",monitoring,"), + "__meta_linode_group": model.LabelValue(""), + "__meta_linode_gpus": model.LabelValue("0"), + "__meta_linode_hypervisor": model.LabelValue("kvm"), + "__meta_linode_backups": model.LabelValue("disabled"), + "__meta_linode_specs_disk_bytes": model.LabelValue("53687091200"), + "__meta_linode_specs_memory_bytes": model.LabelValue("2147483648"), + "__meta_linode_specs_vcpus": model.LabelValue("1"), + "__meta_linode_specs_transfer_bytes": model.LabelValue("2097152000"), + "__meta_linode_ipv6_ranges": model.LabelValue(",2600:3c04:e001:456::/64,"), + }, + { + "__address__": model.LabelValue("66.228.47.103:80"), + "__meta_linode_instance_id": model.LabelValue("26837992"), + "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-4"), + "__meta_linode_image": model.LabelValue("linode/ubuntu20.04"), + "__meta_linode_private_ipv4": model.LabelValue("192.168.148.94"), + "__meta_linode_public_ipv4": model.LabelValue("66.228.47.103"), + "__meta_linode_public_ipv6": model.LabelValue("2600:3c03::f03c:92ff:fe1a:fb4c"), + "__meta_linode_private_ipv4_rdns": model.LabelValue(""), + "__meta_linode_public_ipv4_rdns": model.LabelValue("li328-103.members.linode.com"), + "__meta_linode_public_ipv6_rdns": model.LabelValue(""), + "__meta_linode_region": model.LabelValue("us-east"), + "__meta_linode_type": model.LabelValue("g6-nanode-1"), + "__meta_linode_status": model.LabelValue("running"), + "__meta_linode_tags": model.LabelValue(",monitoring,"), + "__meta_linode_group": model.LabelValue(""), + "__meta_linode_gpus": model.LabelValue("0"), + "__meta_linode_hypervisor": model.LabelValue("kvm"), + "__meta_linode_backups": model.LabelValue("disabled"), + "__meta_linode_specs_disk_bytes": model.LabelValue("26843545600"), + "__meta_linode_specs_memory_bytes": model.LabelValue("1073741824"), + "__meta_linode_specs_vcpus": model.LabelValue("1"), + "__meta_linode_specs_transfer_bytes": model.LabelValue("1048576000"), + "__meta_linode_extra_ips": model.LabelValue(",172.104.18.104,"), + "__meta_linode_ipv6_ranges": model.LabelValue(",2600:3c03:e000:123::/64,"), + }, + }}, + "us-east": {region: "us-east", targetCount: 2, want: []model.LabelSet{ + { + "__address__": model.LabelValue("45.33.82.151:80"), + "__meta_linode_instance_id": model.LabelValue("26838044"), + "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-1"), + "__meta_linode_image": model.LabelValue("linode/arch"), + "__meta_linode_private_ipv4": model.LabelValue("192.168.170.51"), + "__meta_linode_public_ipv4": model.LabelValue("45.33.82.151"), + "__meta_linode_public_ipv6": model.LabelValue("2600:3c03::f03c:92ff:fe1a:1382"), + "__meta_linode_private_ipv4_rdns": model.LabelValue(""), + "__meta_linode_public_ipv4_rdns": model.LabelValue("li1028-151.members.linode.com"), + "__meta_linode_public_ipv6_rdns": model.LabelValue(""), + "__meta_linode_region": model.LabelValue("us-east"), + "__meta_linode_type": model.LabelValue("g6-standard-2"), + "__meta_linode_status": model.LabelValue("running"), + "__meta_linode_tags": model.LabelValue(",monitoring,"), + "__meta_linode_group": model.LabelValue(""), + "__meta_linode_gpus": model.LabelValue("0"), + "__meta_linode_hypervisor": model.LabelValue("kvm"), + "__meta_linode_backups": model.LabelValue("disabled"), + "__meta_linode_specs_disk_bytes": model.LabelValue("85899345920"), + "__meta_linode_specs_memory_bytes": model.LabelValue("4294967296"), + "__meta_linode_specs_vcpus": model.LabelValue("2"), + "__meta_linode_specs_transfer_bytes": model.LabelValue("4194304000"), + "__meta_linode_extra_ips": model.LabelValue(",96.126.108.16,192.168.201.25,"), + }, + { + "__address__": model.LabelValue("66.228.47.103:80"), + "__meta_linode_instance_id": model.LabelValue("26837992"), + "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-4"), + "__meta_linode_image": model.LabelValue("linode/ubuntu20.04"), + "__meta_linode_private_ipv4": model.LabelValue("192.168.148.94"), + "__meta_linode_public_ipv4": model.LabelValue("66.228.47.103"), + "__meta_linode_public_ipv6": model.LabelValue("2600:3c03::f03c:92ff:fe1a:fb4c"), + "__meta_linode_private_ipv4_rdns": model.LabelValue(""), + "__meta_linode_public_ipv4_rdns": model.LabelValue("li328-103.members.linode.com"), + "__meta_linode_public_ipv6_rdns": model.LabelValue(""), + "__meta_linode_region": model.LabelValue("us-east"), + "__meta_linode_type": model.LabelValue("g6-nanode-1"), + "__meta_linode_status": model.LabelValue("running"), + "__meta_linode_tags": model.LabelValue(",monitoring,"), + "__meta_linode_group": model.LabelValue(""), + "__meta_linode_gpus": model.LabelValue("0"), + "__meta_linode_hypervisor": model.LabelValue("kvm"), + "__meta_linode_backups": model.LabelValue("disabled"), + "__meta_linode_specs_disk_bytes": model.LabelValue("26843545600"), + "__meta_linode_specs_memory_bytes": model.LabelValue("1073741824"), + "__meta_linode_specs_vcpus": model.LabelValue("1"), + "__meta_linode_specs_transfer_bytes": model.LabelValue("1048576000"), + "__meta_linode_extra_ips": model.LabelValue(",172.104.18.104,"), + "__meta_linode_ipv6_ranges": model.LabelValue(",2600:3c03:e000:123::/64,"), + }, + }}, + "us-central": {region: "ca-central", targetCount: 1, want: []model.LabelSet{ + { + "__address__": model.LabelValue("192.53.120.25:80"), + "__meta_linode_instance_id": model.LabelValue("26837938"), + "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-3"), + "__meta_linode_image": model.LabelValue("linode/ubuntu20.04"), + "__meta_linode_private_ipv4": model.LabelValue(""), + "__meta_linode_public_ipv4": model.LabelValue("192.53.120.25"), + "__meta_linode_public_ipv6": model.LabelValue("2600:3c04::f03c:92ff:fe1a:fb68"), + "__meta_linode_private_ipv4_rdns": model.LabelValue(""), + "__meta_linode_public_ipv4_rdns": model.LabelValue("li2216-25.members.linode.com"), + "__meta_linode_public_ipv6_rdns": model.LabelValue(""), + "__meta_linode_region": model.LabelValue("ca-central"), + "__meta_linode_type": model.LabelValue("g6-standard-1"), + "__meta_linode_status": model.LabelValue("running"), + "__meta_linode_tags": model.LabelValue(",monitoring,"), + "__meta_linode_group": model.LabelValue(""), + "__meta_linode_gpus": model.LabelValue("0"), + "__meta_linode_hypervisor": model.LabelValue("kvm"), + "__meta_linode_backups": model.LabelValue("disabled"), + "__meta_linode_specs_disk_bytes": model.LabelValue("53687091200"), + "__meta_linode_specs_memory_bytes": model.LabelValue("2147483648"), + "__meta_linode_specs_vcpus": model.LabelValue("1"), + "__meta_linode_specs_transfer_bytes": model.LabelValue("2097152000"), + "__meta_linode_ipv6_ranges": model.LabelValue(",2600:3c04:e001:456::/64,"), + }, + }}, } - reg := prometheus.NewRegistry() - refreshMetrics := discovery.NewRefreshMetrics(reg) - metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics) - require.NoError(t, metrics.Register()) - defer metrics.Unregister() - defer refreshMetrics.Unregister() + for _, tc := range tests { + cfg := DefaultSDConfig + if tc.region != "" { + cfg.Region = tc.region + } + cfg.HTTPClientConfig.Authorization = &config.Authorization{ + Credentials: tokenID, + Type: "Bearer", + } - d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics) - require.NoError(t, err) - endpoint, err := url.Parse(sdmock.Mock.Endpoint()) - require.NoError(t, err) - d.client.SetBaseURL(endpoint.String()) + reg := prometheus.NewRegistry() + refreshMetrics := discovery.NewRefreshMetrics(reg) + metrics := cfg.NewDiscovererMetrics(reg, refreshMetrics) + require.NoError(t, metrics.Register()) + defer metrics.Unregister() + defer refreshMetrics.Unregister() - tgs, err := d.refresh(context.Background()) - require.NoError(t, err) + d, err := NewDiscovery(&cfg, log.NewNopLogger(), metrics) + require.NoError(t, err) + endpoint, err := url.Parse(sdmock.Endpoint()) + require.NoError(t, err) + d.client.SetBaseURL(endpoint.String()) - require.Len(t, tgs, 1) + tgs, err := d.refresh(context.Background()) + require.NoError(t, err) - tg := tgs[0] - require.NotNil(t, tg) - require.NotNil(t, tg.Targets) - require.Len(t, tg.Targets, 4) + require.Len(t, tgs, 1) - for i, lbls := range []model.LabelSet{ - { - "__address__": model.LabelValue("45.33.82.151:80"), - "__meta_linode_instance_id": model.LabelValue("26838044"), - "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-1"), - "__meta_linode_image": model.LabelValue("linode/arch"), - "__meta_linode_private_ipv4": model.LabelValue("192.168.170.51"), - "__meta_linode_public_ipv4": model.LabelValue("45.33.82.151"), - "__meta_linode_public_ipv6": model.LabelValue("2600:3c03::f03c:92ff:fe1a:1382"), - "__meta_linode_private_ipv4_rdns": model.LabelValue(""), - "__meta_linode_public_ipv4_rdns": model.LabelValue("li1028-151.members.linode.com"), - "__meta_linode_public_ipv6_rdns": model.LabelValue(""), - "__meta_linode_region": model.LabelValue("us-east"), - "__meta_linode_type": model.LabelValue("g6-standard-2"), - "__meta_linode_status": model.LabelValue("running"), - "__meta_linode_tags": model.LabelValue(",monitoring,"), - "__meta_linode_group": model.LabelValue(""), - "__meta_linode_gpus": model.LabelValue("0"), - "__meta_linode_hypervisor": model.LabelValue("kvm"), - "__meta_linode_backups": model.LabelValue("disabled"), - "__meta_linode_specs_disk_bytes": model.LabelValue("85899345920"), - "__meta_linode_specs_memory_bytes": model.LabelValue("4294967296"), - "__meta_linode_specs_vcpus": model.LabelValue("2"), - "__meta_linode_specs_transfer_bytes": model.LabelValue("4194304000"), - "__meta_linode_extra_ips": model.LabelValue(",96.126.108.16,192.168.201.25,"), - }, - { - "__address__": model.LabelValue("139.162.196.43:80"), - "__meta_linode_instance_id": model.LabelValue("26848419"), - "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-2"), - "__meta_linode_image": model.LabelValue("linode/debian10"), - "__meta_linode_private_ipv4": model.LabelValue(""), - "__meta_linode_public_ipv4": model.LabelValue("139.162.196.43"), - "__meta_linode_public_ipv6": model.LabelValue("2a01:7e00::f03c:92ff:fe1a:9976"), - "__meta_linode_private_ipv4_rdns": model.LabelValue(""), - "__meta_linode_public_ipv4_rdns": model.LabelValue("li1359-43.members.linode.com"), - "__meta_linode_public_ipv6_rdns": model.LabelValue(""), - "__meta_linode_region": model.LabelValue("eu-west"), - "__meta_linode_type": model.LabelValue("g6-standard-2"), - "__meta_linode_status": model.LabelValue("running"), - "__meta_linode_tags": model.LabelValue(",monitoring,"), - "__meta_linode_group": model.LabelValue(""), - "__meta_linode_gpus": model.LabelValue("0"), - "__meta_linode_hypervisor": model.LabelValue("kvm"), - "__meta_linode_backups": model.LabelValue("disabled"), - "__meta_linode_specs_disk_bytes": model.LabelValue("85899345920"), - "__meta_linode_specs_memory_bytes": model.LabelValue("4294967296"), - "__meta_linode_specs_vcpus": model.LabelValue("2"), - "__meta_linode_specs_transfer_bytes": model.LabelValue("4194304000"), - }, - { - "__address__": model.LabelValue("192.53.120.25:80"), - "__meta_linode_instance_id": model.LabelValue("26837938"), - "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-3"), - "__meta_linode_image": model.LabelValue("linode/ubuntu20.04"), - "__meta_linode_private_ipv4": model.LabelValue(""), - "__meta_linode_public_ipv4": model.LabelValue("192.53.120.25"), - "__meta_linode_public_ipv6": model.LabelValue("2600:3c04::f03c:92ff:fe1a:fb68"), - "__meta_linode_private_ipv4_rdns": model.LabelValue(""), - "__meta_linode_public_ipv4_rdns": model.LabelValue("li2216-25.members.linode.com"), - "__meta_linode_public_ipv6_rdns": model.LabelValue(""), - "__meta_linode_region": model.LabelValue("ca-central"), - "__meta_linode_type": model.LabelValue("g6-standard-1"), - "__meta_linode_status": model.LabelValue("running"), - "__meta_linode_tags": model.LabelValue(",monitoring,"), - "__meta_linode_group": model.LabelValue(""), - "__meta_linode_gpus": model.LabelValue("0"), - "__meta_linode_hypervisor": model.LabelValue("kvm"), - "__meta_linode_backups": model.LabelValue("disabled"), - "__meta_linode_specs_disk_bytes": model.LabelValue("53687091200"), - "__meta_linode_specs_memory_bytes": model.LabelValue("2147483648"), - "__meta_linode_specs_vcpus": model.LabelValue("1"), - "__meta_linode_specs_transfer_bytes": model.LabelValue("2097152000"), - }, - { - "__address__": model.LabelValue("66.228.47.103:80"), - "__meta_linode_instance_id": model.LabelValue("26837992"), - "__meta_linode_instance_label": model.LabelValue("prometheus-linode-sd-exporter-4"), - "__meta_linode_image": model.LabelValue("linode/ubuntu20.04"), - "__meta_linode_private_ipv4": model.LabelValue("192.168.148.94"), - "__meta_linode_public_ipv4": model.LabelValue("66.228.47.103"), - "__meta_linode_public_ipv6": model.LabelValue("2600:3c03::f03c:92ff:fe1a:fb4c"), - "__meta_linode_private_ipv4_rdns": model.LabelValue(""), - "__meta_linode_public_ipv4_rdns": model.LabelValue("li328-103.members.linode.com"), - "__meta_linode_public_ipv6_rdns": model.LabelValue(""), - "__meta_linode_region": model.LabelValue("us-east"), - "__meta_linode_type": model.LabelValue("g6-nanode-1"), - "__meta_linode_status": model.LabelValue("running"), - "__meta_linode_tags": model.LabelValue(",monitoring,"), - "__meta_linode_group": model.LabelValue(""), - "__meta_linode_gpus": model.LabelValue("0"), - "__meta_linode_hypervisor": model.LabelValue("kvm"), - "__meta_linode_backups": model.LabelValue("disabled"), - "__meta_linode_specs_disk_bytes": model.LabelValue("26843545600"), - "__meta_linode_specs_memory_bytes": model.LabelValue("1073741824"), - "__meta_linode_specs_vcpus": model.LabelValue("1"), - "__meta_linode_specs_transfer_bytes": model.LabelValue("1048576000"), - "__meta_linode_extra_ips": model.LabelValue(",172.104.18.104,"), - }, - } { - t.Run(fmt.Sprintf("item %d", i), func(t *testing.T) { - require.Equal(t, lbls, tg.Targets[i]) - }) + tg := tgs[0] + require.NotNil(t, tg) + require.NotNil(t, tg.Targets) + require.Len(t, tg.Targets, tc.targetCount) + + for i, lbls := range tc.want { + t.Run(fmt.Sprintf("item %d", i), func(t *testing.T) { + require.Equal(t, lbls, tg.Targets[i]) + }) + } } } diff --git a/discovery/linode/mock_test.go b/discovery/linode/mock_test.go index ea0e8e0a8b..50f0572ecd 100644 --- a/discovery/linode/mock_test.go +++ b/discovery/linode/mock_test.go @@ -14,12 +14,17 @@ package linode import ( + "encoding/json" "fmt" "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" ) +const tokenID = "7b2c56dd51edd90952c1b94c472b94b176f20c5c777e376849edd8ad1c6c03bb" + // SDMock is the interface for the Linode mock. type SDMock struct { t *testing.T @@ -43,412 +48,34 @@ func (m *SDMock) Endpoint() string { func (m *SDMock) Setup() { m.Mux = http.NewServeMux() m.Server = httptest.NewServer(m.Mux) + m.t.Cleanup(m.Server.Close) + m.SetupHandlers() } -// ShutdownServer creates the mock server. -func (m *SDMock) ShutdownServer() { - m.Server.Close() -} - -const tokenID = "7b2c56dd51edd90952c1b94c472b94b176f20c5c777e376849edd8ad1c6c03bb" - -// HandleLinodeInstancesList mocks linode instances list. -func (m *SDMock) HandleLinodeInstancesList() { - m.Mux.HandleFunc("/v4/linode/instances", func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", tokenID) { - w.WriteHeader(http.StatusUnauthorized) - return - } - - w.Header().Set("content-type", "application/json; charset=utf-8") - w.WriteHeader(http.StatusOK) - - fmt.Fprint(w, ` -{ - "data": [ - { - "id": 26838044, - "label": "prometheus-linode-sd-exporter-1", - "group": "", - "status": "running", - "created": "2021-05-12T04:23:44", - "updated": "2021-05-12T04:23:44", - "type": "g6-standard-2", - "ipv4": [ - "45.33.82.151", - "96.126.108.16", - "192.168.170.51", - "192.168.201.25" - ], - "ipv6": "2600:3c03::f03c:92ff:fe1a:1382/128", - "image": "linode/arch", - "region": "us-east", - "specs": { - "disk": 81920, - "memory": 4096, - "vcpus": 2, - "gpus": 0, - "transfer": 4000 - }, - "alerts": { - "cpu": 180, - "network_in": 10, - "network_out": 10, - "transfer_quota": 80, - "io": 10000 - }, - "backups": { - "enabled": false, - "schedule": { - "day": null, - "window": null - }, - "last_successful": null - }, - "hypervisor": "kvm", - "watchdog_enabled": true, - "tags": [ - "monitoring" - ] - }, - { - "id": 26848419, - "label": "prometheus-linode-sd-exporter-2", - "group": "", - "status": "running", - "created": "2021-05-12T12:41:49", - "updated": "2021-05-12T12:41:49", - "type": "g6-standard-2", - "ipv4": [ - "139.162.196.43" - ], - "ipv6": "2a01:7e00::f03c:92ff:fe1a:9976/128", - "image": "linode/debian10", - "region": "eu-west", - "specs": { - "disk": 81920, - "memory": 4096, - "vcpus": 2, - "gpus": 0, - "transfer": 4000 - }, - "alerts": { - "cpu": 180, - "network_in": 10, - "network_out": 10, - "transfer_quota": 80, - "io": 10000 - }, - "backups": { - "enabled": false, - "schedule": { - "day": null, - "window": null - }, - "last_successful": null - }, - "hypervisor": "kvm", - "watchdog_enabled": true, - "tags": [ - "monitoring" - ] - }, - { - "id": 26837938, - "label": "prometheus-linode-sd-exporter-3", - "group": "", - "status": "running", - "created": "2021-05-12T04:20:11", - "updated": "2021-05-12T04:20:11", - "type": "g6-standard-1", - "ipv4": [ - "192.53.120.25" - ], - "ipv6": "2600:3c04::f03c:92ff:fe1a:fb68/128", - "image": "linode/ubuntu20.04", - "region": "ca-central", - "specs": { - "disk": 51200, - "memory": 2048, - "vcpus": 1, - "gpus": 0, - "transfer": 2000 - }, - "alerts": { - "cpu": 90, - "network_in": 10, - "network_out": 10, - "transfer_quota": 80, - "io": 10000 - }, - "backups": { - "enabled": false, - "schedule": { - "day": null, - "window": null - }, - "last_successful": null - }, - "hypervisor": "kvm", - "watchdog_enabled": true, - "tags": [ - "monitoring" - ] - }, - { - "id": 26837992, - "label": "prometheus-linode-sd-exporter-4", - "group": "", - "status": "running", - "created": "2021-05-12T04:22:06", - "updated": "2021-05-12T04:22:06", - "type": "g6-nanode-1", - "ipv4": [ - "66.228.47.103", - "172.104.18.104", - "192.168.148.94" - ], - "ipv6": "2600:3c03::f03c:92ff:fe1a:fb4c/128", - "image": "linode/ubuntu20.04", - "region": "us-east", - "specs": { - "disk": 25600, - "memory": 1024, - "vcpus": 1, - "gpus": 0, - "transfer": 1000 - }, - "alerts": { - "cpu": 90, - "network_in": 10, - "network_out": 10, - "transfer_quota": 80, - "io": 10000 - }, - "backups": { - "enabled": false, - "schedule": { - "day": null, - "window": null - }, - "last_successful": null - }, - "hypervisor": "kvm", - "watchdog_enabled": true, - "tags": [ - "monitoring" - ] - } - ], - "page": 1, - "pages": 1, - "results": 4 -}`, - ) - }) -} - -// HandleLinodeNeworkingIPs mocks linode networking ips endpoint. -func (m *SDMock) HandleLinodeNeworkingIPs() { - m.Mux.HandleFunc("/v4/networking/ips", func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", tokenID) { - w.WriteHeader(http.StatusUnauthorized) - return - } - - w.Header().Set("content-type", "application/json; charset=utf-8") - w.WriteHeader(http.StatusOK) - - fmt.Fprint(w, ` -{ - "page": 1, - "pages": 1, - "results": 13, - "data": [ - { - "address": "192.53.120.25", - "gateway": "192.53.120.1", - "subnet_mask": "255.255.255.0", - "prefix": 24, - "type": "ipv4", - "public": true, - "rdns": "li2216-25.members.linode.com", - "linode_id": 26837938, - "region": "ca-central" - }, - { - "address": "66.228.47.103", - "gateway": "66.228.47.1", - "subnet_mask": "255.255.255.0", - "prefix": 24, - "type": "ipv4", - "public": true, - "rdns": "li328-103.members.linode.com", - "linode_id": 26837992, - "region": "us-east" - }, - { - "address": "172.104.18.104", - "gateway": "172.104.18.1", - "subnet_mask": "255.255.255.0", - "prefix": 24, - "type": "ipv4", - "public": true, - "rdns": "li1832-104.members.linode.com", - "linode_id": 26837992, - "region": "us-east" - }, - { - "address": "192.168.148.94", - "gateway": null, - "subnet_mask": "255.255.128.0", - "prefix": 17, - "type": "ipv4", - "public": false, - "rdns": null, - "linode_id": 26837992, - "region": "us-east" - }, - { - "address": "192.168.170.51", - "gateway": null, - "subnet_mask": "255.255.128.0", - "prefix": 17, - "type": "ipv4", - "public": false, - "rdns": null, - "linode_id": 26838044, - "region": "us-east" - }, - { - "address": "96.126.108.16", - "gateway": "96.126.108.1", - "subnet_mask": "255.255.255.0", - "prefix": 24, - "type": "ipv4", - "public": true, - "rdns": "li365-16.members.linode.com", - "linode_id": 26838044, - "region": "us-east" - }, - { - "address": "45.33.82.151", - "gateway": "45.33.82.1", - "subnet_mask": "255.255.255.0", - "prefix": 24, - "type": "ipv4", - "public": true, - "rdns": "li1028-151.members.linode.com", - "linode_id": 26838044, - "region": "us-east" - }, - { - "address": "192.168.201.25", - "gateway": null, - "subnet_mask": "255.255.128.0", - "prefix": 17, - "type": "ipv4", - "public": false, - "rdns": null, - "linode_id": 26838044, - "region": "us-east" - }, - { - "address": "139.162.196.43", - "gateway": "139.162.196.1", - "subnet_mask": "255.255.255.0", - "prefix": 24, - "type": "ipv4", - "public": true, - "rdns": "li1359-43.members.linode.com", - "linode_id": 26848419, - "region": "eu-west" - }, - { - "address": "2600:3c04::f03c:92ff:fe1a:fb68", - "gateway": "fe80::1", - "subnet_mask": "ffff:ffff:ffff:ffff::", - "prefix": 64, - "type": "ipv6", - "rdns": null, - "linode_id": 26837938, - "region": "ca-central", - "public": true - }, - { - "address": "2600:3c03::f03c:92ff:fe1a:fb4c", - "gateway": "fe80::1", - "subnet_mask": "ffff:ffff:ffff:ffff::", - "prefix": 64, - "type": "ipv6", - "rdns": null, - "linode_id": 26837992, - "region": "us-east", - "public": true - }, - { - "address": "2600:3c03::f03c:92ff:fe1a:1382", - "gateway": "fe80::1", - "subnet_mask": "ffff:ffff:ffff:ffff::", - "prefix": 64, - "type": "ipv6", - "rdns": null, - "linode_id": 26838044, - "region": "us-east", - "public": true - }, - { - "address": "2a01:7e00::f03c:92ff:fe1a:9976", - "gateway": "fe80::1", - "subnet_mask": "ffff:ffff:ffff:ffff::", - "prefix": 64, - "type": "ipv6", - "rdns": null, - "linode_id": 26848419, - "region": "eu-west", - "public": true - } - ] -}`, - ) - }) -} - -// HandleLinodeAccountEvents mocks linode the account/events endpoint. -func (m *SDMock) HandleLinodeAccountEvents() { - m.Mux.HandleFunc("/v4/account/events", func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", tokenID) { - w.WriteHeader(http.StatusUnauthorized) - return - } - - if r.Header.Get("X-Filter") == "" { - // This should never happen; if the client sends an events request without - // a filter, cause it to fail. The error below is not a real response from - // the API, but should aid in debugging failed tests. - w.WriteHeader(http.StatusBadRequest) - fmt.Fprint(w, ` -{ - "errors": [ - { - "reason": "Request missing expected X-Filter headers" - } - ] -}`, - ) - return - } - - w.Header().Set("content-type", "application/json; charset=utf-8") - w.WriteHeader(http.StatusOK) - - fmt.Fprint(w, ` -{ - "data": [], - "results": 0, - "pages": 1, - "page": 1 -}`, - ) - }) +// SetupHandlers for endpoints of interest. +func (m *SDMock) SetupHandlers() { + for _, handler := range []string{"/v4/account/events", "/v4/linode/instances", "/v4/networking/ips", "/v4/networking/ipv6/ranges"} { + m.Mux.HandleFunc(handler, func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", tokenID) { + w.WriteHeader(http.StatusUnauthorized) + return + } + xFilter := struct { + Region string `json:"region"` + }{} + json.Unmarshal([]byte(r.Header.Get("X-Filter")), &xFilter) + + directory := "testdata/no_region_filter" + if xFilter.Region != "" { // Validate region filter matches test criteria. + directory = "testdata/" + xFilter.Region + } + if response, err := os.ReadFile(filepath.Join(directory, r.URL.Path+".json")); err == nil { + w.Header().Add("content-type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write(response) + return + } + w.WriteHeader(http.StatusInternalServerError) + }) + } } diff --git a/discovery/linode/testdata/ca-central/v4/account/events.json b/discovery/linode/testdata/ca-central/v4/account/events.json new file mode 100644 index 0000000000..ca302e4fd0 --- /dev/null +++ b/discovery/linode/testdata/ca-central/v4/account/events.json @@ -0,0 +1,6 @@ +{ + "data": [], + "results": 0, + "pages": 1, + "page": 1 +} diff --git a/discovery/linode/testdata/ca-central/v4/linode/instances.json b/discovery/linode/testdata/ca-central/v4/linode/instances.json new file mode 100644 index 0000000000..dfc1172477 --- /dev/null +++ b/discovery/linode/testdata/ca-central/v4/linode/instances.json @@ -0,0 +1,49 @@ +{ + "data": [ + { + "id": 26837938, + "label": "prometheus-linode-sd-exporter-3", + "group": "", + "status": "running", + "created": "2021-05-12T04:20:11", + "updated": "2021-05-12T04:20:11", + "type": "g6-standard-1", + "ipv4": [ + "192.53.120.25" + ], + "ipv6": "2600:3c04::f03c:92ff:fe1a:fb68/128", + "image": "linode/ubuntu20.04", + "region": "ca-central", + "specs": { + "disk": 51200, + "memory": 2048, + "vcpus": 1, + "gpus": 0, + "transfer": 2000 + }, + "alerts": { + "cpu": 90, + "network_in": 10, + "network_out": 10, + "transfer_quota": 80, + "io": 10000 + }, + "backups": { + "enabled": false, + "schedule": { + "day": null, + "window": null + }, + "last_successful": null + }, + "hypervisor": "kvm", + "watchdog_enabled": true, + "tags": [ + "monitoring" + ] + } + ], + "page": 1, + "pages": 1, + "results": 1 +} diff --git a/discovery/linode/testdata/ca-central/v4/networking/ips.json b/discovery/linode/testdata/ca-central/v4/networking/ips.json new file mode 100644 index 0000000000..23d974a886 --- /dev/null +++ b/discovery/linode/testdata/ca-central/v4/networking/ips.json @@ -0,0 +1,29 @@ +{ + "page": 1, + "pages": 1, + "results": 2, + "data": [ + { + "address": "192.53.120.25", + "gateway": "192.53.120.1", + "subnet_mask": "255.255.255.0", + "prefix": 24, + "type": "ipv4", + "public": true, + "rdns": "li2216-25.members.linode.com", + "linode_id": 26837938, + "region": "ca-central" + }, + { + "address": "2600:3c04::f03c:92ff:fe1a:fb68", + "gateway": "fe80::1", + "subnet_mask": "ffff:ffff:ffff:ffff::", + "prefix": 64, + "type": "ipv6", + "rdns": null, + "linode_id": 26837938, + "region": "ca-central", + "public": true + } + ] +} diff --git a/discovery/linode/testdata/ca-central/v4/networking/ipv6/ranges.json b/discovery/linode/testdata/ca-central/v4/networking/ipv6/ranges.json new file mode 100644 index 0000000000..442615cbbc --- /dev/null +++ b/discovery/linode/testdata/ca-central/v4/networking/ipv6/ranges.json @@ -0,0 +1,13 @@ +{ + "data": [ + { + "range": "2600:3c04:e001:456::", + "prefix": 64, + "region": "ca-central", + "route_target": "2600:3c04::f03c:92ff:fe1a:fb68" + } + ], + "page": 1, + "pages": 1, + "results": 1 +} diff --git a/discovery/linode/testdata/no_region_filter/v4/account/events.json b/discovery/linode/testdata/no_region_filter/v4/account/events.json new file mode 100644 index 0000000000..ca302e4fd0 --- /dev/null +++ b/discovery/linode/testdata/no_region_filter/v4/account/events.json @@ -0,0 +1,6 @@ +{ + "data": [], + "results": 0, + "pages": 1, + "page": 1 +} diff --git a/discovery/linode/testdata/no_region_filter/v4/linode/instances.json b/discovery/linode/testdata/no_region_filter/v4/linode/instances.json new file mode 100644 index 0000000000..25d5271d9c --- /dev/null +++ b/discovery/linode/testdata/no_region_filter/v4/linode/instances.json @@ -0,0 +1,180 @@ +{ + "data": [ + { + "id": 26838044, + "label": "prometheus-linode-sd-exporter-1", + "group": "", + "status": "running", + "created": "2021-05-12T04:23:44", + "updated": "2021-05-12T04:23:44", + "type": "g6-standard-2", + "ipv4": [ + "45.33.82.151", + "96.126.108.16", + "192.168.170.51", + "192.168.201.25" + ], + "ipv6": "2600:3c03::f03c:92ff:fe1a:1382/128", + "image": "linode/arch", + "region": "us-east", + "specs": { + "disk": 81920, + "memory": 4096, + "vcpus": 2, + "gpus": 0, + "transfer": 4000 + }, + "alerts": { + "cpu": 180, + "network_in": 10, + "network_out": 10, + "transfer_quota": 80, + "io": 10000 + }, + "backups": { + "enabled": false, + "schedule": { + "day": null, + "window": null + }, + "last_successful": null + }, + "hypervisor": "kvm", + "watchdog_enabled": true, + "tags": [ + "monitoring" + ] + }, + { + "id": 26848419, + "label": "prometheus-linode-sd-exporter-2", + "group": "", + "status": "running", + "created": "2021-05-12T12:41:49", + "updated": "2021-05-12T12:41:49", + "type": "g6-standard-2", + "ipv4": [ + "139.162.196.43" + ], + "ipv6": "2a01:7e00::f03c:92ff:fe1a:9976/128", + "image": "linode/debian10", + "region": "eu-west", + "specs": { + "disk": 81920, + "memory": 4096, + "vcpus": 2, + "gpus": 0, + "transfer": 4000 + }, + "alerts": { + "cpu": 180, + "network_in": 10, + "network_out": 10, + "transfer_quota": 80, + "io": 10000 + }, + "backups": { + "enabled": false, + "schedule": { + "day": null, + "window": null + }, + "last_successful": null + }, + "hypervisor": "kvm", + "watchdog_enabled": true, + "tags": [ + "monitoring" + ] + }, + { + "id": 26837938, + "label": "prometheus-linode-sd-exporter-3", + "group": "", + "status": "running", + "created": "2021-05-12T04:20:11", + "updated": "2021-05-12T04:20:11", + "type": "g6-standard-1", + "ipv4": [ + "192.53.120.25" + ], + "ipv6": "2600:3c04::f03c:92ff:fe1a:fb68/128", + "image": "linode/ubuntu20.04", + "region": "ca-central", + "specs": { + "disk": 51200, + "memory": 2048, + "vcpus": 1, + "gpus": 0, + "transfer": 2000 + }, + "alerts": { + "cpu": 90, + "network_in": 10, + "network_out": 10, + "transfer_quota": 80, + "io": 10000 + }, + "backups": { + "enabled": false, + "schedule": { + "day": null, + "window": null + }, + "last_successful": null + }, + "hypervisor": "kvm", + "watchdog_enabled": true, + "tags": [ + "monitoring" + ] + }, + { + "id": 26837992, + "label": "prometheus-linode-sd-exporter-4", + "group": "", + "status": "running", + "created": "2021-05-12T04:22:06", + "updated": "2021-05-12T04:22:06", + "type": "g6-nanode-1", + "ipv4": [ + "66.228.47.103", + "172.104.18.104", + "192.168.148.94" + ], + "ipv6": "2600:3c03::f03c:92ff:fe1a:fb4c/128", + "image": "linode/ubuntu20.04", + "region": "us-east", + "specs": { + "disk": 25600, + "memory": 1024, + "vcpus": 1, + "gpus": 0, + "transfer": 1000 + }, + "alerts": { + "cpu": 90, + "network_in": 10, + "network_out": 10, + "transfer_quota": 80, + "io": 10000 + }, + "backups": { + "enabled": false, + "schedule": { + "day": null, + "window": null + }, + "last_successful": null + }, + "hypervisor": "kvm", + "watchdog_enabled": true, + "tags": [ + "monitoring" + ] + } + ], + "page": 1, + "pages": 1, + "results": 4 +} diff --git a/discovery/linode/testdata/no_region_filter/v4/networking/ips.json b/discovery/linode/testdata/no_region_filter/v4/networking/ips.json new file mode 100644 index 0000000000..5173036f1c --- /dev/null +++ b/discovery/linode/testdata/no_region_filter/v4/networking/ips.json @@ -0,0 +1,150 @@ +{ + "page": 1, + "pages": 1, + "results": 13, + "data": [ + { + "address": "192.53.120.25", + "gateway": "192.53.120.1", + "subnet_mask": "255.255.255.0", + "prefix": 24, + "type": "ipv4", + "public": true, + "rdns": "li2216-25.members.linode.com", + "linode_id": 26837938, + "region": "ca-central" + }, + { + "address": "66.228.47.103", + "gateway": "66.228.47.1", + "subnet_mask": "255.255.255.0", + "prefix": 24, + "type": "ipv4", + "public": true, + "rdns": "li328-103.members.linode.com", + "linode_id": 26837992, + "region": "us-east" + }, + { + "address": "172.104.18.104", + "gateway": "172.104.18.1", + "subnet_mask": "255.255.255.0", + "prefix": 24, + "type": "ipv4", + "public": true, + "rdns": "li1832-104.members.linode.com", + "linode_id": 26837992, + "region": "us-east" + }, + { + "address": "192.168.148.94", + "gateway": null, + "subnet_mask": "255.255.128.0", + "prefix": 17, + "type": "ipv4", + "public": false, + "rdns": null, + "linode_id": 26837992, + "region": "us-east" + }, + { + "address": "192.168.170.51", + "gateway": null, + "subnet_mask": "255.255.128.0", + "prefix": 17, + "type": "ipv4", + "public": false, + "rdns": null, + "linode_id": 26838044, + "region": "us-east" + }, + { + "address": "96.126.108.16", + "gateway": "96.126.108.1", + "subnet_mask": "255.255.255.0", + "prefix": 24, + "type": "ipv4", + "public": true, + "rdns": "li365-16.members.linode.com", + "linode_id": 26838044, + "region": "us-east" + }, + { + "address": "45.33.82.151", + "gateway": "45.33.82.1", + "subnet_mask": "255.255.255.0", + "prefix": 24, + "type": "ipv4", + "public": true, + "rdns": "li1028-151.members.linode.com", + "linode_id": 26838044, + "region": "us-east" + }, + { + "address": "192.168.201.25", + "gateway": null, + "subnet_mask": "255.255.128.0", + "prefix": 17, + "type": "ipv4", + "public": false, + "rdns": null, + "linode_id": 26838044, + "region": "us-east" + }, + { + "address": "139.162.196.43", + "gateway": "139.162.196.1", + "subnet_mask": "255.255.255.0", + "prefix": 24, + "type": "ipv4", + "public": true, + "rdns": "li1359-43.members.linode.com", + "linode_id": 26848419, + "region": "eu-west" + }, + { + "address": "2600:3c04::f03c:92ff:fe1a:fb68", + "gateway": "fe80::1", + "subnet_mask": "ffff:ffff:ffff:ffff::", + "prefix": 64, + "type": "ipv6", + "rdns": null, + "linode_id": 26837938, + "region": "ca-central", + "public": true + }, + { + "address": "2600:3c03::f03c:92ff:fe1a:fb4c", + "gateway": "fe80::1", + "subnet_mask": "ffff:ffff:ffff:ffff::", + "prefix": 64, + "type": "ipv6", + "rdns": null, + "linode_id": 26837992, + "region": "us-east", + "public": true + }, + { + "address": "2600:3c03::f03c:92ff:fe1a:1382", + "gateway": "fe80::1", + "subnet_mask": "ffff:ffff:ffff:ffff::", + "prefix": 64, + "type": "ipv6", + "rdns": null, + "linode_id": 26838044, + "region": "us-east", + "public": true + }, + { + "address": "2a01:7e00::f03c:92ff:fe1a:9976", + "gateway": "fe80::1", + "subnet_mask": "ffff:ffff:ffff:ffff::", + "prefix": 64, + "type": "ipv6", + "rdns": null, + "linode_id": 26848419, + "region": "eu-west", + "public": true + } + ] +} diff --git a/discovery/linode/testdata/no_region_filter/v4/networking/ipv6/ranges.json b/discovery/linode/testdata/no_region_filter/v4/networking/ipv6/ranges.json new file mode 100644 index 0000000000..511a4d9a8c --- /dev/null +++ b/discovery/linode/testdata/no_region_filter/v4/networking/ipv6/ranges.json @@ -0,0 +1,19 @@ +{ + "data": [ + { + "range": "2600:3c03:e000:123::", + "prefix": 64, + "region": "us-east", + "route_target": "2600:3c03::f03c:92ff:fe1a:fb4c" + }, + { + "range": "2600:3c04:e001:456::", + "prefix": 64, + "region": "ca-central", + "route_target": "2600:3c04::f03c:92ff:fe1a:fb68" + } + ], + "page": 1, + "pages": 1, + "results": 2 +} diff --git a/discovery/linode/testdata/us-east/v4/account/events.json b/discovery/linode/testdata/us-east/v4/account/events.json new file mode 100644 index 0000000000..ca302e4fd0 --- /dev/null +++ b/discovery/linode/testdata/us-east/v4/account/events.json @@ -0,0 +1,6 @@ +{ + "data": [], + "results": 0, + "pages": 1, + "page": 1 +} diff --git a/discovery/linode/testdata/us-east/v4/linode/instances.json b/discovery/linode/testdata/us-east/v4/linode/instances.json new file mode 100644 index 0000000000..5e9a8f5abe --- /dev/null +++ b/discovery/linode/testdata/us-east/v4/linode/instances.json @@ -0,0 +1,97 @@ +{ + "data": [ + { + "id": 26838044, + "label": "prometheus-linode-sd-exporter-1", + "group": "", + "status": "running", + "created": "2021-05-12T04:23:44", + "updated": "2021-05-12T04:23:44", + "type": "g6-standard-2", + "ipv4": [ + "45.33.82.151", + "96.126.108.16", + "192.168.170.51", + "192.168.201.25" + ], + "ipv6": "2600:3c03::f03c:92ff:fe1a:1382/128", + "image": "linode/arch", + "region": "us-east", + "specs": { + "disk": 81920, + "memory": 4096, + "vcpus": 2, + "gpus": 0, + "transfer": 4000 + }, + "alerts": { + "cpu": 180, + "network_in": 10, + "network_out": 10, + "transfer_quota": 80, + "io": 10000 + }, + "backups": { + "enabled": false, + "schedule": { + "day": null, + "window": null + }, + "last_successful": null + }, + "hypervisor": "kvm", + "watchdog_enabled": true, + "tags": [ + "monitoring" + ] + }, + { + "id": 26837992, + "label": "prometheus-linode-sd-exporter-4", + "group": "", + "status": "running", + "created": "2021-05-12T04:22:06", + "updated": "2021-05-12T04:22:06", + "type": "g6-nanode-1", + "ipv4": [ + "66.228.47.103", + "172.104.18.104", + "192.168.148.94" + ], + "ipv6": "2600:3c03::f03c:92ff:fe1a:fb4c/128", + "image": "linode/ubuntu20.04", + "region": "us-east", + "specs": { + "disk": 25600, + "memory": 1024, + "vcpus": 1, + "gpus": 0, + "transfer": 1000 + }, + "alerts": { + "cpu": 90, + "network_in": 10, + "network_out": 10, + "transfer_quota": 80, + "io": 10000 + }, + "backups": { + "enabled": false, + "schedule": { + "day": null, + "window": null + }, + "last_successful": null + }, + "hypervisor": "kvm", + "watchdog_enabled": true, + "tags": [ + "monitoring" + ] + } + ], + "page": 1, + "pages": 1, + "results": 2 +} + diff --git a/discovery/linode/testdata/us-east/v4/networking/ips.json b/discovery/linode/testdata/us-east/v4/networking/ips.json new file mode 100644 index 0000000000..388cf59659 --- /dev/null +++ b/discovery/linode/testdata/us-east/v4/networking/ips.json @@ -0,0 +1,106 @@ +{ + "page": 1, + "pages": 1, + "results": 9, + "data": [ + { + "address": "66.228.47.103", + "gateway": "66.228.47.1", + "subnet_mask": "255.255.255.0", + "prefix": 24, + "type": "ipv4", + "public": true, + "rdns": "li328-103.members.linode.com", + "linode_id": 26837992, + "region": "us-east" + }, + { + "address": "172.104.18.104", + "gateway": "172.104.18.1", + "subnet_mask": "255.255.255.0", + "prefix": 24, + "type": "ipv4", + "public": true, + "rdns": "li1832-104.members.linode.com", + "linode_id": 26837992, + "region": "us-east" + }, + { + "address": "192.168.148.94", + "gateway": null, + "subnet_mask": "255.255.128.0", + "prefix": 17, + "type": "ipv4", + "public": false, + "rdns": null, + "linode_id": 26837992, + "region": "us-east" + }, + { + "address": "192.168.170.51", + "gateway": null, + "subnet_mask": "255.255.128.0", + "prefix": 17, + "type": "ipv4", + "public": false, + "rdns": null, + "linode_id": 26838044, + "region": "us-east" + }, + { + "address": "96.126.108.16", + "gateway": "96.126.108.1", + "subnet_mask": "255.255.255.0", + "prefix": 24, + "type": "ipv4", + "public": true, + "rdns": "li365-16.members.linode.com", + "linode_id": 26838044, + "region": "us-east" + }, + { + "address": "45.33.82.151", + "gateway": "45.33.82.1", + "subnet_mask": "255.255.255.0", + "prefix": 24, + "type": "ipv4", + "public": true, + "rdns": "li1028-151.members.linode.com", + "linode_id": 26838044, + "region": "us-east" + }, + { + "address": "192.168.201.25", + "gateway": null, + "subnet_mask": "255.255.128.0", + "prefix": 17, + "type": "ipv4", + "public": false, + "rdns": null, + "linode_id": 26838044, + "region": "us-east" + }, + { + "address": "2600:3c03::f03c:92ff:fe1a:fb4c", + "gateway": "fe80::1", + "subnet_mask": "ffff:ffff:ffff:ffff::", + "prefix": 64, + "type": "ipv6", + "rdns": null, + "linode_id": 26837992, + "region": "us-east", + "public": true + }, + { + "address": "2600:3c03::f03c:92ff:fe1a:1382", + "gateway": "fe80::1", + "subnet_mask": "ffff:ffff:ffff:ffff::", + "prefix": 64, + "type": "ipv6", + "rdns": null, + "linode_id": 26838044, + "region": "us-east", + "public": true + } + ] +} diff --git a/discovery/linode/testdata/us-east/v4/networking/ipv6/ranges.json b/discovery/linode/testdata/us-east/v4/networking/ipv6/ranges.json new file mode 100644 index 0000000000..34b2ae1cdd --- /dev/null +++ b/discovery/linode/testdata/us-east/v4/networking/ipv6/ranges.json @@ -0,0 +1,13 @@ +{ + "data": [ + { + "range": "2600:3c03:e000:123::", + "prefix": 64, + "region": "us-east", + "route_target": "2600:3c03::f03c:92ff:fe1a:fb4c" + } + ], + "page": 1, + "pages": 1, + "results": 1 +} diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index d751a4084e..010a454661 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -2449,11 +2449,15 @@ The following meta labels are available on targets during [relabeling](#relabel_ * `__meta_linode_private_ipv4`: the private IPv4 of the linode instance * `__meta_linode_public_ipv4`: the public IPv4 of the linode instance * `__meta_linode_public_ipv6`: the public IPv6 of the linode instance +* `__meta_linode_private_ipv4_rdns`: the reverse DNS for the first private IPv4 of the linode instance +* `__meta_linode_public_ipv4_rdns`: the reverse DNS for the first public IPv4 of the linode instance +* `__meta_linode_public_ipv6_rdns`: the reverse DNS for the first public IPv6 of the linode instance * `__meta_linode_region`: the region of the linode instance * `__meta_linode_type`: the type of the linode instance * `__meta_linode_status`: the status of the linode instance * `__meta_linode_tags`: a list of tags of the linode instance joined by the tag separator * `__meta_linode_group`: the display group a linode instance is a member of +* `__meta_linode_gpus`: the number of GPU's of the linode instance * `__meta_linode_hypervisor`: the virtualization software powering the linode instance * `__meta_linode_backups`: the backup service status of the linode instance * `__meta_linode_specs_disk_bytes`: the amount of storage space the linode instance has access to @@ -2461,6 +2465,7 @@ The following meta labels are available on targets during [relabeling](#relabel_ * `__meta_linode_specs_vcpus`: the number of VCPUS this linode has access to * `__meta_linode_specs_transfer_bytes`: the amount of network transfer the linode instance is allotted each month * `__meta_linode_extra_ips`: a list of all extra IPv4 addresses assigned to the linode instance joined by the tag separator +* `__meta_linode_ipv6_ranges`: a list of IPv6 ranges with mask assigned to the linode instance joined by the tag separator ```yaml # Authentication information used to authenticate to the API server. @@ -2491,6 +2496,9 @@ authorization: oauth2: [ ] +# Optional region to filter on. +[ region: ] + # Optional proxy URL. [ proxy_url: ] # Comma-separated string that can contain IPs, CIDR notation, domain names diff --git a/documentation/examples/prometheus-linode.yml b/documentation/examples/prometheus-linode.yml index 993b6a5c12..fe1a740028 100644 --- a/documentation/examples/prometheus-linode.yml +++ b/documentation/examples/prometheus-linode.yml @@ -12,6 +12,7 @@ scrape_configs: linode_sd_configs: - authorization: credentials: "" + region: "us-east" relabel_configs: # Only scrape targets that have a tag 'monitoring'. - source_labels: [__meta_linode_tags] From dc7d3fbc3c9f4066ec3ecc5b3cb5e61293ab499c Mon Sep 17 00:00:00 2001 From: Sven Dewit Date: Fri, 5 Apr 2024 11:28:58 +0200 Subject: [PATCH 097/127] fix: scrape_config/interval relabelling is not experimental any more Signed-off-by: Sven Dewit --- docs/configuration/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index 010a454661..51eb84ae19 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -3236,7 +3236,7 @@ are set to the scheme and metrics path of the target respectively. The `__param_ label is set to the value of the first passed URL parameter called ``. The `__scrape_interval__` and `__scrape_timeout__` labels are set to the target's -interval and timeout. This is **experimental** and could change in the future. +interval and timeout. Additional labels prefixed with `__meta_` may be available during the relabeling phase. They are set by the service discovery mechanism that provided From bbfc72b4e2ac06e109c151fa792bd31da8b0bd4b Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Fri, 5 Apr 2024 10:19:07 -0400 Subject: [PATCH 098/127] support unregistering discovery manager metrics (#13896) Signed-off-by: David Ashpole --- discovery/manager.go | 7 +++++++ discovery/manager_test.go | 25 +++++++++++++++++++++++-- discovery/metrics.go | 9 +++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/discovery/manager.go b/discovery/manager.go index e3a2635575..f14071af30 100644 --- a/discovery/manager.go +++ b/discovery/manager.go @@ -169,6 +169,13 @@ func (m *Manager) Providers() []*Provider { return m.providers } +// UnregisterMetrics unregisters manager metrics. It does not unregister +// service discovery or refresh metrics, whose lifecycle is managed independent +// of the discovery Manager. +func (m *Manager) UnregisterMetrics() { + m.metrics.Unregister(m.registerer) +} + // Run starts the background processing. func (m *Manager) Run() error { go m.sender() diff --git a/discovery/manager_test.go b/discovery/manager_test.go index 8f2345911e..656d7c3c66 100644 --- a/discovery/manager_test.go +++ b/discovery/manager_test.go @@ -36,11 +36,11 @@ func TestMain(m *testing.M) { testutil.TolerantVerifyLeak(m) } -func NewTestMetrics(t *testing.T, reg prometheus.Registerer) (*RefreshMetricsManager, map[string]DiscovererMetrics) { +func NewTestMetrics(t *testing.T, reg prometheus.Registerer) (RefreshMetricsManager, map[string]DiscovererMetrics) { refreshMetrics := NewRefreshMetrics(reg) sdMetrics, err := RegisterSDMetrics(reg, refreshMetrics) require.NoError(t, err) - return &refreshMetrics, sdMetrics + return refreshMetrics, sdMetrics } // TestTargetUpdatesOrder checks that the target updates are received in the expected order. @@ -1541,3 +1541,24 @@ func (t *testDiscoverer) update(tgs []*targetgroup.Group) { <-t.ready t.up <- tgs } + +func TestUnregisterMetrics(t *testing.T) { + reg := prometheus.NewRegistry() + // Check that all metrics can be unregistered, allowing a second manager to be created. + for i := 0; i < 2; i++ { + ctx, cancel := context.WithCancel(context.Background()) + + refreshMetrics, sdMetrics := NewTestMetrics(t, reg) + + discoveryManager := NewManager(ctx, log.NewNopLogger(), reg, sdMetrics) + // discoveryManager will be nil if there was an error configuring metrics. + require.NotNil(t, discoveryManager) + // Unregister all metrics. + discoveryManager.UnregisterMetrics() + for _, sdMetric := range sdMetrics { + sdMetric.Unregister() + } + refreshMetrics.Unregister() + cancel() + } +} diff --git a/discovery/metrics.go b/discovery/metrics.go index a77b86d274..e738331a18 100644 --- a/discovery/metrics.go +++ b/discovery/metrics.go @@ -99,3 +99,12 @@ func NewManagerMetrics(registerer prometheus.Registerer, sdManagerName string) ( return m, nil } + +// Unregister unregisters all metrics. +func (m *Metrics) Unregister(registerer prometheus.Registerer) { + registerer.Unregister(m.FailedConfigs) + registerer.Unregister(m.DiscoveredTargets) + registerer.Unregister(m.ReceivedUpdates) + registerer.Unregister(m.DelayedUpdates) + registerer.Unregister(m.SentUpdates) +} From 8e04ab6dd4eb3fc3a2c67f974f07f8ac2b852fa8 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 16 Jan 2024 17:49:43 +0000 Subject: [PATCH 099/127] promql: refactor: extract generateGroupingLabels function Signed-off-by: Bryan Boreham --- promql/engine.go | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 2f7dcb222e..56a7774c68 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -2691,19 +2691,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par group, ok := result[groupingKey] // Add a new group if it doesn't exist. if !ok { - var m labels.Labels - enh.resetBuilder(metric) - switch { - case without: - enh.lb.Del(grouping...) - enh.lb.Del(labels.MetricName) - m = enh.lb.Labels() - case len(grouping) > 0: - enh.lb.Keep(grouping...) - m = enh.lb.Labels() - default: - m = labels.EmptyLabels() - } + m := generateGroupingLabels(enh, metric, without, grouping) newAgg := &groupedAggregation{ labels: m, floatValue: s.F, @@ -2969,6 +2957,21 @@ func generateGroupingKey(metric labels.Labels, grouping []string, without bool, return metric.HashForLabels(buf, grouping...) } +func generateGroupingLabels(enh *EvalNodeHelper, metric labels.Labels, without bool, grouping []string) labels.Labels { + enh.resetBuilder(metric) + switch { + case without: + enh.lb.Del(grouping...) + enh.lb.Del(labels.MetricName) + return enh.lb.Labels() + case len(grouping) > 0: + enh.lb.Keep(grouping...) + return enh.lb.Labels() + default: + return labels.EmptyLabels() + } +} + // btos returns 1 if b is true, 0 otherwise. func btos(b bool) float64 { if b { From 29244fb84173313479e5c0603c793f66d9959e46 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 16 Jan 2024 16:22:34 +0000 Subject: [PATCH 100/127] promql: refactor: extract count_values implementation The existing aggregation function is very long and covers very different cases. `aggregationCountValues` is just for `count_values`, which differs from other aggregations in that it outputs as many series per group as there are values in the input. Remove the top-level switch on string parameter type; use the same `Op` check there as elswehere. Pull checking parameters out to caller, where it is only executed once. Signed-off-by: Bryan Boreham --- promql/engine.go | 99 +++++++++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 56a7774c68..9074912d66 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1352,9 +1352,18 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, annotations.Annotatio unwrapParenExpr(&e.Param) param := unwrapStepInvariantExpr(e.Param) unwrapParenExpr(¶m) - if s, ok := param.(*parser.StringLiteral); ok { - return ev.rangeEval(initSeries, func(v []parser.Value, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, annotations.Annotations) { - return ev.aggregation(e, sortedGrouping, s.Val, v[0].(Vector), sh[0], enh) + + if e.Op == parser.COUNT_VALUES { + valueLabel := param.(*parser.StringLiteral) + if !model.LabelName(valueLabel.Val).IsValid() { + ev.errorf("invalid label name %q", valueLabel) + } + if !e.Without { + sortedGrouping = append(sortedGrouping, valueLabel.Val) + slices.Sort(sortedGrouping) + } + return ev.rangeEval(nil, func(v []parser.Value, _ [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + return ev.aggregationCountValues(e, sortedGrouping, valueLabel.Val, v[0].(Vector), enh) }, e.Expr) } @@ -2649,44 +2658,10 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par if op == parser.QUANTILE { q = param.(float64) } - var valueLabel string - var recomputeGroupingKey bool - if op == parser.COUNT_VALUES { - valueLabel = param.(string) - if !model.LabelName(valueLabel).IsValid() { - ev.errorf("invalid label name %q", valueLabel) - } - if !without { - // We're changing the grouping labels so we have to ensure they're still sorted - // and we have to flag to recompute the grouping key. Considering the count_values() - // operator is less frequently used than other aggregations, we're fine having to - // re-compute the grouping key on each step for this case. - grouping = append(grouping, valueLabel) - slices.Sort(grouping) - recomputeGroupingKey = true - } - } - var buf []byte for si, s := range vec { metric := s.Metric - - if op == parser.COUNT_VALUES { - enh.resetBuilder(metric) - enh.lb.Set(valueLabel, strconv.FormatFloat(s.F, 'f', -1, 64)) - metric = enh.lb.Labels() - - // We've changed the metric so we have to recompute the grouping key. - recomputeGroupingKey = true - } - - // We can use the pre-computed grouping key unless grouping labels have changed. - var groupingKey uint64 - if !recomputeGroupingKey { - groupingKey = seriesHelper[si].groupingKey - } else { - groupingKey, buf = generateGroupingKey(metric, grouping, without, buf) - } + groupingKey := seriesHelper[si].groupingKey group, ok := result[groupingKey] // Add a new group if it doesn't exist. @@ -2807,7 +2782,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par group.floatValue = s.F } - case parser.COUNT, parser.COUNT_VALUES: + case parser.COUNT: group.groupCount++ case parser.STDVAR, parser.STDDEV: @@ -2879,7 +2854,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par aggr.floatValue = aggr.floatMean } - case parser.COUNT, parser.COUNT_VALUES: + case parser.COUNT: aggr.floatValue = float64(aggr.groupCount) case parser.STDVAR: @@ -2942,6 +2917,50 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par return enh.Out, annos } +// aggregationK evaluates count_values on vec. +// Outputs as many series per group as there are values in the input. +func (ev *evaluator) aggregationCountValues(e *parser.AggregateExpr, grouping []string, valueLabel string, vec Vector, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + result := map[uint64]*groupedAggregation{} + orderedResult := []*groupedAggregation{} + + var buf []byte + for _, s := range vec { + enh.resetBuilder(s.Metric) + enh.lb.Set(valueLabel, strconv.FormatFloat(s.F, 'f', -1, 64)) + metric := enh.lb.Labels() + + // Considering the count_values() + // operator is less frequently used than other aggregations, we're fine having to + // re-compute the grouping key on each step for this case. + var groupingKey uint64 + groupingKey, buf = generateGroupingKey(metric, grouping, e.Without, buf) + + group, ok := result[groupingKey] + // Add a new group if it doesn't exist. + if !ok { + newAgg := &groupedAggregation{ + labels: generateGroupingLabels(enh, metric, e.Without, grouping), + groupCount: 1, + } + + result[groupingKey] = newAgg + orderedResult = append(orderedResult, newAgg) + continue + } + + group.groupCount++ + } + + // Construct the result Vector from the aggregated groups. + for _, aggr := range orderedResult { + enh.Out = append(enh.Out, Sample{ + Metric: aggr.labels, + F: float64(aggr.groupCount), + }) + } + return enh.Out, nil +} + // groupingKey builds and returns the grouping key for the given metric and // grouping labels. func generateGroupingKey(metric labels.Labels, grouping []string, without bool, buf []byte) (uint64, []byte) { From e5f667537c94dbd1e5ad70d766c0c27ea5cb8830 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Wed, 17 Jan 2024 10:57:02 +0000 Subject: [PATCH 101/127] promql: refactor: initialize aggregation before storing in map This seems more consistent to me. Signed-off-by: Bryan Boreham --- promql/engine.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 9074912d66..0a2c5ff3fc 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -2686,9 +2686,6 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par newAgg.groupCount = 0 } - result[groupingKey] = newAgg - orderedResult = append(orderedResult, newAgg) - inputVecLen := int64(len(vec)) resultSize := k switch { @@ -2699,22 +2696,25 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par } switch op { case parser.STDVAR, parser.STDDEV: - result[groupingKey].floatValue = 0 + newAgg.floatValue = 0 case parser.TOPK, parser.QUANTILE: - result[groupingKey].heap = make(vectorByValueHeap, 1, resultSize) - result[groupingKey].heap[0] = Sample{ + newAgg.heap = make(vectorByValueHeap, 1, resultSize) + newAgg.heap[0] = Sample{ F: s.F, Metric: s.Metric, } case parser.BOTTOMK: - result[groupingKey].reverseHeap = make(vectorByReverseValueHeap, 1, resultSize) - result[groupingKey].reverseHeap[0] = Sample{ + newAgg.reverseHeap = make(vectorByReverseValueHeap, 1, resultSize) + newAgg.reverseHeap[0] = Sample{ F: s.F, Metric: s.Metric, } case parser.GROUP: - result[groupingKey].floatValue = 1 + newAgg.floatValue = 1 } + + result[groupingKey] = newAgg + orderedResult = append(orderedResult, newAgg) continue } From 5f10d17cef1a042ea7c9cd99026a502f7ae7c80e Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 27 Feb 2024 06:38:49 +0000 Subject: [PATCH 102/127] promql: refactor: split out aggregations over range The new function `rangeEvalAgg` is mostly a copy of `rangeEval`, but without `initSeries` which we don't need and inlining the callback to `aggregation()`. Signed-off-by: Bryan Boreham --- promql/engine.go | 180 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 167 insertions(+), 13 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 0a2c5ff3fc..91c5e9e249 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1291,6 +1291,172 @@ func (ev *evaluator) rangeEval(prepSeries func(labels.Labels, *EvalSeriesHelper) return mat, warnings } +func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping []string) (Matrix, annotations.Annotations) { + numSteps := int((ev.endTimestamp-ev.startTimestamp)/ev.interval) + 1 + matrixes := make([]Matrix, 2) + origMatrixes := make([]Matrix, 2) + originalNumSamples := ev.currentSamples + + var warnings annotations.Annotations + for i, e := range []parser.Expr{aggExpr.Param, aggExpr.Expr} { + // Functions will take string arguments from the expressions, not the values. + if e != nil && e.Type() != parser.ValueTypeString { + // ev.currentSamples will be updated to the correct value within the ev.eval call. + val, ws := ev.eval(e) + warnings.Merge(ws) + matrixes[i] = val.(Matrix) + + // Keep a copy of the original point slices so that they + // can be returned to the pool. + origMatrixes[i] = make(Matrix, len(matrixes[i])) + copy(origMatrixes[i], matrixes[i]) + } + } + + vectors := make([]Vector, 2) // Input vectors for the function. + args := make([]parser.Value, 2) // Argument to function. + biggestLen := len(matrixes[1]) + enh := &EvalNodeHelper{Out: make(Vector, 0, biggestLen)} + type seriesAndTimestamp struct { + Series + ts int64 + } + seriess := make(map[uint64]seriesAndTimestamp, biggestLen) // Output series by series hash. + tempNumSamples := ev.currentSamples + + seriesHelpers := make([][]EvalSeriesHelper, 2) + bufHelpers := make([][]EvalSeriesHelper, 2) + // Prepare a function to initialise series helpers with the grouping key. + buf := make([]byte, 0, 1024) + + seriesHelpers[1] = make([]EvalSeriesHelper, len(matrixes[1])) + bufHelpers[1] = make([]EvalSeriesHelper, len(matrixes[1])) + + for si, series := range matrixes[1] { + seriesHelpers[1][si].groupingKey, buf = generateGroupingKey(series.Metric, sortedGrouping, aggExpr.Without, buf) + } + + for ts := ev.startTimestamp; ts <= ev.endTimestamp; ts += ev.interval { + if err := contextDone(ev.ctx, "expression evaluation"); err != nil { + ev.error(err) + } + // Reset number of samples in memory after each timestamp. + ev.currentSamples = tempNumSamples + // Gather input vectors for this timestamp. + for i := range []parser.Expr{aggExpr.Param, aggExpr.Expr} { + vectors[i] = vectors[i][:0] + bufHelpers[i] = bufHelpers[i][:0] + + for si, series := range matrixes[i] { + switch { + case len(series.Floats) > 0 && series.Floats[0].T == ts: + vectors[i] = append(vectors[i], Sample{Metric: series.Metric, F: series.Floats[0].F, T: ts}) + // Move input vectors forward so we don't have to re-scan the same + // past points at the next step. + matrixes[i][si].Floats = series.Floats[1:] + case len(series.Histograms) > 0 && series.Histograms[0].T == ts: + vectors[i] = append(vectors[i], Sample{Metric: series.Metric, H: series.Histograms[0].H, T: ts}) + matrixes[i][si].Histograms = series.Histograms[1:] + default: + continue + } + if seriesHelpers[i] != nil { + bufHelpers[i] = append(bufHelpers[i], seriesHelpers[i][si]) + } + ev.currentSamples++ + if ev.currentSamples > ev.maxSamples { + ev.error(ErrTooManySamples(env)) + } + } + args[i] = vectors[i] + ev.samplesStats.UpdatePeak(ev.currentSamples) + } + + // Make the function call. + enh.Ts = ts + var param float64 + if aggExpr.Param != nil { + param = args[0].(Vector)[0].F + } + result, ws := ev.aggregation(aggExpr, sortedGrouping, param, args[1].(Vector), bufHelpers[1], enh) + + enh.Out = result[:0] // Reuse result vector. + warnings.Merge(ws) + + vecNumSamples := result.TotalSamples() + ev.currentSamples += vecNumSamples + // When we reset currentSamples to tempNumSamples during the next iteration of the loop it also + // needs to include the samples from the result here, as they're still in memory. + tempNumSamples += vecNumSamples + ev.samplesStats.UpdatePeak(ev.currentSamples) + + if ev.currentSamples > ev.maxSamples { + ev.error(ErrTooManySamples(env)) + } + ev.samplesStats.UpdatePeak(ev.currentSamples) + + // If this could be an instant query, shortcut so as not to change sort order. + if ev.endTimestamp == ev.startTimestamp { + if result.ContainsSameLabelset() { + ev.errorf("vector cannot contain metrics with the same labelset") + } + mat := make(Matrix, len(result)) + for i, s := range result { + if s.H == nil { + mat[i] = Series{Metric: s.Metric, Floats: []FPoint{{T: ts, F: s.F}}} + } else { + mat[i] = Series{Metric: s.Metric, Histograms: []HPoint{{T: ts, H: s.H}}} + } + } + ev.currentSamples = originalNumSamples + mat.TotalSamples() + ev.samplesStats.UpdatePeak(ev.currentSamples) + return mat, warnings + } + + // Add samples in output vector to output series. + for _, sample := range result { + h := sample.Metric.Hash() + ss, ok := seriess[h] + if ok { + if ss.ts == ts { // If we've seen this output series before at this timestamp, it's a duplicate. + ev.errorf("vector cannot contain metrics with the same labelset") + } + ss.ts = ts + } else { + ss = seriesAndTimestamp{Series{Metric: sample.Metric}, ts} + } + if sample.H == nil { + if ss.Floats == nil { + ss.Floats = getFPointSlice(numSteps) + } + ss.Floats = append(ss.Floats, FPoint{T: ts, F: sample.F}) + } else { + if ss.Histograms == nil { + ss.Histograms = getHPointSlice(numSteps) + } + ss.Histograms = append(ss.Histograms, HPoint{T: ts, H: sample.H}) + } + seriess[h] = ss + } + } + + // Reuse the original point slices. + for _, m := range origMatrixes { + for _, s := range m { + putFPointSlice(s.Floats) + putHPointSlice(s.Histograms) + } + } + // Assemble the output matrix. By the time we get here we know we don't have too many samples. + mat := make(Matrix, 0, len(seriess)) + for _, ss := range seriess { + mat = append(mat, ss.Series) + } + ev.currentSamples = originalNumSamples + mat.TotalSamples() + ev.samplesStats.UpdatePeak(ev.currentSamples) + return mat, warnings +} + // evalSubquery evaluates given SubqueryExpr and returns an equivalent // evaluated MatrixSelector in its place. Note that the Name and LabelMatchers are not set. func (ev *evaluator) evalSubquery(subq *parser.SubqueryExpr) (*parser.MatrixSelector, int, annotations.Annotations) { @@ -1343,12 +1509,6 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, annotations.Annotatio sortedGrouping := e.Grouping slices.Sort(sortedGrouping) - // Prepare a function to initialise series helpers with the grouping key. - buf := make([]byte, 0, 1024) - initSeries := func(series labels.Labels, h *EvalSeriesHelper) { - h.groupingKey, buf = generateGroupingKey(series, sortedGrouping, e.Without, buf) - } - unwrapParenExpr(&e.Param) param := unwrapStepInvariantExpr(e.Param) unwrapParenExpr(¶m) @@ -1367,13 +1527,7 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, annotations.Annotatio }, e.Expr) } - return ev.rangeEval(initSeries, func(v []parser.Value, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, annotations.Annotations) { - var param float64 - if e.Param != nil { - param = v[0].(Vector)[0].F - } - return ev.aggregation(e, sortedGrouping, param, v[1].(Vector), sh[1], enh) - }, e.Param, e.Expr) + return ev.rangeEvalAgg(e, sortedGrouping) case *parser.Call: call := FunctionCalls[e.Func.Name] From bd9bdccb22bb0ba99dd2da29de6d1d96d47fa0d0 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 27 Feb 2024 07:00:35 +0000 Subject: [PATCH 103/127] promql: refactor: simplify internal data structures Signed-off-by: Bryan Boreham --- promql/engine.go | 83 ++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 48 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 91c5e9e249..821a0e3698 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1293,29 +1293,27 @@ func (ev *evaluator) rangeEval(prepSeries func(labels.Labels, *EvalSeriesHelper) func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping []string) (Matrix, annotations.Annotations) { numSteps := int((ev.endTimestamp-ev.startTimestamp)/ev.interval) + 1 - matrixes := make([]Matrix, 2) - origMatrixes := make([]Matrix, 2) originalNumSamples := ev.currentSamples - var warnings annotations.Annotations - for i, e := range []parser.Expr{aggExpr.Param, aggExpr.Expr} { - // Functions will take string arguments from the expressions, not the values. - if e != nil && e.Type() != parser.ValueTypeString { - // ev.currentSamples will be updated to the correct value within the ev.eval call. - val, ws := ev.eval(e) - warnings.Merge(ws) - matrixes[i] = val.(Matrix) - // Keep a copy of the original point slices so that they - // can be returned to the pool. - origMatrixes[i] = make(Matrix, len(matrixes[i])) - copy(origMatrixes[i], matrixes[i]) - } + // param is the number k for topk/bottomk. + var param float64 + if aggExpr.Param != nil { + val, ws := ev.eval(aggExpr.Param) + warnings.Merge(ws) + param = val.(Matrix)[0].Floats[0].F } + // Now fetch the data to be aggregated. + // ev.currentSamples will be updated to the correct value within the ev.eval call. + val, ws := ev.eval(aggExpr.Expr) + warnings.Merge(ws) + inputMatrix := val.(Matrix) - vectors := make([]Vector, 2) // Input vectors for the function. - args := make([]parser.Value, 2) // Argument to function. - biggestLen := len(matrixes[1]) + // Keep a copy of the original point slice so that it can be returned to the pool. + origMatrix := inputMatrix + + var vector Vector // Input vectors for the function. + biggestLen := len(inputMatrix) enh := &EvalNodeHelper{Out: make(Vector, 0, biggestLen)} type seriesAndTimestamp struct { Series @@ -1324,16 +1322,14 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping seriess := make(map[uint64]seriesAndTimestamp, biggestLen) // Output series by series hash. tempNumSamples := ev.currentSamples - seriesHelpers := make([][]EvalSeriesHelper, 2) - bufHelpers := make([][]EvalSeriesHelper, 2) - // Prepare a function to initialise series helpers with the grouping key. + // Initialise series helpers with the grouping key. buf := make([]byte, 0, 1024) - seriesHelpers[1] = make([]EvalSeriesHelper, len(matrixes[1])) - bufHelpers[1] = make([]EvalSeriesHelper, len(matrixes[1])) + seriesHelper := make([]EvalSeriesHelper, len(inputMatrix)) + bufHelper := make([]EvalSeriesHelper, len(inputMatrix)) - for si, series := range matrixes[1] { - seriesHelpers[1][si].groupingKey, buf = generateGroupingKey(series.Metric, sortedGrouping, aggExpr.Without, buf) + for si, series := range inputMatrix { + seriesHelper[si].groupingKey, buf = generateGroupingKey(series.Metric, sortedGrouping, aggExpr.Without, buf) } for ts := ev.startTimestamp; ts <= ev.endTimestamp; ts += ev.interval { @@ -1343,42 +1339,35 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping // Reset number of samples in memory after each timestamp. ev.currentSamples = tempNumSamples // Gather input vectors for this timestamp. - for i := range []parser.Expr{aggExpr.Param, aggExpr.Expr} { - vectors[i] = vectors[i][:0] - bufHelpers[i] = bufHelpers[i][:0] + { + vector = vector[:0] + bufHelper = bufHelper[:0] - for si, series := range matrixes[i] { + for si, series := range inputMatrix { switch { case len(series.Floats) > 0 && series.Floats[0].T == ts: - vectors[i] = append(vectors[i], Sample{Metric: series.Metric, F: series.Floats[0].F, T: ts}) + vector = append(vector, Sample{Metric: series.Metric, F: series.Floats[0].F, T: ts}) // Move input vectors forward so we don't have to re-scan the same // past points at the next step. - matrixes[i][si].Floats = series.Floats[1:] + inputMatrix[si].Floats = series.Floats[1:] case len(series.Histograms) > 0 && series.Histograms[0].T == ts: - vectors[i] = append(vectors[i], Sample{Metric: series.Metric, H: series.Histograms[0].H, T: ts}) - matrixes[i][si].Histograms = series.Histograms[1:] + vector = append(vector, Sample{Metric: series.Metric, H: series.Histograms[0].H, T: ts}) + inputMatrix[si].Histograms = series.Histograms[1:] default: continue } - if seriesHelpers[i] != nil { - bufHelpers[i] = append(bufHelpers[i], seriesHelpers[i][si]) - } + bufHelper = append(bufHelper, seriesHelper[si]) ev.currentSamples++ if ev.currentSamples > ev.maxSamples { ev.error(ErrTooManySamples(env)) } } - args[i] = vectors[i] ev.samplesStats.UpdatePeak(ev.currentSamples) } // Make the function call. enh.Ts = ts - var param float64 - if aggExpr.Param != nil { - param = args[0].(Vector)[0].F - } - result, ws := ev.aggregation(aggExpr, sortedGrouping, param, args[1].(Vector), bufHelpers[1], enh) + result, ws := ev.aggregation(aggExpr, sortedGrouping, param, vector, bufHelper, enh) enh.Out = result[:0] // Reuse result vector. warnings.Merge(ws) @@ -1440,12 +1429,10 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping } } - // Reuse the original point slices. - for _, m := range origMatrixes { - for _, s := range m { - putFPointSlice(s.Floats) - putHPointSlice(s.Histograms) - } + // Reuse the original point slice. + for _, s := range origMatrix { + putFPointSlice(s.Floats) + putHPointSlice(s.Histograms) } // Assemble the output matrix. By the time we get here we know we don't have too many samples. mat := make(Matrix, 0, len(seriess)) From 59548b8a0b8f7f4e35d9b1fcd43c26f6d7cd10f5 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 27 Feb 2024 08:19:01 +0000 Subject: [PATCH 104/127] promql: refactor: move collection of results into aggregation() We don't need to check for duplicates as aggregation cannot generate them. Signed-off-by: Bryan Boreham --- promql/engine.go | 73 ++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 821a0e3698..b73364bc87 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1292,7 +1292,6 @@ func (ev *evaluator) rangeEval(prepSeries func(labels.Labels, *EvalSeriesHelper) } func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping []string) (Matrix, annotations.Annotations) { - numSteps := int((ev.endTimestamp-ev.startTimestamp)/ev.interval) + 1 originalNumSamples := ev.currentSamples var warnings annotations.Annotations @@ -1315,11 +1314,7 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping var vector Vector // Input vectors for the function. biggestLen := len(inputMatrix) enh := &EvalNodeHelper{Out: make(Vector, 0, biggestLen)} - type seriesAndTimestamp struct { - Series - ts int64 - } - seriess := make(map[uint64]seriesAndTimestamp, biggestLen) // Output series by series hash. + seriess := make(map[uint64]Series, biggestLen) // Output series by series hash. tempNumSamples := ev.currentSamples // Initialise series helpers with the grouping key. @@ -1367,7 +1362,7 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping // Make the function call. enh.Ts = ts - result, ws := ev.aggregation(aggExpr, sortedGrouping, param, vector, bufHelper, enh) + result, ws := ev.aggregation(aggExpr, sortedGrouping, param, vector, bufHelper, enh, seriess) enh.Out = result[:0] // Reuse result vector. warnings.Merge(ws) @@ -1386,9 +1381,6 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping // If this could be an instant query, shortcut so as not to change sort order. if ev.endTimestamp == ev.startTimestamp { - if result.ContainsSameLabelset() { - ev.errorf("vector cannot contain metrics with the same labelset") - } mat := make(Matrix, len(result)) for i, s := range result { if s.H == nil { @@ -1401,32 +1393,6 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping ev.samplesStats.UpdatePeak(ev.currentSamples) return mat, warnings } - - // Add samples in output vector to output series. - for _, sample := range result { - h := sample.Metric.Hash() - ss, ok := seriess[h] - if ok { - if ss.ts == ts { // If we've seen this output series before at this timestamp, it's a duplicate. - ev.errorf("vector cannot contain metrics with the same labelset") - } - ss.ts = ts - } else { - ss = seriesAndTimestamp{Series{Metric: sample.Metric}, ts} - } - if sample.H == nil { - if ss.Floats == nil { - ss.Floats = getFPointSlice(numSteps) - } - ss.Floats = append(ss.Floats, FPoint{T: ts, F: sample.F}) - } else { - if ss.Histograms == nil { - ss.Histograms = getHPointSlice(numSteps) - } - ss.Histograms = append(ss.Histograms, HPoint{T: ts, H: sample.H}) - } - seriess[h] = ss - } } // Reuse the original point slice. @@ -1437,7 +1403,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. mat := make(Matrix, 0, len(seriess)) for _, ss := range seriess { - mat = append(mat, ss.Series) + mat = append(mat, ss) } ev.currentSamples = originalNumSamples + mat.TotalSamples() ev.samplesStats.UpdatePeak(ev.currentSamples) @@ -2778,7 +2744,7 @@ type groupedAggregation struct { // aggregation evaluates an aggregation operation on a Vector. The provided grouping labels // must be sorted. -func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, param interface{}, vec Vector, seriesHelper []EvalSeriesHelper, enh *EvalNodeHelper) (Vector, annotations.Annotations) { +func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, param interface{}, vec Vector, seriesHelper []EvalSeriesHelper, enh *EvalNodeHelper, seriess map[uint64]Series) (Vector, annotations.Annotations) { op := e.Op without := e.Without var annos annotations.Annotations @@ -3055,7 +3021,36 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par H: aggr.histogramValue, }) } - return enh.Out, annos + + ts := enh.Ts + // If this could be an instant query, shortcut so as not to change sort order. + if ev.endTimestamp == ev.startTimestamp { + return enh.Out, annos + } + + numSteps := int((ev.endTimestamp-ev.startTimestamp)/ev.interval) + 1 + // Add samples in output vector to output series. + for _, sample := range enh.Out { + h := sample.Metric.Hash() + ss, ok := seriess[h] + if !ok { + ss = Series{Metric: sample.Metric} + } + if sample.H == nil { + if ss.Floats == nil { + ss.Floats = getFPointSlice(numSteps) + } + ss.Floats = append(ss.Floats, FPoint{T: ts, F: sample.F}) + } else { + if ss.Histograms == nil { + ss.Histograms = getHPointSlice(numSteps) + } + ss.Histograms = append(ss.Histograms, HPoint{T: ts, H: sample.H}) + } + seriess[h] = ss + } + + return nil, annos } // aggregationK evaluates count_values on vec. From 3851b74db1923ac414a7a0b88a81c9b663efd1ab Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Thu, 29 Feb 2024 23:21:46 +0000 Subject: [PATCH 105/127] promql: aggregations: skip result vector in range queries Adjust test to match the lower count, since samples in the vector are no longer counted. Signed-off-by: Bryan Boreham --- promql/engine.go | 89 +++++++++++++++++-------------------------- promql/engine_test.go | 2 +- 2 files changed, 35 insertions(+), 56 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index b73364bc87..5757604b7a 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1367,18 +1367,6 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping enh.Out = result[:0] // Reuse result vector. warnings.Merge(ws) - vecNumSamples := result.TotalSamples() - ev.currentSamples += vecNumSamples - // When we reset currentSamples to tempNumSamples during the next iteration of the loop it also - // needs to include the samples from the result here, as they're still in memory. - tempNumSamples += vecNumSamples - ev.samplesStats.UpdatePeak(ev.currentSamples) - - if ev.currentSamples > ev.maxSamples { - ev.error(ErrTooManySamples(env)) - } - ev.samplesStats.UpdatePeak(ev.currentSamples) - // If this could be an instant query, shortcut so as not to change sort order. if ev.endTimestamp == ev.startTimestamp { mat := make(Matrix, len(result)) @@ -1393,6 +1381,9 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping ev.samplesStats.UpdatePeak(ev.currentSamples) return mat, warnings } + if ev.currentSamples > ev.maxSamples { + ev.error(ErrTooManySamples(env)) + } } // Reuse the original point slice. @@ -2946,7 +2937,33 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par } } - // Construct the result Vector from the aggregated groups. + // Construct the result from the aggregated groups. + numSteps := int((ev.endTimestamp-ev.startTimestamp)/ev.interval) + 1 + add := func(lbls labels.Labels, f float64, h *histogram.FloatHistogram) { + // If this could be an instant query, build a slice so the result is in consistent order. + if ev.endTimestamp == ev.startTimestamp { + enh.Out = append(enh.Out, Sample{Metric: lbls, F: f, H: h}) + } else { + // Otherwise the results are added into seriess elements. + hash := lbls.Hash() + ss, ok := seriess[hash] + if !ok { + ss = Series{Metric: lbls} + } + if h == nil { + if ss.Floats == nil { + ss.Floats = getFPointSlice(numSteps) + } + ss.Floats = append(ss.Floats, FPoint{T: enh.Ts, F: f}) + } else { + if ss.Histograms == nil { + ss.Histograms = getHPointSlice(numSteps) + } + ss.Histograms = append(ss.Histograms, HPoint{T: enh.Ts, H: h}) + } + seriess[hash] = ss + } + } for _, aggr := range orderedResult { switch op { case parser.AVG: @@ -2976,10 +2993,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par sort.Sort(sort.Reverse(aggr.heap)) } for _, v := range aggr.heap { - enh.Out = append(enh.Out, Sample{ - Metric: v.Metric, - F: v.F, - }) + add(v.Metric, v.F, nil) } continue // Bypass default append. @@ -2989,10 +3003,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par sort.Sort(sort.Reverse(aggr.reverseHeap)) } for _, v := range aggr.reverseHeap { - enh.Out = append(enh.Out, Sample{ - Metric: v.Metric, - F: v.F, - }) + add(v.Metric, v.F, nil) } continue // Bypass default append. @@ -3015,42 +3026,10 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par // For other aggregations, we already have the right value. } - enh.Out = append(enh.Out, Sample{ - Metric: aggr.labels, - F: aggr.floatValue, - H: aggr.histogramValue, - }) + add(aggr.labels, aggr.floatValue, aggr.histogramValue) } - ts := enh.Ts - // If this could be an instant query, shortcut so as not to change sort order. - if ev.endTimestamp == ev.startTimestamp { - return enh.Out, annos - } - - numSteps := int((ev.endTimestamp-ev.startTimestamp)/ev.interval) + 1 - // Add samples in output vector to output series. - for _, sample := range enh.Out { - h := sample.Metric.Hash() - ss, ok := seriess[h] - if !ok { - ss = Series{Metric: sample.Metric} - } - if sample.H == nil { - if ss.Floats == nil { - ss.Floats = getFPointSlice(numSteps) - } - ss.Floats = append(ss.Floats, FPoint{T: ts, F: sample.F}) - } else { - if ss.Histograms == nil { - ss.Histograms = getHPointSlice(numSteps) - } - ss.Histograms = append(ss.Histograms, HPoint{T: ts, H: sample.H}) - } - seriess[h] = ss - } - - return nil, annos + return enh.Out, annos } // aggregationK evaluates count_values on vec. diff --git a/promql/engine_test.go b/promql/engine_test.go index 13731efd45..0202c15ae1 100644 --- a/promql/engine_test.go +++ b/promql/engine_test.go @@ -966,7 +966,7 @@ load 10s { Query: "sum by (b) (max_over_time(metricWith3SampleEvery10Seconds[60s] @ 30))", Start: time.Unix(201, 0), - PeakSamples: 8, + PeakSamples: 7, TotalSamples: 12, // @ modifier force the evaluation to at 30 seconds - So it brings 4 datapoints (0, 10, 20, 30 seconds) * 3 series TotalSamplesPerStep: stats.TotalSamplesPerStep{ 201000: 12, From c9b6c4c55ae89f9ab4b4f232247eae67d4062c40 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Thu, 29 Feb 2024 23:39:29 +0000 Subject: [PATCH 106/127] promql: aggregations: output directly to matrix for instant queries Signed-off-by: Bryan Boreham --- promql/engine.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 5757604b7a..2e5f620e32 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1313,7 +1313,7 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping var vector Vector // Input vectors for the function. biggestLen := len(inputMatrix) - enh := &EvalNodeHelper{Out: make(Vector, 0, biggestLen)} + enh := &EvalNodeHelper{} seriess := make(map[uint64]Series, biggestLen) // Output series by series hash. tempNumSamples := ev.currentSamples @@ -1364,22 +1364,13 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping enh.Ts = ts result, ws := ev.aggregation(aggExpr, sortedGrouping, param, vector, bufHelper, enh, seriess) - enh.Out = result[:0] // Reuse result vector. warnings.Merge(ws) // If this could be an instant query, shortcut so as not to change sort order. if ev.endTimestamp == ev.startTimestamp { - mat := make(Matrix, len(result)) - for i, s := range result { - if s.H == nil { - mat[i] = Series{Metric: s.Metric, Floats: []FPoint{{T: ts, F: s.F}}} - } else { - mat[i] = Series{Metric: s.Metric, Histograms: []HPoint{{T: ts, H: s.H}}} - } - } - ev.currentSamples = originalNumSamples + mat.TotalSamples() + ev.currentSamples = originalNumSamples + result.TotalSamples() ev.samplesStats.UpdatePeak(ev.currentSamples) - return mat, warnings + return result, warnings } if ev.currentSamples > ev.maxSamples { ev.error(ErrTooManySamples(env)) @@ -2735,7 +2726,7 @@ type groupedAggregation struct { // aggregation evaluates an aggregation operation on a Vector. The provided grouping labels // must be sorted. -func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, param interface{}, vec Vector, seriesHelper []EvalSeriesHelper, enh *EvalNodeHelper, seriess map[uint64]Series) (Vector, annotations.Annotations) { +func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, param interface{}, vec Vector, seriesHelper []EvalSeriesHelper, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { op := e.Op without := e.Without var annos annotations.Annotations @@ -2749,7 +2740,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par } k = int64(f) if k < 1 { - return Vector{}, annos + return nil, annos } } var q float64 @@ -2939,10 +2930,19 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par // Construct the result from the aggregated groups. numSteps := int((ev.endTimestamp-ev.startTimestamp)/ev.interval) + 1 + var mat Matrix + if ev.endTimestamp == ev.startTimestamp { + mat = make(Matrix, 0, len(orderedResult)) + } + add := func(lbls labels.Labels, f float64, h *histogram.FloatHistogram) { - // If this could be an instant query, build a slice so the result is in consistent order. + // If this could be an instant query, add directly to the matrix so the result is in consistent order. if ev.endTimestamp == ev.startTimestamp { - enh.Out = append(enh.Out, Sample{Metric: lbls, F: f, H: h}) + if h == nil { + mat = append(mat, Series{Metric: lbls, Floats: []FPoint{{T: enh.Ts, F: f}}}) + } else { + mat = append(mat, Series{Metric: lbls, Histograms: []HPoint{{T: enh.Ts, H: h}}}) + } } else { // Otherwise the results are added into seriess elements. hash := lbls.Hash() @@ -3029,7 +3029,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par add(aggr.labels, aggr.floatValue, aggr.histogramValue) } - return enh.Out, annos + return mat, annos } // aggregationK evaluates count_values on vec. From b3bda7df4b7d1e5e47edb0bb3b7ea9d3057fefdc Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 1 Mar 2024 16:02:54 +0000 Subject: [PATCH 107/127] promql: aggregations: skip copying input to a Vector We can work directly from the inputMatrix on each timestep. Signed-off-by: Bryan Boreham --- promql/engine.go | 55 ++++++++++++++++++++---------------------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 2e5f620e32..5fb1d849df 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1311,7 +1311,6 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping // Keep a copy of the original point slice so that it can be returned to the pool. origMatrix := inputMatrix - var vector Vector // Input vectors for the function. biggestLen := len(inputMatrix) enh := &EvalNodeHelper{} seriess := make(map[uint64]Series, biggestLen) // Output series by series hash. @@ -1321,7 +1320,6 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping buf := make([]byte, 0, 1024) seriesHelper := make([]EvalSeriesHelper, len(inputMatrix)) - bufHelper := make([]EvalSeriesHelper, len(inputMatrix)) for si, series := range inputMatrix { seriesHelper[si].groupingKey, buf = generateGroupingKey(series.Metric, sortedGrouping, aggExpr.Without, buf) @@ -1333,36 +1331,10 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping } // Reset number of samples in memory after each timestamp. ev.currentSamples = tempNumSamples - // Gather input vectors for this timestamp. - { - vector = vector[:0] - bufHelper = bufHelper[:0] - - for si, series := range inputMatrix { - switch { - case len(series.Floats) > 0 && series.Floats[0].T == ts: - vector = append(vector, Sample{Metric: series.Metric, F: series.Floats[0].F, T: ts}) - // Move input vectors forward so we don't have to re-scan the same - // past points at the next step. - inputMatrix[si].Floats = series.Floats[1:] - case len(series.Histograms) > 0 && series.Histograms[0].T == ts: - vector = append(vector, Sample{Metric: series.Metric, H: series.Histograms[0].H, T: ts}) - inputMatrix[si].Histograms = series.Histograms[1:] - default: - continue - } - bufHelper = append(bufHelper, seriesHelper[si]) - ev.currentSamples++ - if ev.currentSamples > ev.maxSamples { - ev.error(ErrTooManySamples(env)) - } - } - ev.samplesStats.UpdatePeak(ev.currentSamples) - } // Make the function call. enh.Ts = ts - result, ws := ev.aggregation(aggExpr, sortedGrouping, param, vector, bufHelper, enh, seriess) + result, ws := ev.aggregation(aggExpr, sortedGrouping, param, inputMatrix, seriesHelper, enh, seriess) warnings.Merge(ws) @@ -2726,7 +2698,7 @@ type groupedAggregation struct { // aggregation evaluates an aggregation operation on a Vector. The provided grouping labels // must be sorted. -func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, param interface{}, vec Vector, seriesHelper []EvalSeriesHelper, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { +func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, param interface{}, inputMatrix Matrix, seriesHelper []EvalSeriesHelper, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { op := e.Op without := e.Without var annos annotations.Annotations @@ -2748,7 +2720,26 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par q = param.(float64) } - for si, s := range vec { + for si, series := range inputMatrix { + var s Sample + + switch { + case len(series.Floats) > 0 && series.Floats[0].T == enh.Ts: + s = Sample{Metric: series.Metric, F: series.Floats[0].F, T: enh.Ts} + // Move input vectors forward so we don't have to re-scan the same + // past points at the next step. + inputMatrix[si].Floats = series.Floats[1:] + case len(series.Histograms) > 0 && series.Histograms[0].T == enh.Ts: + s = Sample{Metric: series.Metric, H: series.Histograms[0].H, T: enh.Ts} + inputMatrix[si].Histograms = series.Histograms[1:] + default: + continue + } + ev.currentSamples++ + if ev.currentSamples > ev.maxSamples { + ev.error(ErrTooManySamples(env)) + } + metric := s.Metric groupingKey := seriesHelper[si].groupingKey @@ -2775,7 +2766,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par newAgg.groupCount = 0 } - inputVecLen := int64(len(vec)) + inputVecLen := int64(len(inputMatrix)) resultSize := k switch { case k > inputVecLen: From cb6c4b3092ce7e42a2fe08011c5ad283ff17c64e Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 1 Mar 2024 16:01:20 +0000 Subject: [PATCH 108/127] promql: simplify k/q parameter to topk/bottomk/quantile Pass it as a float64 not as interface{}. Make k a simple int, since that is the parameter to make(). Pull invalid quantile warning out of the loop. Signed-off-by: Bryan Boreham --- promql/engine.go | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 5fb1d849df..35dc52942c 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1295,7 +1295,7 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping originalNumSamples := ev.currentSamples var warnings annotations.Annotations - // param is the number k for topk/bottomk. + // param is the number k for topk/bottomk, or q for quantile. var param float64 if aggExpr.Param != nil { val, ws := ev.eval(aggExpr.Param) @@ -2698,26 +2698,29 @@ type groupedAggregation struct { // aggregation evaluates an aggregation operation on a Vector. The provided grouping labels // must be sorted. -func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, param interface{}, inputMatrix Matrix, seriesHelper []EvalSeriesHelper, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { +func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, q float64, inputMatrix Matrix, seriesHelper []EvalSeriesHelper, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { op := e.Op without := e.Without var annos annotations.Annotations result := map[uint64]*groupedAggregation{} orderedResult := []*groupedAggregation{} - var k int64 + k := 1 if op == parser.TOPK || op == parser.BOTTOMK { - f := param.(float64) - if !convertibleToInt64(f) { - ev.errorf("Scalar value %v overflows int64", f) + if !convertibleToInt64(q) { + ev.errorf("Scalar value %v overflows int64", q) + } + k = int(q) + if k > len(inputMatrix) { + k = len(inputMatrix) } - k = int64(f) if k < 1 { return nil, annos } } - var q float64 if op == parser.QUANTILE { - q = param.(float64) + if math.IsNaN(q) || q < 0 || q > 1 { + annos.Add(annotations.NewInvalidQuantileWarning(q, e.Param.PositionRange())) + } } for si, series := range inputMatrix { @@ -2766,25 +2769,17 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par newAgg.groupCount = 0 } - inputVecLen := int64(len(inputMatrix)) - resultSize := k - switch { - case k > inputVecLen: - resultSize = inputVecLen - case k == 0: - resultSize = 1 - } switch op { case parser.STDVAR, parser.STDDEV: newAgg.floatValue = 0 case parser.TOPK, parser.QUANTILE: - newAgg.heap = make(vectorByValueHeap, 1, resultSize) + newAgg.heap = make(vectorByValueHeap, 1, k) newAgg.heap[0] = Sample{ F: s.F, Metric: s.Metric, } case parser.BOTTOMK: - newAgg.reverseHeap = make(vectorByReverseValueHeap, 1, resultSize) + newAgg.reverseHeap = make(vectorByReverseValueHeap, 1, k) newAgg.reverseHeap[0] = Sample{ F: s.F, Metric: s.Metric, @@ -2876,7 +2871,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par case parser.TOPK: // We build a heap of up to k elements, with the smallest element at heap[0]. switch { - case int64(len(group.heap)) < k: + case len(group.heap) < k: heap.Push(&group.heap, &Sample{ F: s.F, Metric: s.Metric, @@ -2895,7 +2890,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par case parser.BOTTOMK: // We build a heap of up to k elements, with the biggest element at heap[0]. switch { - case int64(len(group.reverseHeap)) < k: + case len(group.reverseHeap) < k: heap.Push(&group.reverseHeap, &Sample{ F: s.F, Metric: s.Metric, @@ -2999,9 +2994,6 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, par continue // Bypass default append. case parser.QUANTILE: - if math.IsNaN(q) || q < 0 || q > 1 { - annos.Add(annotations.NewInvalidQuantileWarning(q, e.Param.PositionRange())) - } aggr.floatValue = quantile(q, aggr.heap) case parser.SUM: From 53a3138eeb54eeb7331c300cebd2d0122c8ba796 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Sat, 2 Mar 2024 12:52:35 +0000 Subject: [PATCH 109/127] promql aggregations: pre-generate mapping from inputs to outputs So we don't have to re-create it on every time step. Signed-off-by: Bryan Boreham --- promql/engine.go | 77 ++++++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 35dc52942c..140c0a0e49 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1067,8 +1067,6 @@ func (ev *evaluator) Eval(expr parser.Expr) (v parser.Value, ws annotations.Anno // EvalSeriesHelper stores extra information about a series. type EvalSeriesHelper struct { - // The grouping key used by aggregation. - groupingKey uint64 // Used to map left-hand to right-hand in binary operations. signature string } @@ -1316,13 +1314,25 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping seriess := make(map[uint64]Series, biggestLen) // Output series by series hash. tempNumSamples := ev.currentSamples - // Initialise series helpers with the grouping key. + // Create a mapping from input series to output groups. buf := make([]byte, 0, 1024) - - seriesHelper := make([]EvalSeriesHelper, len(inputMatrix)) + groupToResultIndex := make(map[uint64]int) + seriesToResult := make([]int, len(inputMatrix)) + orderedResult := make([]*groupedAggregation, 0, 16) for si, series := range inputMatrix { - seriesHelper[si].groupingKey, buf = generateGroupingKey(series.Metric, sortedGrouping, aggExpr.Without, buf) + var groupingKey uint64 + groupingKey, buf = generateGroupingKey(series.Metric, sortedGrouping, aggExpr.Without, buf) + index, ok := groupToResultIndex[groupingKey] + // Add a new group if it doesn't exist. + if !ok { + m := generateGroupingLabels(enh, series.Metric, aggExpr.Without, sortedGrouping) + newAgg := &groupedAggregation{labels: m} + index = len(orderedResult) + groupToResultIndex[groupingKey] = index + orderedResult = append(orderedResult, newAgg) + } + seriesToResult[si] = index } for ts := ev.startTimestamp; ts <= ev.endTimestamp; ts += ev.interval { @@ -1334,7 +1344,7 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping // Make the function call. enh.Ts = ts - result, ws := ev.aggregation(aggExpr, sortedGrouping, param, inputMatrix, seriesHelper, enh, seriess) + result, ws := ev.aggregation(aggExpr, param, inputMatrix, seriesToResult, orderedResult, enh, seriess) warnings.Merge(ws) @@ -2698,12 +2708,10 @@ type groupedAggregation struct { // aggregation evaluates an aggregation operation on a Vector. The provided grouping labels // must be sorted. -func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, q float64, inputMatrix Matrix, seriesHelper []EvalSeriesHelper, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { +func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix Matrix, seriesToResult []int, orderedResult []*groupedAggregation, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { op := e.Op - without := e.Without var annos annotations.Annotations - result := map[uint64]*groupedAggregation{} - orderedResult := []*groupedAggregation{} + seen := make([]bool, len(orderedResult)) // Which output groups were seen in the input at this timestamp. k := 1 if op == parser.TOPK || op == parser.BOTTOMK { if !convertibleToInt64(q) { @@ -2743,53 +2751,47 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, q f ev.error(ErrTooManySamples(env)) } - metric := s.Metric - groupingKey := seriesHelper[si].groupingKey - - group, ok := result[groupingKey] - // Add a new group if it doesn't exist. - if !ok { - m := generateGroupingLabels(enh, metric, without, grouping) - newAgg := &groupedAggregation{ - labels: m, + group := orderedResult[seriesToResult[si]] + // Initialize this group if it's the first time we've seen it. + if !seen[seriesToResult[si]] { + *group = groupedAggregation{ + labels: group.labels, floatValue: s.F, floatMean: s.F, groupCount: 1, } switch { case s.H == nil: - newAgg.hasFloat = true + group.hasFloat = true case op == parser.SUM: - newAgg.histogramValue = s.H.Copy() - newAgg.hasHistogram = true + group.histogramValue = s.H.Copy() + group.hasHistogram = true case op == parser.AVG: - newAgg.histogramMean = s.H.Copy() - newAgg.hasHistogram = true + group.histogramMean = s.H.Copy() + group.hasHistogram = true case op == parser.STDVAR || op == parser.STDDEV: - newAgg.groupCount = 0 + group.groupCount = 0 } switch op { case parser.STDVAR, parser.STDDEV: - newAgg.floatValue = 0 + group.floatValue = 0 case parser.TOPK, parser.QUANTILE: - newAgg.heap = make(vectorByValueHeap, 1, k) - newAgg.heap[0] = Sample{ + group.heap = make(vectorByValueHeap, 1, k) + group.heap[0] = Sample{ F: s.F, Metric: s.Metric, } case parser.BOTTOMK: - newAgg.reverseHeap = make(vectorByReverseValueHeap, 1, k) - newAgg.reverseHeap[0] = Sample{ + group.reverseHeap = make(vectorByReverseValueHeap, 1, k) + group.reverseHeap[0] = Sample{ F: s.F, Metric: s.Metric, } case parser.GROUP: - newAgg.floatValue = 1 + group.floatValue = 1 } - - result[groupingKey] = newAgg - orderedResult = append(orderedResult, newAgg) + seen[seriesToResult[si]] = true continue } @@ -2950,7 +2952,10 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, grouping []string, q f seriess[hash] = ss } } - for _, aggr := range orderedResult { + for ri, aggr := range orderedResult { + if !seen[ri] { + continue + } switch op { case parser.AVG: if aggr.hasFloat && aggr.hasHistogram { From eb41e770b79fe3824a342efbab4fc169364706ef Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Sun, 3 Mar 2024 16:52:43 +0000 Subject: [PATCH 110/127] promql: refactor: extract function addToSeries Signed-off-by: Bryan Boreham --- promql/engine.go | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 140c0a0e49..f915c3480e 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1257,17 +1257,7 @@ func (ev *evaluator) rangeEval(prepSeries func(labels.Labels, *EvalSeriesHelper) } else { ss = seriesAndTimestamp{Series{Metric: sample.Metric}, ts} } - if sample.H == nil { - if ss.Floats == nil { - ss.Floats = getFPointSlice(numSteps) - } - ss.Floats = append(ss.Floats, FPoint{T: ts, F: sample.F}) - } else { - if ss.Histograms == nil { - ss.Histograms = getHPointSlice(numSteps) - } - ss.Histograms = append(ss.Histograms, HPoint{T: ts, H: sample.H}) - } + addToSeries(&ss.Series, enh.Ts, sample.F, sample.H, numSteps) seriess[h] = ss } } @@ -2938,17 +2928,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix if !ok { ss = Series{Metric: lbls} } - if h == nil { - if ss.Floats == nil { - ss.Floats = getFPointSlice(numSteps) - } - ss.Floats = append(ss.Floats, FPoint{T: enh.Ts, F: f}) - } else { - if ss.Histograms == nil { - ss.Histograms = getHPointSlice(numSteps) - } - ss.Histograms = append(ss.Histograms, HPoint{T: enh.Ts, H: h}) - } + addToSeries(&ss, enh.Ts, f, h, numSteps) seriess[hash] = ss } } @@ -3064,6 +3044,20 @@ func (ev *evaluator) aggregationCountValues(e *parser.AggregateExpr, grouping [] return enh.Out, nil } +func addToSeries(ss *Series, ts int64, f float64, h *histogram.FloatHistogram, numSteps int) { + if h == nil { + if ss.Floats == nil { + ss.Floats = getFPointSlice(numSteps) + } + ss.Floats = append(ss.Floats, FPoint{T: ts, F: f}) + } else { + if ss.Histograms == nil { + ss.Histograms = getHPointSlice(numSteps) + } + ss.Histograms = append(ss.Histograms, HPoint{T: ts, H: h}) + } +} + // groupingKey builds and returns the grouping key for the given metric and // grouping labels. func generateGroupingKey(metric labels.Labels, grouping []string, without bool, buf []byte) (uint64, []byte) { From 602eb69edfe87f90e8a1a2844168c946f83aa3f6 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 5 Apr 2024 11:37:55 +0100 Subject: [PATCH 111/127] promql: refactor: extract function nextSample With sub-function nextValues which we shall use shortly. Signed-off-by: Bryan Boreham --- promql/engine.go | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index f915c3480e..9e0a6b17c4 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -2721,25 +2721,11 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix } } - for si, series := range inputMatrix { - var s Sample - - switch { - case len(series.Floats) > 0 && series.Floats[0].T == enh.Ts: - s = Sample{Metric: series.Metric, F: series.Floats[0].F, T: enh.Ts} - // Move input vectors forward so we don't have to re-scan the same - // past points at the next step. - inputMatrix[si].Floats = series.Floats[1:] - case len(series.Histograms) > 0 && series.Histograms[0].T == enh.Ts: - s = Sample{Metric: series.Metric, H: series.Histograms[0].H, T: enh.Ts} - inputMatrix[si].Histograms = series.Histograms[1:] - default: + for si := range inputMatrix { + s, ok := ev.nextSample(enh.Ts, inputMatrix, si) + if !ok { continue } - ev.currentSamples++ - if ev.currentSamples > ev.maxSamples { - ev.error(ErrTooManySamples(env)) - } group := orderedResult[seriesToResult[si]] // Initialize this group if it's the first time we've seen it. @@ -3058,6 +3044,29 @@ func addToSeries(ss *Series, ts int64, f float64, h *histogram.FloatHistogram, n } } +func (ev *evaluator) nextValues(ts int64, series *Series) (f float64, h *histogram.FloatHistogram, b bool) { + switch { + case len(series.Floats) > 0 && series.Floats[0].T == ts: + f = series.Floats[0].F + series.Floats = series.Floats[1:] // Move input vectors forward + case len(series.Histograms) > 0 && series.Histograms[0].T == ts: + h = series.Histograms[0].H + series.Histograms = series.Histograms[1:] + default: + return f, h, false + } + return f, h, true +} + +func (ev *evaluator) nextSample(ts int64, inputMatrix Matrix, si int) (Sample, bool) { + f, h, ok := ev.nextValues(ts, &inputMatrix[si]) + ev.currentSamples++ + if ev.currentSamples > ev.maxSamples { + ev.error(ErrTooManySamples(env)) + } + return Sample{Metric: inputMatrix[si].Metric, F: f, H: h, T: ts}, ok +} + // groupingKey builds and returns the grouping key for the given metric and // grouping labels. func generateGroupingKey(metric labels.Labels, grouping []string, without bool, buf []byte) (uint64, []byte) { From 74eed67ef6c41ea252e3208f7db6ac52e9b941d8 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 5 Apr 2024 11:56:04 +0100 Subject: [PATCH 112/127] promql: refactor: pull fetching input data out of rangeEvalAgg This is a cleaner split of responsibilities. We now check the sample count after calling rangeEvalAgg. Changed re-use of samples to use `Clone` and `defer`. Signed-off-by: Bryan Boreham --- promql/engine.go | 62 +++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 9e0a6b17c4..770550dac1 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1279,29 +1279,19 @@ func (ev *evaluator) rangeEval(prepSeries func(labels.Labels, *EvalSeriesHelper) return mat, warnings } -func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping []string) (Matrix, annotations.Annotations) { - originalNumSamples := ev.currentSamples +func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping []string, inputMatrix Matrix, param float64) (Matrix, annotations.Annotations) { + // Keep a copy of the original point slice so that it can be returned to the pool. + origMatrix := slices.Clone(inputMatrix) + defer func() { + for _, s := range origMatrix { + putFPointSlice(s.Floats) + putHPointSlice(s.Histograms) + } + }() + var warnings annotations.Annotations - // param is the number k for topk/bottomk, or q for quantile. - var param float64 - if aggExpr.Param != nil { - val, ws := ev.eval(aggExpr.Param) - warnings.Merge(ws) - param = val.(Matrix)[0].Floats[0].F - } - // Now fetch the data to be aggregated. - // ev.currentSamples will be updated to the correct value within the ev.eval call. - val, ws := ev.eval(aggExpr.Expr) - warnings.Merge(ws) - inputMatrix := val.(Matrix) - - // Keep a copy of the original point slice so that it can be returned to the pool. - origMatrix := inputMatrix - - biggestLen := len(inputMatrix) enh := &EvalNodeHelper{} - seriess := make(map[uint64]Series, biggestLen) // Output series by series hash. tempNumSamples := ev.currentSamples // Create a mapping from input series to output groups. @@ -1325,6 +1315,8 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping seriesToResult[si] = index } + seriess := make(map[uint64]Series, len(inputMatrix)) // Output series by series hash. + for ts := ev.startTimestamp; ts <= ev.endTimestamp; ts += ev.interval { if err := contextDone(ev.ctx, "expression evaluation"); err != nil { ev.error(err) @@ -1340,8 +1332,6 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping // If this could be an instant query, shortcut so as not to change sort order. if ev.endTimestamp == ev.startTimestamp { - ev.currentSamples = originalNumSamples + result.TotalSamples() - ev.samplesStats.UpdatePeak(ev.currentSamples) return result, warnings } if ev.currentSamples > ev.maxSamples { @@ -1349,18 +1339,11 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping } } - // Reuse the original point slice. - for _, s := range origMatrix { - putFPointSlice(s.Floats) - putHPointSlice(s.Histograms) - } // Assemble the output matrix. By the time we get here we know we don't have too many samples. mat := make(Matrix, 0, len(seriess)) for _, ss := range seriess { mat = append(mat, ss) } - ev.currentSamples = originalNumSamples + mat.TotalSamples() - ev.samplesStats.UpdatePeak(ev.currentSamples) return mat, warnings } @@ -1434,7 +1417,26 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, annotations.Annotatio }, e.Expr) } - return ev.rangeEvalAgg(e, sortedGrouping) + var warnings annotations.Annotations + originalNumSamples := ev.currentSamples + // param is the number k for topk/bottomk, or q for quantile. + var fParam float64 + if param != nil { + val, ws := ev.eval(param) + warnings.Merge(ws) + fParam = val.(Matrix)[0].Floats[0].F + } + // Now fetch the data to be aggregated. + val, ws := ev.eval(e.Expr) + warnings.Merge(ws) + inputMatrix := val.(Matrix) + + result, ws := ev.rangeEvalAgg(e, sortedGrouping, inputMatrix, fParam) + warnings.Merge(ws) + ev.currentSamples = originalNumSamples + result.TotalSamples() + ev.samplesStats.UpdatePeak(ev.currentSamples) + + return result, warnings case *parser.Call: call := FunctionCalls[e.Func.Name] From 2f03acbafc08dd83cc8a5c3d94b2f071bb34f809 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 5 Apr 2024 12:22:44 +0100 Subject: [PATCH 113/127] promql: refactor: split topk/bottomk from sum/avg/etc They aggregate results in different ways. topk/bottomk don't consider histograms so can simplify data collection. Signed-off-by: Bryan Boreham --- promql/engine.go | 294 ++++++++++++++++++++++++++++------------------- 1 file changed, 174 insertions(+), 120 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 770550dac1..22428e12c7 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1299,6 +1299,7 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping groupToResultIndex := make(map[uint64]int) seriesToResult := make([]int, len(inputMatrix)) orderedResult := make([]*groupedAggregation, 0, 16) + var result Matrix for si, series := range inputMatrix { var groupingKey uint64 @@ -1306,8 +1307,11 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping index, ok := groupToResultIndex[groupingKey] // Add a new group if it doesn't exist. if !ok { - m := generateGroupingLabels(enh, series.Metric, aggExpr.Without, sortedGrouping) - newAgg := &groupedAggregation{labels: m} + if aggExpr.Op != parser.TOPK && aggExpr.Op != parser.BOTTOMK { + m := generateGroupingLabels(enh, series.Metric, aggExpr.Without, sortedGrouping) + result = append(result, Series{Metric: m}) + } + newAgg := &groupedAggregation{} index = len(orderedResult) groupToResultIndex[groupingKey] = index orderedResult = append(orderedResult, newAgg) @@ -1315,7 +1319,11 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping seriesToResult[si] = index } - seriess := make(map[uint64]Series, len(inputMatrix)) // Output series by series hash. + var seriess map[uint64]Series + switch aggExpr.Op { + case parser.TOPK, parser.BOTTOMK: + seriess = make(map[uint64]Series, len(inputMatrix)) // Output series by series hash. + } for ts := ev.startTimestamp; ts <= ev.endTimestamp; ts += ev.interval { if err := contextDone(ev.ctx, "expression evaluation"); err != nil { @@ -1326,25 +1334,44 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping // Make the function call. enh.Ts = ts - result, ws := ev.aggregation(aggExpr, param, inputMatrix, seriesToResult, orderedResult, enh, seriess) + var ws annotations.Annotations + switch aggExpr.Op { + case parser.TOPK, parser.BOTTOMK: + result, ws = ev.aggregationK(aggExpr, param, inputMatrix, seriesToResult, orderedResult, 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 + } + default: + ws = ev.aggregation(aggExpr, param, inputMatrix, result, seriesToResult, orderedResult, enh) + } warnings.Merge(ws) - // If this could be an instant query, shortcut so as not to change sort order. - if ev.endTimestamp == ev.startTimestamp { - return result, warnings - } if ev.currentSamples > ev.maxSamples { ev.error(ErrTooManySamples(env)) } } // Assemble the output matrix. By the time we get here we know we don't have too many samples. - mat := make(Matrix, 0, len(seriess)) - for _, ss := range seriess { - mat = append(mat, ss) + switch aggExpr.Op { + case parser.TOPK, parser.BOTTOMK: + result = make(Matrix, 0, len(seriess)) + for _, ss := range seriess { + result = append(result, ss) + } + default: + // Remove empty result rows. + dst := 0 + for _, series := range result { + if len(series.Floats) > 0 || len(series.Histograms) > 0 { + result[dst] = series + dst++ + } + } + result = result[:dst] } - return mat, warnings + return result, warnings } // evalSubquery evaluates given SubqueryExpr and returns an equivalent @@ -2698,25 +2725,14 @@ type groupedAggregation struct { reverseHeap vectorByReverseValueHeap } -// aggregation evaluates an aggregation operation on a Vector. The provided grouping labels -// must be sorted. -func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix Matrix, seriesToResult []int, orderedResult []*groupedAggregation, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { +// aggregation evaluates sum, avg, count, stdvar, stddev or quantile at one timestep on inputMatrix. +// These functions produce one output series for each group specified in the expression, with just the labels from `by(...)`. +// outputMatrix should be already populated with grouping labels; groups is one-to-one with outputMatrix. +// seriesToResult maps inputMatrix indexes to outputMatrix indexes. +func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix, outputMatrix Matrix, seriesToResult []int, orderedResult []*groupedAggregation, enh *EvalNodeHelper) annotations.Annotations { op := e.Op var annos annotations.Annotations seen := make([]bool, len(orderedResult)) // Which output groups were seen in the input at this timestamp. - k := 1 - if op == parser.TOPK || op == parser.BOTTOMK { - if !convertibleToInt64(q) { - ev.errorf("Scalar value %v overflows int64", q) - } - k = int(q) - if k > len(inputMatrix) { - k = len(inputMatrix) - } - if k < 1 { - return nil, annos - } - } if op == parser.QUANTILE { if math.IsNaN(q) || q < 0 || q > 1 { annos.Add(annotations.NewInvalidQuantileWarning(q, e.Param.PositionRange())) @@ -2733,7 +2749,6 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix // Initialize this group if it's the first time we've seen it. if !seen[seriesToResult[si]] { *group = groupedAggregation{ - labels: group.labels, floatValue: s.F, floatMean: s.F, groupCount: 1, @@ -2754,18 +2769,12 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix switch op { case parser.STDVAR, parser.STDDEV: group.floatValue = 0 - case parser.TOPK, parser.QUANTILE: - group.heap = make(vectorByValueHeap, 1, k) + case parser.QUANTILE: + group.heap = make(vectorByValueHeap, 1) group.heap[0] = Sample{ F: s.F, Metric: s.Metric, } - case parser.BOTTOMK: - group.reverseHeap = make(vectorByReverseValueHeap, 1, k) - group.reverseHeap[0] = Sample{ - F: s.F, - Metric: s.Metric, - } case parser.GROUP: group.floatValue = 1 } @@ -2848,44 +2857,6 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix group.floatValue += delta * (s.F - group.floatMean) } - case parser.TOPK: - // We build a heap of up to k elements, with the smallest element at heap[0]. - switch { - case len(group.heap) < k: - heap.Push(&group.heap, &Sample{ - F: s.F, - Metric: s.Metric, - }) - case group.heap[0].F < s.F || (math.IsNaN(group.heap[0].F) && !math.IsNaN(s.F)): - // This new element is bigger than the previous smallest element - overwrite that. - group.heap[0] = Sample{ - F: s.F, - Metric: s.Metric, - } - if k > 1 { - heap.Fix(&group.heap, 0) // Maintain the heap invariant. - } - } - - case parser.BOTTOMK: - // We build a heap of up to k elements, with the biggest element at heap[0]. - switch { - case len(group.reverseHeap) < k: - heap.Push(&group.reverseHeap, &Sample{ - F: s.F, - Metric: s.Metric, - }) - case group.reverseHeap[0].F > s.F || (math.IsNaN(group.reverseHeap[0].F) && !math.IsNaN(s.F)): - // This new element is smaller than the previous biggest element - overwrite that. - group.reverseHeap[0] = Sample{ - F: s.F, - Metric: s.Metric, - } - if k > 1 { - heap.Fix(&group.reverseHeap, 0) // Maintain the heap invariant. - } - } - case parser.QUANTILE: group.heap = append(group.heap, s) @@ -2894,32 +2865,9 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix } } - // Construct the result from the aggregated groups. + // Construct the output matrix from the aggregated groups. numSteps := int((ev.endTimestamp-ev.startTimestamp)/ev.interval) + 1 - var mat Matrix - if ev.endTimestamp == ev.startTimestamp { - mat = make(Matrix, 0, len(orderedResult)) - } - add := func(lbls labels.Labels, f float64, h *histogram.FloatHistogram) { - // If this could be an instant query, add directly to the matrix so the result is in consistent order. - if ev.endTimestamp == ev.startTimestamp { - if h == nil { - mat = append(mat, Series{Metric: lbls, Floats: []FPoint{{T: enh.Ts, F: f}}}) - } else { - mat = append(mat, Series{Metric: lbls, Histograms: []HPoint{{T: enh.Ts, H: h}}}) - } - } else { - // Otherwise the results are added into seriess elements. - hash := lbls.Hash() - ss, ok := seriess[hash] - if !ok { - ss = Series{Metric: lbls} - } - addToSeries(&ss, enh.Ts, f, h, numSteps) - seriess[hash] = ss - } - } for ri, aggr := range orderedResult { if !seen[ri] { continue @@ -2946,26 +2894,6 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix case parser.STDDEV: aggr.floatValue = math.Sqrt(aggr.floatValue / float64(aggr.groupCount)) - case parser.TOPK: - // The heap keeps the lowest value on top, so reverse it. - if len(aggr.heap) > 1 { - sort.Sort(sort.Reverse(aggr.heap)) - } - for _, v := range aggr.heap { - add(v.Metric, v.F, nil) - } - continue // Bypass default append. - - case parser.BOTTOMK: - // The heap keeps the highest value on top, so reverse it. - if len(aggr.reverseHeap) > 1 { - sort.Sort(sort.Reverse(aggr.reverseHeap)) - } - for _, v := range aggr.reverseHeap { - add(v.Metric, v.F, nil) - } - continue // Bypass default append. - case parser.QUANTILE: aggr.floatValue = quantile(q, aggr.heap) @@ -2982,7 +2910,133 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix // For other aggregations, we already have the right value. } - add(aggr.labels, aggr.floatValue, aggr.histogramValue) + ss := &outputMatrix[ri] + addToSeries(ss, enh.Ts, aggr.floatValue, aggr.histogramValue, numSteps) + } + + return annos +} + +// aggregationK evaluates topk or bottomk 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 a range query, aggregates output in the seriess map. +func (ev *evaluator) aggregationK(e *parser.AggregateExpr, q float64, inputMatrix Matrix, seriesToResult []int, orderedResult []*groupedAggregation, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { + op := e.Op + var annos annotations.Annotations + seen := make([]bool, len(orderedResult)) // Which output groups were seen in the input at this timestamp. + if !convertibleToInt64(q) { + ev.errorf("Scalar value %v overflows int64", q) + } + k := int(q) + if k > len(inputMatrix) { + k = len(inputMatrix) + } + if k < 1 { + return nil, annos + } + + for si := range inputMatrix { + s, ok := ev.nextSample(enh.Ts, inputMatrix, si) + if !ok { + continue + } + + group := orderedResult[seriesToResult[si]] + // Initialize this group if it's the first time we've seen it. + if !seen[seriesToResult[si]] { + *group = groupedAggregation{} + + switch op { + case parser.TOPK: + group.heap = make(vectorByValueHeap, 1, k) + group.heap[0] = s + case parser.BOTTOMK: + group.reverseHeap = make(vectorByReverseValueHeap, 1, k) + group.reverseHeap[0] = s + } + seen[seriesToResult[si]] = true + continue + } + + switch op { + case parser.TOPK: + // We build a heap of up to k elements, with the smallest element at heap[0]. + switch { + case len(group.heap) < k: + heap.Push(&group.heap, &s) + case group.heap[0].F < s.F || (math.IsNaN(group.heap[0].F) && !math.IsNaN(s.F)): + // This new element is bigger than the previous smallest element - overwrite that. + group.heap[0] = s + if k > 1 { + heap.Fix(&group.heap, 0) // Maintain the heap invariant. + } + } + + case parser.BOTTOMK: + // We build a heap of up to k elements, with the biggest element at heap[0]. + switch { + case len(group.reverseHeap) < k: + heap.Push(&group.reverseHeap, &s) + case group.reverseHeap[0].F > s.F || (math.IsNaN(group.reverseHeap[0].F) && !math.IsNaN(s.F)): + // This new element is smaller than the previous biggest element - overwrite that. + group.reverseHeap[0] = s + if k > 1 { + heap.Fix(&group.reverseHeap, 0) // Maintain the heap invariant. + } + } + + default: + panic(fmt.Errorf("expected aggregation operator but got %q", op)) + } + } + + // Construct the result from the aggregated groups. + numSteps := int((ev.endTimestamp-ev.startTimestamp)/ev.interval) + 1 + var mat Matrix + if ev.endTimestamp == ev.startTimestamp { + mat = make(Matrix, 0, len(orderedResult)) + } + + add := func(lbls labels.Labels, f float64) { + // If this could be an instant query, add directly to the matrix so the result is in consistent order. + if ev.endTimestamp == ev.startTimestamp { + mat = append(mat, Series{Metric: lbls, Floats: []FPoint{{T: enh.Ts, F: f}}}) + } else { + // Otherwise the results are added into seriess elements. + hash := lbls.Hash() + ss, ok := seriess[hash] + if !ok { + ss = Series{Metric: lbls} + } + addToSeries(&ss, enh.Ts, f, nil, numSteps) + seriess[hash] = ss + } + } + for ri, aggr := range orderedResult { + if !seen[ri] { + continue + } + switch op { + case parser.TOPK: + // The heap keeps the lowest value on top, so reverse it. + if len(aggr.heap) > 1 { + sort.Sort(sort.Reverse(aggr.heap)) + } + for _, v := range aggr.heap { + add(v.Metric, v.F) + } + + case parser.BOTTOMK: + // The heap keeps the highest value on top, so reverse it. + if len(aggr.reverseHeap) > 1 { + sort.Sort(sort.Reverse(aggr.reverseHeap)) + } + for _, v := range aggr.reverseHeap { + add(v.Metric, v.F) + } + } } return mat, annos From 526ce4ee7ad8cd2802590596801f2be8821faa63 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Mon, 4 Mar 2024 21:05:00 +0000 Subject: [PATCH 114/127] promql: simplify data collection in aggregations We don't need a Sample, just the float and histogram values. Signed-off-by: Bryan Boreham --- promql/engine.go | 49 +++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 22428e12c7..a38cdf218e 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -2740,7 +2740,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix } for si := range inputMatrix { - s, ok := ev.nextSample(enh.Ts, inputMatrix, si) + f, h, ok := ev.nextValues(enh.Ts, &inputMatrix[si]) if !ok { continue } @@ -2749,18 +2749,18 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix // Initialize this group if it's the first time we've seen it. if !seen[seriesToResult[si]] { *group = groupedAggregation{ - floatValue: s.F, - floatMean: s.F, + floatValue: f, + floatMean: f, groupCount: 1, } switch { - case s.H == nil: + case h == nil: group.hasFloat = true case op == parser.SUM: - group.histogramValue = s.H.Copy() + group.histogramValue = h.Copy() group.hasHistogram = true case op == parser.AVG: - group.histogramMean = s.H.Copy() + group.histogramMean = h.Copy() group.hasHistogram = true case op == parser.STDVAR || op == parser.STDDEV: group.groupCount = 0 @@ -2771,10 +2771,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix group.floatValue = 0 case parser.QUANTILE: group.heap = make(vectorByValueHeap, 1) - group.heap[0] = Sample{ - F: s.F, - Metric: s.Metric, - } + group.heap[0] = Sample{F: f} case parser.GROUP: group.floatValue = 1 } @@ -2784,25 +2781,25 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix switch op { case parser.SUM: - if s.H != nil { + if h != nil { group.hasHistogram = true if group.histogramValue != nil { - group.histogramValue.Add(s.H) + group.histogramValue.Add(h) } // Otherwise the aggregation contained floats // previously and will be invalid anyway. No // point in copying the histogram in that case. } else { group.hasFloat = true - group.floatValue += s.F + group.floatValue += f } case parser.AVG: group.groupCount++ - if s.H != nil { + if h != nil { group.hasHistogram = true if group.histogramMean != nil { - left := s.H.Copy().Div(float64(group.groupCount)) + left := h.Copy().Div(float64(group.groupCount)) right := group.histogramMean.Copy().Div(float64(group.groupCount)) toAdd := left.Sub(right) group.histogramMean.Add(toAdd) @@ -2813,13 +2810,13 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix } else { group.hasFloat = true if math.IsInf(group.floatMean, 0) { - if math.IsInf(s.F, 0) && (group.floatMean > 0) == (s.F > 0) { + if math.IsInf(f, 0) && (group.floatMean > 0) == (f > 0) { // The `floatMean` and `s.F` values are `Inf` of the same sign. They // can't be subtracted, but the value of `floatMean` is correct // already. break } - if !math.IsInf(s.F, 0) && !math.IsNaN(s.F) { + if !math.IsInf(f, 0) && !math.IsNaN(f) { // At this stage, the mean is an infinite. If the added // value is neither an Inf or a Nan, we can keep that mean // value. @@ -2830,35 +2827,35 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix } } // Divide each side of the `-` by `group.groupCount` to avoid float64 overflows. - group.floatMean += s.F/float64(group.groupCount) - group.floatMean/float64(group.groupCount) + group.floatMean += f/float64(group.groupCount) - group.floatMean/float64(group.groupCount) } case parser.GROUP: // Do nothing. Required to avoid the panic in `default:` below. case parser.MAX: - if group.floatValue < s.F || math.IsNaN(group.floatValue) { - group.floatValue = s.F + if group.floatValue < f || math.IsNaN(group.floatValue) { + group.floatValue = f } case parser.MIN: - if group.floatValue > s.F || math.IsNaN(group.floatValue) { - group.floatValue = s.F + if group.floatValue > f || math.IsNaN(group.floatValue) { + group.floatValue = f } case parser.COUNT: group.groupCount++ case parser.STDVAR, parser.STDDEV: - if s.H == nil { // Ignore native histograms. + if h == nil { // Ignore native histograms. group.groupCount++ - delta := s.F - group.floatMean + delta := f - group.floatMean group.floatMean += delta / float64(group.groupCount) - group.floatValue += delta * (s.F - group.floatMean) + group.floatValue += delta * (f - group.floatMean) } case parser.QUANTILE: - group.heap = append(group.heap, s) + group.heap = append(group.heap, Sample{F: f}) default: panic(fmt.Errorf("expected aggregation operator but got %q", op)) From 4584f67e1706b5fa15bc4436c158fec701ae9402 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Sat, 9 Mar 2024 11:31:46 +0000 Subject: [PATCH 115/127] promql: inline nextSample function Move Sample out of loop to reduce allocations, otherwise it escapes to the heap. Signed-off-by: Bryan Boreham --- promql/engine.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index a38cdf218e..592114db2b 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -2921,6 +2921,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix // For a range query, aggregates output in the seriess map. func (ev *evaluator) aggregationK(e *parser.AggregateExpr, q float64, inputMatrix Matrix, seriesToResult []int, orderedResult []*groupedAggregation, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { op := e.Op + var s Sample var annos annotations.Annotations seen := make([]bool, len(orderedResult)) // Which output groups were seen in the input at this timestamp. if !convertibleToInt64(q) { @@ -2935,10 +2936,11 @@ func (ev *evaluator) aggregationK(e *parser.AggregateExpr, q float64, inputMatri } for si := range inputMatrix { - s, ok := ev.nextSample(enh.Ts, inputMatrix, si) + f, _, ok := ev.nextValues(enh.Ts, &inputMatrix[si]) if !ok { continue } + s = Sample{Metric: inputMatrix[si].Metric, F: f} group := orderedResult[seriesToResult[si]] // Initialize this group if it's the first time we've seen it. @@ -3111,15 +3113,6 @@ func (ev *evaluator) nextValues(ts int64, series *Series) (f float64, h *histogr return f, h, true } -func (ev *evaluator) nextSample(ts int64, inputMatrix Matrix, si int) (Sample, bool) { - f, h, ok := ev.nextValues(ts, &inputMatrix[si]) - ev.currentSamples++ - if ev.currentSamples > ev.maxSamples { - ev.error(ErrTooManySamples(env)) - } - return Sample{Metric: inputMatrix[si].Metric, F: f, H: h, T: ts}, ok -} - // groupingKey builds and returns the grouping key for the given metric and // grouping labels. func generateGroupingKey(metric labels.Labels, grouping []string, without bool, buf []byte) (uint64, []byte) { From 185290a0d2f5908665a7d59710850ce2fdf59cf2 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 8 Mar 2024 17:53:19 +0000 Subject: [PATCH 116/127] promql: pull checking of q and k out of loop Signed-off-by: Bryan Boreham --- promql/engine.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 592114db2b..0dd33a7f94 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1319,10 +1319,25 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping seriesToResult[si] = index } + var k int var seriess map[uint64]Series switch aggExpr.Op { case parser.TOPK, parser.BOTTOMK: + if !convertibleToInt64(param) { + ev.errorf("Scalar value %v overflows int64", param) + } + k = int(param) + if k > len(inputMatrix) { + k = len(inputMatrix) + } + if k < 1 { + return nil, warnings + } 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())) + } } for ts := ev.startTimestamp; ts <= ev.endTimestamp; ts += ev.interval { @@ -1337,7 +1352,7 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping var ws annotations.Annotations switch aggExpr.Op { case parser.TOPK, parser.BOTTOMK: - result, ws = ev.aggregationK(aggExpr, param, inputMatrix, seriesToResult, orderedResult, enh, seriess) + result, ws = ev.aggregationK(aggExpr, k, inputMatrix, seriesToResult, orderedResult, 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 @@ -2733,11 +2748,6 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix op := e.Op var annos annotations.Annotations seen := make([]bool, len(orderedResult)) // Which output groups were seen in the input at this timestamp. - if op == parser.QUANTILE { - if math.IsNaN(q) || q < 0 || q > 1 { - annos.Add(annotations.NewInvalidQuantileWarning(q, e.Param.PositionRange())) - } - } for si := range inputMatrix { f, h, ok := ev.nextValues(enh.Ts, &inputMatrix[si]) @@ -2919,21 +2929,11 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix // seriesToResult maps inputMatrix indexes to groups indexes. // For an instant query, returns a Matrix in descending order for topk or ascending for bottomk. // For a range query, aggregates output in the seriess map. -func (ev *evaluator) aggregationK(e *parser.AggregateExpr, q float64, inputMatrix Matrix, seriesToResult []int, orderedResult []*groupedAggregation, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { +func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Matrix, seriesToResult []int, orderedResult []*groupedAggregation, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { op := e.Op var s Sample var annos annotations.Annotations seen := make([]bool, len(orderedResult)) // Which output groups were seen in the input at this timestamp. - if !convertibleToInt64(q) { - ev.errorf("Scalar value %v overflows int64", q) - } - k := int(q) - if k > len(inputMatrix) { - k = len(inputMatrix) - } - if k < 1 { - return nil, annos - } for si := range inputMatrix { f, _, ok := ev.nextValues(enh.Ts, &inputMatrix[si]) From 2cf3c9de8f3fc0d88b42c13b072c7d3516de37e9 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 5 Apr 2024 14:39:29 +0100 Subject: [PATCH 117/127] promql: store labels per-group only for count_values This saves memory in other kinds of aggregation. We don't need `orderedResult` in `aggregationCountValues`; the ordering is not guaranteed. Signed-off-by: Bryan Boreham --- promql/engine.go | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 0dd33a7f94..b81617a1d1 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -2730,7 +2730,6 @@ func vectorElemBinop(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram type groupedAggregation struct { hasFloat bool // Has at least 1 float64 sample aggregated. hasHistogram bool // Has at least 1 histogram sample aggregated. - labels labels.Labels floatValue float64 histogramValue *histogram.FloatHistogram floatMean float64 @@ -3044,8 +3043,11 @@ func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Ma // aggregationK evaluates count_values on vec. // Outputs as many series per group as there are values in the input. func (ev *evaluator) aggregationCountValues(e *parser.AggregateExpr, grouping []string, valueLabel string, vec Vector, enh *EvalNodeHelper) (Vector, annotations.Annotations) { - result := map[uint64]*groupedAggregation{} - orderedResult := []*groupedAggregation{} + type groupCount struct { + labels labels.Labels + count int + } + result := map[uint64]*groupCount{} var buf []byte for _, s := range vec { @@ -3062,24 +3064,21 @@ func (ev *evaluator) aggregationCountValues(e *parser.AggregateExpr, grouping [] group, ok := result[groupingKey] // Add a new group if it doesn't exist. if !ok { - newAgg := &groupedAggregation{ - labels: generateGroupingLabels(enh, metric, e.Without, grouping), - groupCount: 1, + result[groupingKey] = &groupCount{ + labels: generateGroupingLabels(enh, metric, e.Without, grouping), + count: 1, } - - result[groupingKey] = newAgg - orderedResult = append(orderedResult, newAgg) continue } - group.groupCount++ + group.count++ } // Construct the result Vector from the aggregated groups. - for _, aggr := range orderedResult { + for _, aggr := range result { enh.Out = append(enh.Out, Sample{ Metric: aggr.labels, - F: float64(aggr.groupCount), + F: float64(aggr.count), }) } return enh.Out, nil From 5e3914a27cfbd32f4d54177ae896ccb65fe9cc4e Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Sat, 9 Mar 2024 10:23:31 +0000 Subject: [PATCH 118/127] promql: remove histogramMean from groupedAggregation Re-use histogramValue since we don't need them separately. Tidy up initialization. Signed-off-by: Bryan Boreham --- promql/engine.go | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index b81617a1d1..4230bff878 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -2733,7 +2733,6 @@ type groupedAggregation struct { floatValue float64 histogramValue *histogram.FloatHistogram floatMean float64 - histogramMean *histogram.FloatHistogram groupCount int heap vectorByValueHeap reverseHeap vectorByReverseValueHeap @@ -2762,20 +2761,14 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix floatMean: f, groupCount: 1, } - switch { - case h == nil: - group.hasFloat = true - case op == parser.SUM: - group.histogramValue = h.Copy() - group.hasHistogram = true - case op == parser.AVG: - group.histogramMean = h.Copy() - group.hasHistogram = true - case op == parser.STDVAR || op == parser.STDDEV: - group.groupCount = 0 - } - switch op { + case parser.SUM, parser.AVG: + if h == nil { + group.hasFloat = true + } else { + group.histogramValue = h.Copy() + group.hasHistogram = true + } case parser.STDVAR, parser.STDDEV: group.floatValue = 0 case parser.QUANTILE: @@ -2807,11 +2800,11 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix group.groupCount++ if h != nil { group.hasHistogram = true - if group.histogramMean != nil { + if group.histogramValue != nil { left := h.Copy().Div(float64(group.groupCount)) - right := group.histogramMean.Copy().Div(float64(group.groupCount)) + right := group.histogramValue.Copy().Div(float64(group.groupCount)) toAdd := left.Sub(right) - group.histogramMean.Add(toAdd) + group.histogramValue.Add(toAdd) } // Otherwise the aggregation contained floats // previously and will be invalid anyway. No @@ -2886,7 +2879,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix continue } if aggr.hasHistogram { - aggr.histogramValue = aggr.histogramMean.Compact(0) + aggr.histogramValue = aggr.histogramValue.Compact(0) } else { aggr.floatValue = aggr.floatMean } From cfbeb6681bfb2a7d51e3ce3f6ef891097547bf10 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Sat, 9 Mar 2024 10:51:16 +0000 Subject: [PATCH 119/127] promql: re-use one heap for topk and bottomk Slightly ugly casting saves memory. Signed-off-by: Bryan Boreham --- promql/engine.go | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 4230bff878..2819b36d02 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -2735,7 +2735,6 @@ type groupedAggregation struct { floatMean float64 groupCount int heap vectorByValueHeap - reverseHeap vectorByReverseValueHeap } // aggregation evaluates sum, avg, count, stdvar, stddev or quantile at one timestep on inputMatrix. @@ -2937,16 +2936,10 @@ func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Ma group := orderedResult[seriesToResult[si]] // Initialize this group if it's the first time we've seen it. if !seen[seriesToResult[si]] { - *group = groupedAggregation{} - - switch op { - case parser.TOPK: - group.heap = make(vectorByValueHeap, 1, k) - group.heap[0] = s - case parser.BOTTOMK: - group.reverseHeap = make(vectorByReverseValueHeap, 1, k) - group.reverseHeap[0] = s + *group = groupedAggregation{ + heap: make(vectorByValueHeap, 1, k), } + group.heap[0] = s seen[seriesToResult[si]] = true continue } @@ -2968,13 +2961,13 @@ func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Ma case parser.BOTTOMK: // We build a heap of up to k elements, with the biggest element at heap[0]. switch { - case len(group.reverseHeap) < k: - heap.Push(&group.reverseHeap, &s) - case group.reverseHeap[0].F > s.F || (math.IsNaN(group.reverseHeap[0].F) && !math.IsNaN(s.F)): + case len(group.heap) < k: + heap.Push((*vectorByReverseValueHeap)(&group.heap), &s) + case group.heap[0].F > s.F || (math.IsNaN(group.heap[0].F) && !math.IsNaN(s.F)): // This new element is smaller than the previous biggest element - overwrite that. - group.reverseHeap[0] = s + group.heap[0] = s if k > 1 { - heap.Fix(&group.reverseHeap, 0) // Maintain the heap invariant. + heap.Fix((*vectorByReverseValueHeap)(&group.heap), 0) // Maintain the heap invariant. } } @@ -3021,10 +3014,10 @@ func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Ma case parser.BOTTOMK: // The heap keeps the highest value on top, so reverse it. - if len(aggr.reverseHeap) > 1 { - sort.Sort(sort.Reverse(aggr.reverseHeap)) + if len(aggr.heap) > 1 { + sort.Sort(sort.Reverse((*vectorByReverseValueHeap)(&aggr.heap))) } - for _, v := range aggr.reverseHeap { + for _, v := range aggr.heap { add(v.Metric, v.F) } } From 7499d90913725e1d95f231a8584be34a10fafbce Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Sat, 9 Mar 2024 11:06:46 +0000 Subject: [PATCH 120/127] promql: remove pointer to aggregation groups Just allocate in one slice. Signed-off-by: Bryan Boreham --- promql/engine.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 2819b36d02..bd4c7ed467 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1298,9 +1298,9 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping buf := make([]byte, 0, 1024) groupToResultIndex := make(map[uint64]int) seriesToResult := make([]int, len(inputMatrix)) - orderedResult := make([]*groupedAggregation, 0, 16) var result Matrix + groupCount := 0 for si, series := range inputMatrix { var groupingKey uint64 groupingKey, buf = generateGroupingKey(series.Metric, sortedGrouping, aggExpr.Without, buf) @@ -1311,13 +1311,13 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping m := generateGroupingLabels(enh, series.Metric, aggExpr.Without, sortedGrouping) result = append(result, Series{Metric: m}) } - newAgg := &groupedAggregation{} - index = len(orderedResult) + index = groupCount groupToResultIndex[groupingKey] = index - orderedResult = append(orderedResult, newAgg) + groupCount++ } seriesToResult[si] = index } + groups := make([]groupedAggregation, groupCount) var k int var seriess map[uint64]Series @@ -1352,13 +1352,13 @@ func (ev *evaluator) rangeEvalAgg(aggExpr *parser.AggregateExpr, sortedGrouping var ws annotations.Annotations switch aggExpr.Op { case parser.TOPK, parser.BOTTOMK: - result, ws = ev.aggregationK(aggExpr, k, inputMatrix, seriesToResult, orderedResult, enh, seriess) + result, ws = ev.aggregationK(aggExpr, k, 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 } default: - ws = ev.aggregation(aggExpr, param, inputMatrix, result, seriesToResult, orderedResult, enh) + ws = ev.aggregation(aggExpr, param, inputMatrix, result, seriesToResult, groups, enh) } warnings.Merge(ws) @@ -2741,10 +2741,10 @@ type groupedAggregation struct { // These functions produce one output series for each group specified in the expression, with just the labels from `by(...)`. // outputMatrix should be already populated with grouping labels; groups is one-to-one with outputMatrix. // seriesToResult maps inputMatrix indexes to outputMatrix indexes. -func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix, outputMatrix Matrix, seriesToResult []int, orderedResult []*groupedAggregation, enh *EvalNodeHelper) annotations.Annotations { +func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix, outputMatrix Matrix, seriesToResult []int, groups []groupedAggregation, enh *EvalNodeHelper) annotations.Annotations { op := e.Op var annos annotations.Annotations - seen := make([]bool, len(orderedResult)) // Which output groups were seen in the input at this timestamp. + seen := make([]bool, len(groups)) // Which output groups were seen in the input at this timestamp. for si := range inputMatrix { f, h, ok := ev.nextValues(enh.Ts, &inputMatrix[si]) @@ -2752,7 +2752,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix continue } - group := orderedResult[seriesToResult[si]] + group := &groups[seriesToResult[si]] // Initialize this group if it's the first time we've seen it. if !seen[seriesToResult[si]] { *group = groupedAggregation{ @@ -2866,7 +2866,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix // Construct the output matrix from the aggregated groups. numSteps := int((ev.endTimestamp-ev.startTimestamp)/ev.interval) + 1 - for ri, aggr := range orderedResult { + for ri, aggr := range groups { if !seen[ri] { continue } @@ -2920,11 +2920,11 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix // seriesToResult maps inputMatrix indexes to groups indexes. // For an instant query, returns a Matrix in descending order for topk or ascending for bottomk. // For a range query, aggregates output in the seriess map. -func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Matrix, seriesToResult []int, orderedResult []*groupedAggregation, enh *EvalNodeHelper, seriess map[uint64]Series) (Matrix, annotations.Annotations) { +func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, 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 - seen := make([]bool, len(orderedResult)) // Which output groups were seen in the input at this timestamp. + seen := make([]bool, len(groups)) // Which output groups were seen in the input at this timestamp. for si := range inputMatrix { f, _, ok := ev.nextValues(enh.Ts, &inputMatrix[si]) @@ -2933,7 +2933,7 @@ func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Ma } s = Sample{Metric: inputMatrix[si].Metric, F: f} - group := orderedResult[seriesToResult[si]] + group := &groups[seriesToResult[si]] // Initialize this group if it's the first time we've seen it. if !seen[seriesToResult[si]] { *group = groupedAggregation{ @@ -2980,7 +2980,7 @@ func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Ma numSteps := int((ev.endTimestamp-ev.startTimestamp)/ev.interval) + 1 var mat Matrix if ev.endTimestamp == ev.startTimestamp { - mat = make(Matrix, 0, len(orderedResult)) + mat = make(Matrix, 0, len(groups)) } add := func(lbls labels.Labels, f float64) { @@ -2998,7 +2998,7 @@ func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Ma seriess[hash] = ss } } - for ri, aggr := range orderedResult { + for ri, aggr := range groups { if !seen[ri] { continue } From 0ac927515b193abdc65575f8a71a1138a1debf80 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Sat, 9 Mar 2024 11:24:32 +0000 Subject: [PATCH 121/127] promql: move group-seen into group struct Save allocating an auxilliary array. Signed-off-by: Bryan Boreham --- promql/engine.go | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index bd4c7ed467..c23964ed82 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -2728,6 +2728,7 @@ 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 @@ -2744,7 +2745,9 @@ type groupedAggregation struct { func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix, outputMatrix Matrix, seriesToResult []int, groups []groupedAggregation, enh *EvalNodeHelper) annotations.Annotations { op := e.Op var annos annotations.Annotations - seen := make([]bool, len(groups)) // Which output groups were seen in the input at this timestamp. + for i := range groups { + groups[i].seen = false + } for si := range inputMatrix { f, h, ok := ev.nextValues(enh.Ts, &inputMatrix[si]) @@ -2754,8 +2757,9 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix group := &groups[seriesToResult[si]] // Initialize this group if it's the first time we've seen it. - if !seen[seriesToResult[si]] { + if !group.seen { *group = groupedAggregation{ + seen: true, floatValue: f, floatMean: f, groupCount: 1, @@ -2776,7 +2780,6 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix case parser.GROUP: group.floatValue = 1 } - seen[seriesToResult[si]] = true continue } @@ -2867,7 +2870,7 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix numSteps := int((ev.endTimestamp-ev.startTimestamp)/ev.interval) + 1 for ri, aggr := range groups { - if !seen[ri] { + if !aggr.seen { continue } switch op { @@ -2924,7 +2927,9 @@ func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Ma op := e.Op var s Sample var annos annotations.Annotations - seen := make([]bool, len(groups)) // Which output groups were seen in the input at this timestamp. + for i := range groups { + groups[i].seen = false + } for si := range inputMatrix { f, _, ok := ev.nextValues(enh.Ts, &inputMatrix[si]) @@ -2935,12 +2940,12 @@ 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 !seen[seriesToResult[si]] { + if !group.seen { *group = groupedAggregation{ + seen: true, heap: make(vectorByValueHeap, 1, k), } group.heap[0] = s - seen[seriesToResult[si]] = true continue } @@ -2998,8 +3003,8 @@ func (ev *evaluator) aggregationK(e *parser.AggregateExpr, k int, inputMatrix Ma seriess[hash] = ss } } - for ri, aggr := range groups { - if !seen[ri] { + for _, aggr := range groups { + if !aggr.seen { continue } switch op { From 12961c6a373fe9d19f97a7bff9cac6587896eec2 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 5 Apr 2024 15:40:07 +0100 Subject: [PATCH 122/127] promql: refactor: eliminate one 'else' Signed-off-by: Bryan Boreham --- promql/engine.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index c23964ed82..b8a8ea0959 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -3081,12 +3081,12 @@ func addToSeries(ss *Series, ts int64, f float64, h *histogram.FloatHistogram, n ss.Floats = getFPointSlice(numSteps) } ss.Floats = append(ss.Floats, FPoint{T: ts, F: f}) - } else { - if ss.Histograms == nil { - ss.Histograms = getHPointSlice(numSteps) - } - ss.Histograms = append(ss.Histograms, HPoint{T: ts, H: h}) + return } + if ss.Histograms == nil { + ss.Histograms = getHPointSlice(numSteps) + } + ss.Histograms = append(ss.Histograms, HPoint{T: ts, H: h}) } func (ev *evaluator) nextValues(ts int64, series *Series) (f float64, h *histogram.FloatHistogram, b bool) { From c755fa99357b9e81e4a2979365865c63fe020710 Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Fri, 5 Apr 2024 16:00:52 +0000 Subject: [PATCH 123/127] support unregistering scrape manager metrics Signed-off-by: David Ashpole --- scrape/manager.go | 5 +++++ scrape/manager_test.go | 13 +++++++++++++ scrape/metrics.go | 29 ++++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/scrape/manager.go b/scrape/manager.go index 3ad315a50a..e805fd43af 100644 --- a/scrape/manager.go +++ b/scrape/manager.go @@ -129,6 +129,11 @@ func (m *Manager) Run(tsets <-chan map[string][]*targetgroup.Group) error { } } +// UnregisterMetrics unregisters manager metrics. +func (m *Manager) UnregisterMetrics() { + m.metrics.Unregister() +} + func (m *Manager) reloader() { reloadIntervalDuration := m.opts.DiscoveryReloadInterval if reloadIntervalDuration < model.Duration(5*time.Second) { diff --git a/scrape/manager_test.go b/scrape/manager_test.go index f90fd0ce66..51af45f8cf 100644 --- a/scrape/manager_test.go +++ b/scrape/manager_test.go @@ -857,3 +857,16 @@ func getResultFloats(app *collectResultAppender, expectedMetricName string) (res } return result } + +func TestUnregisterMetrics(t *testing.T) { + reg := prometheus.NewRegistry() + // Check that all metrics can be unregistered, allowing a second manager to be created. + for i := 0; i < 2; i++ { + opts := Options{} + manager, err := NewManager(&opts, nil, nil, reg) + require.NotNil(t, manager) + require.NoError(t, err) + // Unregister all metrics. + manager.UnregisterMetrics() + } +} diff --git a/scrape/metrics.go b/scrape/metrics.go index 7082bc743b..b67d0686b6 100644 --- a/scrape/metrics.go +++ b/scrape/metrics.go @@ -20,6 +20,7 @@ import ( ) type scrapeMetrics struct { + reg prometheus.Registerer // Used by Manager. targetMetadataCache *MetadataMetricsCollector targetScrapePools prometheus.Counter @@ -54,7 +55,7 @@ type scrapeMetrics struct { } func newScrapeMetrics(reg prometheus.Registerer) (*scrapeMetrics, error) { - sm := &scrapeMetrics{} + sm := &scrapeMetrics{reg: reg} // Manager metrics. sm.targetMetadataCache = &MetadataMetricsCollector{ @@ -260,6 +261,32 @@ func (sm *scrapeMetrics) setTargetMetadataCacheGatherer(gatherer TargetsGatherer sm.targetMetadataCache.TargetsGatherer = gatherer } +// Unregister unregisters all metrics. +func (sm *scrapeMetrics) Unregister() { + sm.reg.Unregister(sm.targetMetadataCache) + sm.reg.Unregister(sm.targetScrapePools) + sm.reg.Unregister(sm.targetScrapePoolsFailed) + sm.reg.Unregister(sm.targetReloadIntervalLength) + sm.reg.Unregister(sm.targetScrapePoolReloads) + sm.reg.Unregister(sm.targetScrapePoolReloadsFailed) + sm.reg.Unregister(sm.targetSyncIntervalLength) + sm.reg.Unregister(sm.targetScrapePoolSyncsCounter) + sm.reg.Unregister(sm.targetScrapePoolExceededTargetLimit) + sm.reg.Unregister(sm.targetScrapePoolTargetLimit) + sm.reg.Unregister(sm.targetScrapePoolTargetsAdded) + sm.reg.Unregister(sm.targetSyncFailed) + sm.reg.Unregister(sm.targetScrapeExceededBodySizeLimit) + sm.reg.Unregister(sm.targetScrapeCacheFlushForced) + sm.reg.Unregister(sm.targetIntervalLength) + sm.reg.Unregister(sm.targetScrapeSampleLimit) + sm.reg.Unregister(sm.targetScrapeSampleDuplicate) + sm.reg.Unregister(sm.targetScrapeSampleOutOfOrder) + sm.reg.Unregister(sm.targetScrapeSampleOutOfBounds) + sm.reg.Unregister(sm.targetScrapeExemplarOutOfOrder) + sm.reg.Unregister(sm.targetScrapePoolExceededLabelLimits) + sm.reg.Unregister(sm.targetScrapeNativeHistogramBucketLimit) +} + type TargetsGatherer interface { TargetsActive() map[string][]*Target } From 277f04f0c44219bb4158b021c7038015410c9745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sun, 7 Apr 2024 18:28:28 +0100 Subject: [PATCH 124/127] Stop compactions if there's a block to write (#13754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Stop compactions if there's a block to write db.Compact() checks if there's a block to write with HEAD chunks before calling db.compactBlocks(). This is to ensure that if we need to write a block then it happens ASAP, otherwise memory usage might keep growing. But what can also happen is that we don't need to write any block, we start db.compactBlocks(), compaction takes hours, and in the meantime HEAD needs to write out chunks to a block. This can be especially problematic if, for example, you run Thanos sidecar that's uploading block, which requires that compactions are disabled. Then you disable Thanos sidecar and re-enable compactions. When db.compactBlocks() is finally called it might have a huge number of blocks to compact, which might take a very long time, during which HEAD cannot write out chunks to a new block. In such case memory usage will keep growing until either: - compactions are finally finished and HEAD can write a block - we run out of memory and Prometheus gets OOM-killed This change adds a check for pending HEAD block writes inside db.compactBlocks(), so that we bail out early if there are still compactions to run, but we also need to write a new block. Also add a test for compactBlocks. --------- Signed-off-by: Ɓukasz Mierzwa Signed-off-by: Lukasz Mierzwa --- tsdb/db.go | 8 +++++++ tsdb/db_test.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/tsdb/db.go b/tsdb/db.go index 293ba646ea..5078f447c4 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -1365,6 +1365,14 @@ func (db *DB) compactHead(head *RangeHead) error { func (db *DB) compactBlocks() (err error) { // Check for compactions of multiple blocks. for { + // If we have a lot of blocks to compact the whole process might take + // long enough that we end up with a HEAD block that needs to be written. + // Check if that's the case and stop compactions early. + if db.head.compactable() { + level.Warn(db.logger).Log("msg", "aborting block compactions to persit the head block") + return nil + } + plan, err := db.compactor.Plan(db.dir) if err != nil { return fmt.Errorf("plan compaction: %w", err) diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 8b1ad106bf..ba60d83eaf 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -6986,3 +6986,63 @@ func requireEqualOOOSamples(t *testing.T, expectedSamples int, db *DB) { 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) + writeFn func() (ulid.ULID, error) +} + +func (c *mockCompactorFn) Plan(_ string) ([]string, error) { + return c.planFn() +} + +func (c *mockCompactorFn) Compact(_ string, _ []string, _ []*Block) (ulid.ULID, error) { + return c.compactFn() +} + +func (c *mockCompactorFn) Write(_ string, _ BlockReader, _, _ int64, _ *BlockMeta) (ulid.ULID, error) { + return c.writeFn() +} + +// Regression test for https://github.com/prometheus/prometheus/pull/13754 +func TestAbortBlockCompactions(t *testing.T) { + // Create a test DB + db := openTestDB(t, nil, nil) + defer func() { + require.NoError(t, db.Close()) + }() + // It should NOT be compactible at the beginning of the test + require.False(t, db.head.compactable(), "head should NOT be compactable") + + // Track the number of compactions run inside db.compactBlocks() + var compactions int + + // Use a mock compactor with custom Plan() implementation + db.compactor = &mockCompactorFn{ + planFn: func() ([]string, error) { + // On every Plan() run increment compactions. After 4 compactions + // update HEAD to make it compactible to force an exit from db.compactBlocks() loop. + compactions++ + if compactions > 3 { + chunkRange := db.head.chunkRange.Load() + db.head.minTime.Store(0) + db.head.maxTime.Store(chunkRange * 2) + require.True(t, db.head.compactable(), "head should be compactable") + } + // Our custom Plan() will always return something to compact. + return []string{"1", "2", "3"}, nil + }, + compactFn: func() (ulid.ULID, error) { + return ulid.ULID{}, nil + }, + writeFn: func() (ulid.ULID, error) { + return ulid.ULID{}, nil + }, + } + + err := db.Compact(context.Background()) + require.NoError(t, err) + require.True(t, db.head.compactable(), "head should be compactable") + require.Equal(t, 4, compactions, "expected 4 compactions to be completed") +} From c14a158d03e8a9c25dcc4b0c3bf153cf6486880f Mon Sep 17 00:00:00 2001 From: hanghuge Date: Sat, 6 Apr 2024 19:48:24 +0800 Subject: [PATCH 125/127] Signed-off-by: hanghuge Fix unavailable link Signed-off-by: hanghuge --- discovery/kubernetes/kubernetes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discovery/kubernetes/kubernetes.go b/discovery/kubernetes/kubernetes.go index 94058aa04e..a8b6f85899 100644 --- a/discovery/kubernetes/kubernetes.go +++ b/discovery/kubernetes/kubernetes.go @@ -311,7 +311,7 @@ func New(l log.Logger, metrics discovery.DiscovererMetrics, conf *SDConfig) (*Di } case conf.APIServer.URL == nil: // Use the Kubernetes provided pod service account - // as described in https://kubernetes.io/docs/admin/service-accounts-admin/ + // as described in https://kubernetes.io/docs/tasks/run-application/access-api-from-pod/#using-official-client-libraries kcfg, err = rest.InClusterConfig() if err != nil { return nil, err From 633224886a1c975dd3a8a8308a0b1d630048a21c Mon Sep 17 00:00:00 2001 From: Jonathan Halterman Date: Mon, 8 Apr 2024 08:34:14 -0700 Subject: [PATCH 126/127] Write out of order hint when initially creating meta file (#13894) Signed-off-by: Jonathan Halterman Signed-off-by: Jonathan Halterman Co-authored-by: Jesus Vazquez --- tsdb/compact.go | 11 +++++++---- tsdb/db.go | 14 +++----------- tsdb/db_test.go | 2 +- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/tsdb/compact.go b/tsdb/compact.go index 113ef11b4c..e09039cf33 100644 --- a/tsdb/compact.go +++ b/tsdb/compact.go @@ -60,7 +60,7 @@ type Compactor interface { // Write persists a Block into a directory. // No Block is written when resulting Block has 0 samples, and returns empty ulid.ULID{}. - Write(dest string, b BlockReader, mint, maxt int64, parent *BlockMeta) (ulid.ULID, error) + Write(dest string, b BlockReader, mint, maxt int64, base *BlockMeta) (ulid.ULID, error) // Compact runs compaction against the provided directories. Must // only be called concurrently with results of Plan(). @@ -536,7 +536,7 @@ func (c *LeveledCompactor) CompactWithBlockPopulator(dest string, dirs []string, return uid, errs.Err() } -func (c *LeveledCompactor) Write(dest string, b BlockReader, mint, maxt int64, parent *BlockMeta) (ulid.ULID, error) { +func (c *LeveledCompactor) Write(dest string, b BlockReader, mint, maxt int64, base *BlockMeta) (ulid.ULID, error) { start := time.Now() uid := ulid.MustNew(ulid.Now(), rand.Reader) @@ -549,9 +549,12 @@ func (c *LeveledCompactor) Write(dest string, b BlockReader, mint, maxt int64, p meta.Compaction.Level = 1 meta.Compaction.Sources = []ulid.ULID{uid} - if parent != nil { + if base != nil { meta.Compaction.Parents = []BlockDesc{ - {ULID: parent.ULID, MinTime: parent.MinTime, MaxTime: parent.MaxTime}, + {ULID: base.ULID, MinTime: base.MinTime, MaxTime: base.MaxTime}, + } + if base.Compaction.FromOutOfOrder() { + meta.Compaction.SetOutOfOrder() } } diff --git a/tsdb/db.go b/tsdb/db.go index 5078f447c4..d675635d7a 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -1299,25 +1299,17 @@ func (db *DB) compactOOO(dest string, oooHead *OOOCompactionHead) (_ []ulid.ULID } }() + meta := &BlockMeta{} + meta.Compaction.SetOutOfOrder() for t := blockSize * (oooHeadMint / blockSize); t <= oooHeadMaxt; t += blockSize { mint, maxt := t, t+blockSize // Block intervals are half-open: [b.MinTime, b.MaxTime). Block intervals are always +1 than the total samples it includes. - uid, err := db.compactor.Write(dest, oooHead.CloneForTimeRange(mint, maxt-1), mint, maxt, nil) + uid, err := db.compactor.Write(dest, oooHead.CloneForTimeRange(mint, maxt-1), mint, maxt, meta) if err != nil { return nil, err } if uid.Compare(ulid.ULID{}) != 0 { ulids = append(ulids, uid) - blockDir := filepath.Join(dest, uid.String()) - meta, _, err := readMetaFile(blockDir) - if err != nil { - return ulids, fmt.Errorf("read meta: %w", err) - } - meta.Compaction.SetOutOfOrder() - _, err = writeMetaFile(db.logger, blockDir, meta) - if err != nil { - return ulids, fmt.Errorf("write meta: %w", err) - } } } diff --git a/tsdb/db_test.go b/tsdb/db_test.go index ba60d83eaf..71b2f05ac7 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -1441,7 +1441,7 @@ func (c *mockCompactorFailing) Write(dest string, _ BlockReader, _, _ int64, _ * c.blocks = append(c.blocks, block) // Now check that all expected blocks are actually persisted on disk. - // This way we make sure that the we have some blocks that are supposed to be removed. + // This way we make sure that we have some blocks that are supposed to be removed. var expectedBlocks []string for _, b := range c.blocks { expectedBlocks = append(expectedBlocks, filepath.Join(dest, b.Meta().ULID.String())) From d496687c8e7900e127f50ad940c9d8e580eaf212 Mon Sep 17 00:00:00 2001 From: Matthieu MOREL Date: Mon, 8 Apr 2024 19:26:23 +0000 Subject: [PATCH 127/127] golangci-lint: enable usestdlibvars linter Signed-off-by: Matthieu MOREL --- .golangci.yml | 1 + cmd/promtool/main.go | 2 +- discovery/eureka/client.go | 2 +- discovery/hetzner/robot.go | 2 +- discovery/http/http.go | 2 +- discovery/marathon/marathon.go | 2 +- discovery/openstack/mock_test.go | 6 +++--- discovery/puppetdb/puppetdb.go | 2 +- discovery/triton/triton.go | 2 +- discovery/xds/client.go | 2 +- .../remote_storage_adapter/influxdb/client_test.go | 2 +- .../remote_storage_adapter/opentsdb/client.go | 2 +- model/labels/labels_test.go | 3 ++- notifier/notifier.go | 2 +- scrape/scrape.go | 2 +- storage/remote/client.go | 4 ++-- storage/remote/read_handler_test.go | 6 +++--- util/httputil/compression_test.go | 4 ++-- util/httputil/cors_test.go | 4 ++-- web/api/v1/api_test.go | 4 ++-- web/api/v1/errors_test.go | 2 +- web/federate_test.go | 6 +++--- web/web_test.go | 4 ++-- 23 files changed, 35 insertions(+), 33 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 42410aebf8..f350aed6dd 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -26,6 +26,7 @@ linters: - testifylint - unconvert - unused + - usestdlibvars issues: max-same-issues: 0 diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go index 47bf02c104..a62ae4fbf4 100644 --- a/cmd/promtool/main.go +++ b/cmd/promtool/main.go @@ -482,7 +482,7 @@ func CheckServerStatus(serverURL *url.URL, checkEndpoint string, roundTripper ht return err } - request, err := http.NewRequest("GET", config.Address, nil) + request, err := http.NewRequest(http.MethodGet, config.Address, nil) if err != nil { return err } diff --git a/discovery/eureka/client.go b/discovery/eureka/client.go index a833415a53..52e8ce7b48 100644 --- a/discovery/eureka/client.go +++ b/discovery/eureka/client.go @@ -81,7 +81,7 @@ const appListPath string = "/apps" func fetchApps(ctx context.Context, server string, client *http.Client) (*Applications, error) { url := fmt.Sprintf("%s%s", server, appListPath) - request, err := http.NewRequest("GET", url, nil) + request, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } diff --git a/discovery/hetzner/robot.go b/discovery/hetzner/robot.go index 1d8aa9302f..2cf6006f80 100644 --- a/discovery/hetzner/robot.go +++ b/discovery/hetzner/robot.go @@ -70,7 +70,7 @@ func newRobotDiscovery(conf *SDConfig, _ log.Logger) (*robotDiscovery, error) { } func (d *robotDiscovery) refresh(context.Context) ([]*targetgroup.Group, error) { - req, err := http.NewRequest("GET", d.endpoint+"/server", nil) + req, err := http.NewRequest(http.MethodGet, d.endpoint+"/server", nil) if err != nil { return nil, err } diff --git a/discovery/http/http.go b/discovery/http/http.go index 8dd21ec9e4..ff76fd7627 100644 --- a/discovery/http/http.go +++ b/discovery/http/http.go @@ -150,7 +150,7 @@ func NewDiscovery(conf *SDConfig, logger log.Logger, clientOpts []config.HTTPCli } func (d *Discovery) Refresh(ctx context.Context) ([]*targetgroup.Group, error) { - req, err := http.NewRequest("GET", d.url, nil) + req, err := http.NewRequest(http.MethodGet, d.url, nil) if err != nil { return nil, err } diff --git a/discovery/marathon/marathon.go b/discovery/marathon/marathon.go index ecad108e4a..f833af47e7 100644 --- a/discovery/marathon/marathon.go +++ b/discovery/marathon/marathon.go @@ -339,7 +339,7 @@ type appListClient func(ctx context.Context, client *http.Client, url string) (* // fetchApps requests a list of applications from a marathon server. func fetchApps(ctx context.Context, client *http.Client, url string) (*appList, error) { - request, err := http.NewRequest("GET", url, nil) + request, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } diff --git a/discovery/openstack/mock_test.go b/discovery/openstack/mock_test.go index 4aa871e11f..b1267db90e 100644 --- a/discovery/openstack/mock_test.go +++ b/discovery/openstack/mock_test.go @@ -239,7 +239,7 @@ const hypervisorListBody = ` // HandleHypervisorListSuccessfully mocks os-hypervisors detail call. func (m *SDMock) HandleHypervisorListSuccessfully() { m.Mux.HandleFunc("/os-hypervisors/detail", func(w http.ResponseWriter, r *http.Request) { - testMethod(m.t, r, "GET") + testMethod(m.t, r, http.MethodGet) testHeader(m.t, r, "X-Auth-Token", tokenID) w.Header().Add("Content-Type", "application/json") @@ -536,7 +536,7 @@ const serverListBody = ` // HandleServerListSuccessfully mocks server detail call. func (m *SDMock) HandleServerListSuccessfully() { m.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) { - testMethod(m.t, r, "GET") + testMethod(m.t, r, http.MethodGet) testHeader(m.t, r, "X-Auth-Token", tokenID) w.Header().Add("Content-Type", "application/json") @@ -575,7 +575,7 @@ const listOutput = ` // HandleFloatingIPListSuccessfully mocks floating ips call. func (m *SDMock) HandleFloatingIPListSuccessfully() { m.Mux.HandleFunc("/os-floating-ips", func(w http.ResponseWriter, r *http.Request) { - testMethod(m.t, r, "GET") + testMethod(m.t, r, http.MethodGet) testHeader(m.t, r, "X-Auth-Token", tokenID) w.Header().Add("Content-Type", "application/json") diff --git a/discovery/puppetdb/puppetdb.go b/discovery/puppetdb/puppetdb.go index 3f9ad1f113..8c9ccde0a4 100644 --- a/discovery/puppetdb/puppetdb.go +++ b/discovery/puppetdb/puppetdb.go @@ -189,7 +189,7 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { return nil, err } - req, err := http.NewRequest("POST", d.url, bytes.NewBuffer(bodyBytes)) + req, err := http.NewRequest(http.MethodPost, d.url, bytes.NewBuffer(bodyBytes)) if err != nil { return nil, err } diff --git a/discovery/triton/triton.go b/discovery/triton/triton.go index e56b7951b6..675149f2a3 100644 --- a/discovery/triton/triton.go +++ b/discovery/triton/triton.go @@ -211,7 +211,7 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { endpoint = fmt.Sprintf("%s?groups=%s", endpoint, groups) } - req, err := http.NewRequest("GET", endpoint, nil) + req, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { return nil, err } diff --git a/discovery/xds/client.go b/discovery/xds/client.go index 9844c6d7ed..027ceb2715 100644 --- a/discovery/xds/client.go +++ b/discovery/xds/client.go @@ -179,7 +179,7 @@ func (rc *HTTPResourceClient) Fetch(ctx context.Context) (*v3.DiscoveryResponse, return nil, err } - request, err := http.NewRequest("POST", rc.endpoint, bytes.NewBuffer(reqBody)) + request, err := http.NewRequest(http.MethodPost, rc.endpoint, bytes.NewBuffer(reqBody)) if err != nil { return nil, err } diff --git a/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client_test.go b/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client_test.go index cb56514e4b..a738c01dcd 100644 --- a/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client_test.go +++ b/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client_test.go @@ -74,7 +74,7 @@ testmetric,test_label=test_label_value2 value=5.1234 123456789123 server := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method, "Unexpected method.") + require.Equal(t, http.MethodPost, r.Method, "Unexpected method.") require.Equal(t, "/write", r.URL.Path, "Unexpected path.") b, err := io.ReadAll(r.Body) require.NoError(t, err, "Error reading body.") diff --git a/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/client.go b/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/client.go index 0fa7c5a4b7..abb1d0b7d3 100644 --- a/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/client.go +++ b/documentation/examples/remote_storage/remote_storage_adapter/opentsdb/client.go @@ -105,7 +105,7 @@ func (c *Client) Write(samples model.Samples) error { ctx, cancel := context.WithTimeout(context.Background(), c.timeout) defer cancel() - req, err := http.NewRequest("POST", u.String(), bytes.NewBuffer(buf)) + req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewBuffer(buf)) if err != nil { return err } diff --git a/model/labels/labels_test.go b/model/labels/labels_test.go index 5ec7764ca9..3d6e7659f4 100644 --- a/model/labels/labels_test.go +++ b/model/labels/labels_test.go @@ -16,6 +16,7 @@ package labels import ( "encoding/json" "fmt" + "net/http" "strings" "testing" @@ -810,7 +811,7 @@ var benchmarkLabels = []Label{ {"job", "node"}, {"instance", "123.123.1.211:9090"}, {"path", "/api/v1/namespaces//deployments/"}, - {"method", "GET"}, + {"method", http.MethodGet}, {"namespace", "system"}, {"status", "500"}, {"prometheus", "prometheus-core-1"}, diff --git a/notifier/notifier.go b/notifier/notifier.go index d1832402f7..53d3c04198 100644 --- a/notifier/notifier.go +++ b/notifier/notifier.go @@ -590,7 +590,7 @@ func labelsToOpenAPILabelSet(modelLabelSet labels.Labels) models.LabelSet { } func (n *Manager) sendOne(ctx context.Context, c *http.Client, url string, b []byte) error { - req, err := http.NewRequest("POST", url, bytes.NewReader(b)) + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(b)) if err != nil { return err } diff --git a/scrape/scrape.go b/scrape/scrape.go index 734c248130..4bbeab57a7 100644 --- a/scrape/scrape.go +++ b/scrape/scrape.go @@ -726,7 +726,7 @@ var UserAgent = fmt.Sprintf("Prometheus/%s", version.Version) func (s *targetScraper) scrape(ctx context.Context) (*http.Response, error) { if s.req == nil { - req, err := http.NewRequest("GET", s.URL().String(), nil) + req, err := http.NewRequest(http.MethodGet, s.URL().String(), nil) if err != nil { return nil, err } diff --git a/storage/remote/client.go b/storage/remote/client.go index 5ba0f7117f..140194ec71 100644 --- a/storage/remote/client.go +++ b/storage/remote/client.go @@ -199,7 +199,7 @@ type RecoverableError struct { // Store sends a batch of samples to the HTTP endpoint, the request is the proto marshalled // and encoded bytes from codec.go. func (c *Client) Store(ctx context.Context, req []byte, attempt int) error { - httpReq, err := http.NewRequest("POST", c.urlString, bytes.NewReader(req)) + httpReq, err := http.NewRequest(http.MethodPost, c.urlString, bytes.NewReader(req)) if err != nil { // Errors from NewRequest are from unparsable URLs, so are not // recoverable. @@ -290,7 +290,7 @@ func (c *Client) Read(ctx context.Context, query *prompb.Query) (*prompb.QueryRe } compressed := snappy.Encode(nil, data) - httpReq, err := http.NewRequest("POST", c.urlString, bytes.NewReader(compressed)) + httpReq, err := http.NewRequest(http.MethodPost, c.urlString, bytes.NewReader(compressed)) if err != nil { return nil, fmt.Errorf("unable to create request: %w", err) } diff --git a/storage/remote/read_handler_test.go b/storage/remote/read_handler_test.go index e83a0cb21a..e8e0ecb8df 100644 --- a/storage/remote/read_handler_test.go +++ b/storage/remote/read_handler_test.go @@ -75,7 +75,7 @@ func TestSampledReadEndpoint(t *testing.T) { require.NoError(t, err) compressed := snappy.Encode(nil, data) - request, err := http.NewRequest("POST", "", bytes.NewBuffer(compressed)) + request, err := http.NewRequest(http.MethodPost, "", bytes.NewBuffer(compressed)) require.NoError(t, err) recorder := httptest.NewRecorder() @@ -170,7 +170,7 @@ func BenchmarkStreamReadEndpoint(b *testing.B) { for i := 0; i < b.N; i++ { compressed := snappy.Encode(nil, data) - request, err := http.NewRequest("POST", "", bytes.NewBuffer(compressed)) + request, err := http.NewRequest(http.MethodPost, "", bytes.NewBuffer(compressed)) require.NoError(b, err) recorder := httptest.NewRecorder() @@ -268,7 +268,7 @@ func TestStreamReadEndpoint(t *testing.T) { require.NoError(t, err) compressed := snappy.Encode(nil, data) - request, err := http.NewRequest("POST", "", bytes.NewBuffer(compressed)) + request, err := http.NewRequest(http.MethodPost, "", bytes.NewBuffer(compressed)) require.NoError(t, err) recorder := httptest.NewRecorder() diff --git a/util/httputil/compression_test.go b/util/httputil/compression_test.go index 2db6810bd7..e166c7de79 100644 --- a/util/httputil/compression_test.go +++ b/util/httputil/compression_test.go @@ -85,7 +85,7 @@ func TestCompressionHandler_Gzip(t *testing.T) { }, } - req, _ := http.NewRequest("GET", server.URL+"/foo_endpoint", nil) + req, _ := http.NewRequest(http.MethodGet, server.URL+"/foo_endpoint", nil) req.Header.Set(acceptEncodingHeader, gzipEncoding) resp, err := client.Do(req) @@ -120,7 +120,7 @@ func TestCompressionHandler_Deflate(t *testing.T) { }, } - req, _ := http.NewRequest("GET", server.URL+"/foo_endpoint", nil) + req, _ := http.NewRequest(http.MethodGet, server.URL+"/foo_endpoint", nil) req.Header.Set(acceptEncodingHeader, deflateEncoding) resp, err := client.Do(req) diff --git a/util/httputil/cors_test.go b/util/httputil/cors_test.go index cfa2400405..657443ece0 100644 --- a/util/httputil/cors_test.go +++ b/util/httputil/cors_test.go @@ -41,7 +41,7 @@ func TestCORSHandler(t *testing.T) { dummyOrigin := "https://foo.com" // OPTIONS with legit origin - req, err := http.NewRequest("OPTIONS", server.URL+"/any_path", nil) + req, err := http.NewRequest(http.MethodOptions, server.URL+"/any_path", nil) require.NoError(t, err, "could not create request") req.Header.Set("Origin", dummyOrigin) @@ -53,7 +53,7 @@ func TestCORSHandler(t *testing.T) { require.Equal(t, dummyOrigin, AccessControlAllowOrigin, "expected Access-Control-Allow-Origin header") // OPTIONS with bad origin - req, err = http.NewRequest("OPTIONS", server.URL+"/any_path", nil) + req, err = http.NewRequest(http.MethodOptions, server.URL+"/any_path", nil) require.NoError(t, err, "could not create request") req.Header.Set("Origin", "https://not-foo.com") diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index 4158e544ef..c383993815 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -3354,7 +3354,7 @@ func TestParseTimeParam(t *testing.T) { } for _, test := range tests { - req, err := http.NewRequest("GET", "localhost:42/foo?"+test.paramName+"="+test.paramValue, nil) + req, err := http.NewRequest(http.MethodGet, "localhost:42/foo?"+test.paramName+"="+test.paramValue, nil) require.NoError(t, err) result := test.result @@ -3491,7 +3491,7 @@ func TestOptionsMethod(t *testing.T) { s := httptest.NewServer(r) defer s.Close() - req, err := http.NewRequest("OPTIONS", s.URL+"/any_path", nil) + req, err := http.NewRequest(http.MethodOptions, s.URL+"/any_path", nil) require.NoError(t, err, "Error creating OPTIONS request") client := &http.Client{} resp, err := client.Do(req) diff --git a/web/api/v1/errors_test.go b/web/api/v1/errors_test.go index b6ec7d4e1f..e76a1a3d35 100644 --- a/web/api/v1/errors_test.go +++ b/web/api/v1/errors_test.go @@ -89,7 +89,7 @@ func TestApiStatusCodes(t *testing.T) { r := createPrometheusAPI(q) rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/api/v1/query?query=up", nil) + req := httptest.NewRequest(http.MethodGet, "/api/v1/query?query=up", nil) r.ServeHTTP(rec, req) diff --git a/web/federate_test.go b/web/federate_test.go index 16637f60a3..f201210ec0 100644 --- a/web/federate_test.go +++ b/web/federate_test.go @@ -224,7 +224,7 @@ func TestFederation(t *testing.T) { for name, scenario := range scenarios { t.Run(name, func(t *testing.T) { h.config.GlobalConfig.ExternalLabels = scenario.externalLabels - req := httptest.NewRequest("GET", "http://example.org/federate?"+scenario.params, nil) + req := httptest.NewRequest(http.MethodGet, "http://example.org/federate?"+scenario.params, nil) res := httptest.NewRecorder() h.federation(res, req) @@ -265,7 +265,7 @@ func TestFederation_NotReady(t *testing.T) { }, } - req := httptest.NewRequest("GET", "http://example.org/federate?"+scenario.params, nil) + req := httptest.NewRequest(http.MethodGet, "http://example.org/federate?"+scenario.params, nil) res := httptest.NewRecorder() h.federation(res, req) @@ -381,7 +381,7 @@ func TestFederationWithNativeHistograms(t *testing.T) { }, } - req := httptest.NewRequest("GET", "http://example.org/federate?match[]=test_metric", nil) + req := httptest.NewRequest(http.MethodGet, "http://example.org/federate?match[]=test_metric", nil) req.Header.Add("Accept", `application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited,application/openmetrics-text;version=1.0.0;q=0.8,application/openmetrics-text;version=0.0.1;q=0.75,text/plain;version=0.0.4;q=0.5,*/*;q=0.1`) res := httptest.NewRecorder() diff --git a/web/web_test.go b/web/web_test.go index 62bdb2ae31..e1fa66fa8b 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -311,7 +311,7 @@ func TestDebugHandler(t *testing.T) { w := httptest.NewRecorder() - req, err := http.NewRequest("GET", tc.url, nil) + req, err := http.NewRequest(http.MethodGet, tc.url, nil) require.NoError(t, err) @@ -335,7 +335,7 @@ func TestHTTPMetrics(t *testing.T) { t.Helper() w := httptest.NewRecorder() - req, err := http.NewRequest("GET", "/-/ready", nil) + req, err := http.NewRequest(http.MethodGet, "/-/ready", nil) require.NoError(t, err) handler.router.ServeHTTP(w, req)