From 2c3e9e2e8723bf0edb4e82e89c344b1e17bbeef2 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Sat, 16 May 2015 13:33:03 +0200 Subject: [PATCH 01/13] Extract AND operation into own eval method. --- promql/engine.go | 58 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 7b53f661d..917661208 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -592,6 +592,9 @@ func (ev *evaluator) eval(expr Expr) Value { } case lt == ExprVector && rt == ExprVector: + if e.Op == itemLAND { + return ev.vectorAnd(lhs.(Vector), rhs.(Vector), e.VectorMatching) + } return ev.vectorBinop(e.Op, lhs.(Vector), rhs.(Vector), e.VectorMatching) case lt == ExprVector && rt == ExprScalar: @@ -698,6 +701,38 @@ func (ev *evaluator) matrixSelectorBounds(node *MatrixSelector) Matrix { return Matrix(sampleStreams) } +func (ev *evaluator) vectorAnd(lhs, rhs Vector, matching *VectorMatching) Vector { + if matching.Card != CardManyToMany { + panic("logical operations must always be many-to-many matching") + } + // If no matching labels are specified, match by all labels. + signature := func(m clientmodel.COWMetric) uint64 { + return clientmodel.SignatureForLabels(m.Metric, matching.On) + } + if len(matching.On) == 0 { + signature = func(m clientmodel.COWMetric) uint64 { + m.Delete(clientmodel.MetricNameLabel) + return uint64(m.Metric.Fingerprint()) + } + } + + var result Vector + // The set of signatures for the right-hand side vector. + rightSigs := map[uint64]struct{}{} + // Add all rhs samples to a map so we can easily find matches later. + for _, rs := range rhs { + rightSigs[signature(rs.Metric)] = struct{}{} + } + + for _, ls := range lhs { + // If there's a matching entry in the right-hand side vector, add the sample. + if _, ok := rightSigs[signature(ls.Metric)]; ok { + result = append(result, ls) + } + } + return result +} + // vectorBinop evaluates a binary operation between two vector values. func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorMatching) Vector { result := make(Vector, 0, len(rhs)) @@ -748,21 +783,16 @@ func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorM var value clientmodel.SampleValue var keep bool - if op == itemLAND { - value = ls.Value - keep = true - } else { - if _, exists := added[hash]; matching.Card == CardOneToOne && exists { - // Many-to-one matching must be explicit. - ev.errorf("many-to-one matching must be explicit") - } - // Account for potentially swapped sidedness. - vl, vr := ls.Value, rs.Value - if matching.Card == CardOneToMany { - vl, vr = vr, vl - } - value, keep = vectorElemBinop(op, vl, vr) + if _, exists := added[hash]; matching.Card == CardOneToOne && exists { + // Many-to-one matching must be explicit. + ev.errorf("many-to-one matching must be explicit") } + // Account for potentially swapped sidedness. + vl, vr := ls.Value, rs.Value + if matching.Card == CardOneToMany { + vl, vr = vr, vl + } + value, keep = vectorElemBinop(op, vl, vr) if keep { metric := resultMetric(op, ls, rs, matching) From 8a109e061b4815b778129ab2e4b877e9e02a9228 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Sat, 16 May 2015 14:00:11 +0200 Subject: [PATCH 02/13] Extract OR operation into own eval method. --- promql/engine.go | 53 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 917661208..4eb647d5c 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -595,6 +595,9 @@ func (ev *evaluator) eval(expr Expr) Value { if e.Op == itemLAND { return ev.vectorAnd(lhs.(Vector), rhs.(Vector), e.VectorMatching) } + if e.Op == itemLOR { + return ev.vectorOr(lhs.(Vector), rhs.(Vector), e.VectorMatching) + } return ev.vectorBinop(e.Op, lhs.(Vector), rhs.(Vector), e.VectorMatching) case lt == ExprVector && rt == ExprScalar: @@ -733,6 +736,37 @@ func (ev *evaluator) vectorAnd(lhs, rhs Vector, matching *VectorMatching) Vector return result } +func (ev *evaluator) vectorOr(lhs, rhs Vector, matching *VectorMatching) Vector { + if matching.Card != CardManyToMany { + panic("logical operations must always be many-to-many matching") + } + // If no matching labels are specified, match by all labels. + signature := func(m clientmodel.COWMetric) uint64 { + return clientmodel.SignatureForLabels(m.Metric, matching.On) + } + if len(matching.On) == 0 { + signature = func(m clientmodel.COWMetric) uint64 { + m.Delete(clientmodel.MetricNameLabel) + return uint64(m.Metric.Fingerprint()) + } + } + + var result Vector + leftSigs := map[uint64]struct{}{} + // Add everything from the left-hand-side vector. + for _, ls := range lhs { + leftSigs[signature(ls.Metric)] = struct{}{} + result = append(result, ls) + } + // Add all right-hand side elements which have not been added from the left-hand side. + for _, rs := range rhs { + if _, ok := leftSigs[signature(rs.Metric)]; !ok { + result = append(result, rs) + } + } + return result +} + // vectorBinop evaluates a binary operation between two vector values. func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorMatching) Vector { result := make(Vector, 0, len(rhs)) @@ -755,7 +789,7 @@ func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorM // The rhs is guaranteed to be the 'one' side. Having multiple samples // with the same hash means that the matching is many-to-many, // which is not supported. - if _, found := rm[hash]; matching.Card != CardManyToMany && found { + if _, found := rm[hash]; found { // Many-to-many matching not allowed. ev.errorf("many-to-many matching not allowed") } @@ -768,13 +802,6 @@ func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorM // the binary operation. for _, ls := range lhs { hash := hashForMetric(ls.Metric.Metric, matching.On) - // Any lhs sample we encounter in an OR operation belongs to the result. - if op == itemLOR { - ls.Metric = resultMetric(op, ls, nil, matching) - result = append(result, ls) - added[hash] = nil // Ensure matching rhs sample is not added later. - continue - } rs, found := rm[hash] // Look for a match in the rhs vector. if !found { @@ -820,16 +847,6 @@ func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorM } } - // Add all remaining samples in the rhs in an OR operation if they - // have not been matched up with a lhs sample. - if op == itemLOR { - for hash, rs := range rm { - if _, exists := added[hash]; !exists { - rs.Metric = resultMetric(op, rs, nil, matching) - result = append(result, rs) - } - } - } return result } From ce487f763efd73398b767c5625b4b5265ef001e3 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Sat, 16 May 2015 16:35:52 +0200 Subject: [PATCH 03/13] Simplify vector binary evaluation logic --- promql/engine.go | 227 ++++++++++++++++++++--------------------------- 1 file changed, 95 insertions(+), 132 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 4eb647d5c..2a206da65 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -592,14 +592,14 @@ func (ev *evaluator) eval(expr Expr) Value { } case lt == ExprVector && rt == ExprVector: - if e.Op == itemLAND { + switch e.Op { + case itemLAND: return ev.vectorAnd(lhs.(Vector), rhs.(Vector), e.VectorMatching) - } - if e.Op == itemLOR { + case itemLOR: return ev.vectorOr(lhs.(Vector), rhs.(Vector), e.VectorMatching) + default: + return ev.vectorBinop(e.Op, lhs.(Vector), rhs.(Vector), e.VectorMatching) } - return ev.vectorBinop(e.Op, lhs.(Vector), rhs.(Vector), e.VectorMatching) - case lt == ExprVector && rt == ExprScalar: return ev.vectorScalarBinop(e.Op, lhs.(Vector), rhs.(*Scalar), false) @@ -709,27 +709,19 @@ func (ev *evaluator) vectorAnd(lhs, rhs Vector, matching *VectorMatching) Vector panic("logical operations must always be many-to-many matching") } // If no matching labels are specified, match by all labels. - signature := func(m clientmodel.COWMetric) uint64 { - return clientmodel.SignatureForLabels(m.Metric, matching.On) - } - if len(matching.On) == 0 { - signature = func(m clientmodel.COWMetric) uint64 { - m.Delete(clientmodel.MetricNameLabel) - return uint64(m.Metric.Fingerprint()) - } - } + sigf := signatureFunc(matching.On...) var result Vector // The set of signatures for the right-hand side vector. rightSigs := map[uint64]struct{}{} // Add all rhs samples to a map so we can easily find matches later. for _, rs := range rhs { - rightSigs[signature(rs.Metric)] = struct{}{} + rightSigs[sigf(rs.Metric)] = struct{}{} } for _, ls := range lhs { // If there's a matching entry in the right-hand side vector, add the sample. - if _, ok := rightSigs[signature(ls.Metric)]; ok { + if _, ok := rightSigs[sigf(ls.Metric)]; ok { result = append(result, ls) } } @@ -740,114 +732,143 @@ func (ev *evaluator) vectorOr(lhs, rhs Vector, matching *VectorMatching) Vector if matching.Card != CardManyToMany { panic("logical operations must always be many-to-many matching") } - // If no matching labels are specified, match by all labels. - signature := func(m clientmodel.COWMetric) uint64 { - return clientmodel.SignatureForLabels(m.Metric, matching.On) - } - if len(matching.On) == 0 { - signature = func(m clientmodel.COWMetric) uint64 { - m.Delete(clientmodel.MetricNameLabel) - return uint64(m.Metric.Fingerprint()) - } - } + sigf := signatureFunc(matching.On...) var result Vector leftSigs := map[uint64]struct{}{} // Add everything from the left-hand-side vector. for _, ls := range lhs { - leftSigs[signature(ls.Metric)] = struct{}{} + leftSigs[sigf(ls.Metric)] = struct{}{} result = append(result, ls) } // Add all right-hand side elements which have not been added from the left-hand side. for _, rs := range rhs { - if _, ok := leftSigs[signature(rs.Metric)]; !ok { + if _, ok := leftSigs[sigf(rs.Metric)]; !ok { result = append(result, rs) } } return result } -// vectorBinop evaluates a binary operation between two vector values. +// vectorBinop evaluates a binary operation between two vector, excluding AND and OR. func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorMatching) Vector { - result := make(Vector, 0, len(rhs)) + if matching.Card == CardManyToMany { + panic("many-to-many only allowed for AND and OR") + } + var ( + result = Vector{} + sigf = signatureFunc(matching.On...) + resultLabels = append(matching.On, matching.Include...) + ) + // The control flow below handles one-to-one or many-to-one matching. // For one-to-many, swap sidedness and account for the swap when calculating // values. if matching.Card == CardOneToMany { lhs, rhs = rhs, lhs } + // All samples from the rhs hashed by the matching label/values. - rm := map[uint64]*Sample{} - // Maps the hash of the label values used for matching to the hashes of the label - // values of the include labels (if any). It is used to keep track of already - // inserted samples. - added := map[uint64][]uint64{} + rightSigs := map[uint64]*Sample{} // Add all rhs samples to a map so we can easily find matches later. for _, rs := range rhs { - hash := hashForMetric(rs.Metric.Metric, matching.On) + sig := sigf(rs.Metric) // The rhs is guaranteed to be the 'one' side. Having multiple samples - // with the same hash means that the matching is many-to-many, - // which is not supported. - if _, found := rm[hash]; found { + // with the same signature means that the matching is many-to-many. + if _, found := rightSigs[sig]; found { // Many-to-many matching not allowed. - ev.errorf("many-to-many matching not allowed") + ev.errorf("many-to-many matching not allowed: matching labels must be unique on one side") } - // In many-to-many matching the entry is simply overwritten. It can thus only - // be used to check whether any matching rhs entry exists but not retrieve them all. - rm[hash] = rs + rightSigs[sig] = rs } + // Tracks the match-signature. For one-to-one operations the value is nil. For many-to-one + // the value is a set of signatures to detect duplicated result elements. + matchedSigs := map[uint64]map[uint64]struct{}{} + // For all lhs samples find a respective rhs sample and perform // the binary operation. for _, ls := range lhs { - hash := hashForMetric(ls.Metric.Metric, matching.On) + sig := sigf(ls.Metric) - rs, found := rm[hash] // Look for a match in the rhs vector. + rs, found := rightSigs[sig] // Look for a match in the rhs vector. if !found { continue } - var value clientmodel.SampleValue - var keep bool - if _, exists := added[hash]; matching.Card == CardOneToOne && exists { - // Many-to-one matching must be explicit. - ev.errorf("many-to-one matching must be explicit") - } // Account for potentially swapped sidedness. vl, vr := ls.Value, rs.Value if matching.Card == CardOneToMany { vl, vr = vr, vl } - value, keep = vectorElemBinop(op, vl, vr) + value, keep := vectorElemBinop(op, vl, vr) + if !keep { + continue + } + metric := resultMetric(ls.Metric, op, resultLabels...) - if keep { - metric := resultMetric(op, ls, rs, matching) - // Check if the same label set has been added for a many-to-one matching before. - if matching.Card == CardManyToOne || matching.Card == CardOneToMany { - insHash := clientmodel.SignatureForLabels(metric.Metric, matching.Include) - if ihs, exists := added[hash]; exists { - for _, ih := range ihs { - if ih == insHash { - ev.errorf("metric with label set has already been matched") - } - } - added[hash] = append(ihs, insHash) - } else { - added[hash] = []uint64{insHash} - } + insertedSigs, exists := matchedSigs[sig] + if matching.Card == CardOneToOne { + if exists { + ev.errorf("multiple matches for labels: many-to-one matching must be explicit (group_left/group_right)") } - ns := &Sample{ - Metric: metric, - Value: value, - Timestamp: ev.Timestamp, + matchedSigs[sig] = nil // Set existance to true. + } else { + // In many-to-one matching the grouping labels have to ensure a unique metric + // for the result vector. Check whether those labels have already been added for + // the same matching labels. + insertSig := clientmodel.SignatureForLabels(metric.Metric, matching.Include) + if !exists { + insertedSigs = map[uint64]struct{}{} + matchedSigs[sig] = insertedSigs + } else if _, duplicate := insertedSigs[insertSig]; duplicate { + ev.errorf("multiple matches for labels: grouping labels must ensure unique matches") } - result = append(result, ns) - added[hash] = added[hash] // Set existance to true. + insertedSigs[insertSig] = struct{}{} + } + + result = append(result, &Sample{ + Metric: metric, + Value: value, + Timestamp: ev.Timestamp, + }) + } + return result +} + +// signatureFunc returns a function that calculates the signature for a metric +// based on the provided labels. +func signatureFunc(labels ...clientmodel.LabelName) func(m clientmodel.COWMetric) uint64 { + if len(labels) == 0 { + return func(m clientmodel.COWMetric) uint64 { + m.Delete(clientmodel.MetricNameLabel) + return uint64(m.Metric.Fingerprint()) } } + return func(m clientmodel.COWMetric) uint64 { + return clientmodel.SignatureForLabels(m.Metric, labels) + } +} - return result +// resultMetric returns the metric for the given sample(s) based on the vector +// binary operation and the matching options. +func resultMetric(met clientmodel.COWMetric, op itemType, labels ...clientmodel.LabelName) clientmodel.COWMetric { + if len(labels) == 0 { + if shouldDropMetricName(op) { + met.Delete(clientmodel.MetricNameLabel) + } + return met + } + // As we definitly write, creating a new metric is the easiest solution. + m := clientmodel.Metric{} + for _, ln := range labels { + // Included labels from the `group_x` modifier are taken from the "many"-side. + if v, ok := met.Metric[ln]; ok { + m[ln] = v + } + } + return clientmodel.COWMetric{Metric: m, Copied: false} } // vectorScalarBinop evaluates a binary operation between a vector and a scalar. @@ -1065,64 +1086,6 @@ func shouldDropMetricName(op itemType) bool { } } -// resultMetric returns the metric for the given sample(s) based on the vector -// binary operation and the matching options. -func resultMetric(op itemType, ls, rs *Sample, matching *VectorMatching) clientmodel.COWMetric { - if len(matching.On) == 0 || op == itemLOR || op == itemLAND { - if shouldDropMetricName(op) { - ls.Metric.Delete(clientmodel.MetricNameLabel) - } - return ls.Metric - } - - m := clientmodel.Metric{} - for _, ln := range matching.On { - m[ln] = ls.Metric.Metric[ln] - } - - for _, ln := range matching.Include { - // Included labels from the `group_x` modifier are taken from the "many"-side. - v, ok := ls.Metric.Metric[ln] - if ok { - m[ln] = v - } - } - return clientmodel.COWMetric{false, m} -} - -// hashForMetric calculates a hash value for the given metric based on the matching -// options for the binary operation. -func hashForMetric(metric clientmodel.Metric, withLabels clientmodel.LabelNames) uint64 { - var labels clientmodel.LabelNames - - if len(withLabels) > 0 { - var match bool - for _, ln := range withLabels { - if _, match = metric[ln]; !match { - break - } - } - // If the metric does not contain the labels to match on, build the hash - // over the whole metric to give it a unique hash. - if !match { - labels = make(clientmodel.LabelNames, 0, len(metric)) - for ln := range metric { - labels = append(labels, ln) - } - } else { - labels = withLabels - } - } else { - labels = make(clientmodel.LabelNames, 0, len(metric)) - for ln := range metric { - if ln != clientmodel.MetricNameLabel { - labels = append(labels, ln) - } - } - } - return clientmodel.SignatureForLabels(metric, labels) -} - // chooseClosestSample chooses the closest sample of a list of samples // surrounding a given target time. If samples are found both before and after // the target time, the sample value is interpolated between these. Otherwise, From d122749b393f195a7ac4d26532c61872a3e3110c Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Tue, 12 May 2015 10:39:10 +0200 Subject: [PATCH 04/13] Enhance lexer testing output --- promql/lex.go | 50 ++++++++++++++++++++++++++++------------ promql/lex_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 19 deletions(-) diff --git a/promql/lex.go b/promql/lex.go index eccd91bf4..3d5ff235f 100644 --- a/promql/lex.go +++ b/promql/lex.go @@ -104,6 +104,8 @@ const ( itemString itemNumber itemDuration + itemBlank + itemTimes operatorsStart // Operators. @@ -193,6 +195,8 @@ var itemTypeStr = map[itemType]string{ itemComma: ",", itemAssign: "=", itemSemicolon: ";", + itemBlank: "_", + itemTimes: "x", itemSUB: "-", itemADD: "+", @@ -214,6 +218,9 @@ func init() { for s, ty := range key { itemTypeStr[ty] = s } + // Special numbers. + key["inf"] = itemNumber + key["nan"] = itemNumber } func (t itemType) String() string { @@ -450,21 +457,6 @@ func lexStatements(l *lexer) stateFn { case r == '"' || r == '\'': l.stringOpen = r return lexString - case r == 'N' || r == 'n' || r == 'I' || r == 'i': - n2 := strings.ToLower(l.input[l.pos:]) - if len(n2) < 3 || !isAlphaNumeric(rune(n2[2])) { - if (r == 'N' || r == 'n') && strings.HasPrefix(n2, "an") { - l.pos += 2 - l.emit(itemNumber) - break - } - if (r == 'I' || r == 'i') && strings.HasPrefix(n2, "nf") { - l.pos += 2 - l.emit(itemNumber) - break - } - } - fallthrough case isAlpha(r) || r == ':': l.backup() return lexKeywordOrIdentifier @@ -551,6 +543,34 @@ func lexInsideBraces(l *lexer) stateFn { return lexInsideBraces } +// lexValueSequence scans a value sequence of a series description. +func lexValueSequence(l *lexer) stateFn { + switch r := l.next(); { + case r == eof: + return lexStatements + case isSpace(r): + lexSpace(l) + case r == '+': + l.emit(itemADD) + case r == '-': + l.emit(itemSUB) + case r == 'x': + l.emit(itemTimes) + case r == '_': + l.emit(itemBlank) + case unicode.IsDigit(r) || (r == '.' && unicode.IsDigit(l.peek())): + l.backup() + lexNumber(l) + case isAlpha(r): + l.backup() + // We might lex invalid items here but this will be caught by the parser. + return lexKeywordOrIdentifier + default: + return l.errorf("unexpected character in series sequence: %q", r) + } + return lexValueSequence +} + // lexString scans a quoted string. The initial quote has already been seen. func lexString(l *lexer) stateFn { Loop: diff --git a/promql/lex_test.go b/promql/lex_test.go index ce56d83f6..93cb584ad 100644 --- a/promql/lex_test.go +++ b/promql/lex_test.go @@ -14,6 +14,7 @@ package promql import ( + "fmt" "reflect" "testing" ) @@ -354,6 +355,42 @@ var tests = []struct { }, { input: `]`, fail: true, }, + // Test series description. + { + input: `{} _ 1 x .3`, + expected: []item{ + {itemLeftBrace, 0, `{`}, + {itemRightBrace, 1, `}`}, + {itemBlank, 3, `_`}, + {itemNumber, 5, `1`}, + {itemTimes, 7, `x`}, + {itemNumber, 9, `.3`}, + }, + seriesDesc: true, + }, + { + input: `metric +Inf Inf NaN`, + expected: []item{ + {itemIdentifier, 0, `metric`}, + {itemADD, 7, `+`}, + {itemNumber, 8, `Inf`}, + {itemNumber, 12, `Inf`}, + {itemNumber, 16, `NaN`}, + }, + seriesDesc: true, + }, + { + input: `metric 1+1x4`, + expected: []item{ + {itemIdentifier, 0, `metric`}, + {itemNumber, 7, `1`}, + {itemADD, 8, `+`}, + {itemNumber, 9, `1`}, + {itemTimes, 10, `x`}, + {itemNumber, 11, `4`}, + }, + seriesDesc: true, + }, } // TestLexer tests basic functionality of the lexer. More elaborate tests are implemented @@ -370,20 +407,32 @@ func TestLexer(t *testing.T) { lastItem := out[len(out)-1] if test.fail { if lastItem.typ != itemError { - t.Fatalf("%d: expected lexing error but did not fail", i) + t.Logf("%d: input %q", i, test.input) + t.Fatalf("expected lexing error but did not fail") } continue } if lastItem.typ == itemError { - t.Fatalf("%d: unexpected lexing error: %s", i, lastItem) + t.Logf("%d: input %q", i, test.input) + t.Fatalf("unexpected lexing error at position %d: %s", lastItem.pos, lastItem) } if !reflect.DeepEqual(lastItem, item{itemEOF, Pos(len(test.input)), ""}) { - t.Fatalf("%d: lexing error: expected output to end with EOF item", i) + t.Logf("%d: input %q", i, test.input) + t.Fatalf("lexing error: expected output to end with EOF item.\ngot:\n%s", expectedList(out)) } out = out[:len(out)-1] if !reflect.DeepEqual(out, test.expected) { - t.Errorf("%d: lexing mismatch:\nexpected: %#v\n-----\ngot: %#v", i, test.expected, out) + t.Logf("%d: input %q", i, test.input) + t.Fatalf("lexing mismatch:\nexpected:\n%s\ngot:\n%s", expectedList(test.expected), expectedList(out)) } } } + +func expectedList(exp []item) string { + s := "" + for _, it := range exp { + s += fmt.Sprintf("\t%#v\n", it) + } + return s +} From a236c0145777bea02cbd1a168fb62ae2585cd725 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Mon, 11 May 2015 14:04:53 +0200 Subject: [PATCH 05/13] Add time series description parsing. This commit adds parsing of time series description to the exisiting query language parser. Time series descriptions are defined by a metric followed by a sequence of values. --- promql/lex.go | 13 +++++- promql/lex_test.go | 8 ++-- promql/parse.go | 107 +++++++++++++++++++++++++++++++++++++++++++ promql/parse_test.go | 98 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+), 4 deletions(-) diff --git a/promql/lex.go b/promql/lex.go index 3d5ff235f..935c2d8a2 100644 --- a/promql/lex.go +++ b/promql/lex.go @@ -284,6 +284,10 @@ type lexer struct { braceOpen bool // Whether a { is opened. bracketOpen bool // Whether a [ is opened. stringOpen rune // Quote rune of the string currently being read. + + // seriesDesc is set when a series description for the testing + // language is lexed. + seriesDesc bool } // next returns the next rune in the input. @@ -536,6 +540,10 @@ func lexInsideBraces(l *lexer) stateFn { case r == '}': l.emit(itemRightBrace) l.braceOpen = false + + if l.seriesDesc { + return lexValueSequence + } return lexStatements default: return l.errorf("unexpected character inside braces: %q", r) @@ -670,7 +678,7 @@ func (l *lexer) scanNumber() bool { l.acceptRun("0123456789") } // Next thing must not be alphanumeric. - if isAlphaNumeric(l.peek()) { + if isAlphaNumeric(l.peek()) && !l.seriesDesc { return false } return true @@ -709,6 +717,9 @@ Loop: break Loop } } + if l.seriesDesc && l.peek() != '{' { + return lexValueSequence + } return lexStatements } diff --git a/promql/lex_test.go b/promql/lex_test.go index 93cb584ad..c12f75327 100644 --- a/promql/lex_test.go +++ b/promql/lex_test.go @@ -20,9 +20,10 @@ import ( ) var tests = []struct { - input string - expected []item - fail bool + input string + expected []item + fail bool + seriesDesc bool // Whether to lex a series description. }{ // Test common stuff. { @@ -398,6 +399,7 @@ var tests = []struct { func TestLexer(t *testing.T) { for i, test := range tests { l := lex(test.input) + l.seriesDesc = test.seriesDesc out := []item{} for it := range l.items { diff --git a/promql/parse.go b/promql/parse.go index eeff1f7cc..65f94a430 100644 --- a/promql/parse.go +++ b/promql/parse.go @@ -70,6 +70,14 @@ func ParseExpr(input string) (Expr, error) { return expr, err } +// parseSeriesDesc parses the description of a time series. +func parseSeriesDesc(input string) (clientmodel.Metric, []sequenceValue, error) { + p := newParser(input) + p.lex.seriesDesc = true + + return p.parseSeriesDesc() +} + // newParser returns a new parser. func newParser(input string) *parser { p := &parser{ @@ -112,6 +120,105 @@ func (p *parser) parseExpr() (expr Expr, err error) { return } +// sequenceValue is a omittable value in a sequence of time series values. +type sequenceValue struct { + value clientmodel.SampleValue + omitted bool +} + +func (v sequenceValue) String() string { + if v.omitted { + return "_" + } + return v.value.String() +} + +// parseSeriesDesc parses a description of a time series into its metric and value sequence. +func (p *parser) parseSeriesDesc() (m clientmodel.Metric, vals []sequenceValue, err error) { + defer p.recover(&err) + + name := "" + m = clientmodel.Metric{} + + t := p.peek().typ + if t == itemIdentifier || t == itemMetricIdentifier { + name = p.next().val + t = p.peek().typ + } + if t == itemLeftBrace { + m = clientmodel.Metric(p.labelSet()) + } + if name != "" { + m[clientmodel.MetricNameLabel] = clientmodel.LabelValue(name) + } + + const ctx = "series values" + for { + if p.peek().typ == itemEOF { + break + } + + // Extract blanks. + if p.peek().typ == itemBlank { + p.next() + times := uint64(1) + if p.peek().typ == itemTimes { + p.next() + times, err = strconv.ParseUint(p.expect(itemNumber, ctx).val, 10, 64) + if err != nil { + p.errorf("invalid repetition in %s: %s", ctx, err) + } + } + for i := uint64(0); i < times; i++ { + vals = append(vals, sequenceValue{omitted: true}) + } + continue + } + + // Extract values. + sign := 1.0 + if t := p.peek().typ; t == itemSUB || t == itemADD { + if p.next().typ == itemSUB { + sign = -1 + } + } + k := sign * p.number(p.expect(itemNumber, ctx).val) + vals = append(vals, sequenceValue{ + value: clientmodel.SampleValue(k), + }) + + // If there are no offset repetitions specified, proceed with the next value. + if t := p.peek().typ; t == itemNumber || t == itemBlank { + continue + } else if t == itemEOF { + break + } else if t != itemADD && t != itemSUB { + p.errorf("expected next value or relative expansion in %s but got %s", ctx, t.desc()) + } + + // Expand the repeated offsets into values. + sign = 1.0 + if p.next().typ == itemSUB { + sign = -1.0 + } + offset := sign * p.number(p.expect(itemNumber, ctx).val) + p.expect(itemTimes, ctx) + + times, err := strconv.ParseUint(p.expect(itemNumber, ctx).val, 10, 64) + if err != nil { + p.errorf("invalid repetition in %s: %s", ctx, err) + } + + for i := uint64(0); i < times; i++ { + k += offset + vals = append(vals, sequenceValue{ + value: clientmodel.SampleValue(k), + }) + } + } + return m, vals, nil +} + // typecheck checks correct typing of the parsed statements or expression. func (p *parser) typecheck(node Node) (err error) { defer p.recover(&err) diff --git a/promql/parse_test.go b/promql/parse_test.go index ab2ca5c25..ca2ef4555 100644 --- a/promql/parse_test.go +++ b/promql/parse_test.go @@ -1216,3 +1216,101 @@ func mustGetFunction(name string) *Function { } return f } + +var testSeries = []struct { + input string + expectedMetric clientmodel.Metric + expectedValues []sequenceValue + fail bool +}{ + { + input: `{} 1 2 3`, + expectedMetric: clientmodel.Metric{}, + expectedValues: newSeq(1, 2, 3), + }, { + input: `{a="b"} -1 2 3`, + expectedMetric: clientmodel.Metric{ + "a": "b", + }, + expectedValues: newSeq(-1, 2, 3), + }, { + input: `my_metric 1 2 3`, + expectedMetric: clientmodel.Metric{ + clientmodel.MetricNameLabel: "my_metric", + }, + expectedValues: newSeq(1, 2, 3), + }, { + input: `my_metric{} 1 2 3`, + expectedMetric: clientmodel.Metric{ + clientmodel.MetricNameLabel: "my_metric", + }, + expectedValues: newSeq(1, 2, 3), + }, { + input: `my_metric{a="b"} 1 2 3`, + expectedMetric: clientmodel.Metric{ + clientmodel.MetricNameLabel: "my_metric", + "a": "b", + }, + expectedValues: newSeq(1, 2, 3), + }, { + input: `my_metric{a="b"} 1 2 3-10x4`, + expectedMetric: clientmodel.Metric{ + clientmodel.MetricNameLabel: "my_metric", + "a": "b", + }, + expectedValues: newSeq(1, 2, 3, -7, -17, -27, -37), + }, { + input: `my_metric{a="b"} 1 3 _ 5 _x4`, + expectedMetric: clientmodel.Metric{ + clientmodel.MetricNameLabel: "my_metric", + "a": "b", + }, + expectedValues: newSeq(1, 3, none, 5, none, none, none, none), + }, { + input: `my_metric{a="b"} 1 3 _ 5 _a4`, + fail: true, + }, +} + +// For these tests only, we use the samallest float64 to signal an omitted value. +const none = math.SmallestNonzeroFloat64 + +func newSeq(vals ...float64) (res []sequenceValue) { + for _, v := range vals { + if v == none { + res = append(res, sequenceValue{omitted: true}) + } else { + res = append(res, sequenceValue{value: clientmodel.SampleValue(v)}) + } + } + return res +} + +func TestParseSeries(t *testing.T) { + for _, test := range testSeries { + parser := newParser(test.input) + parser.lex.seriesDesc = true + + metric, vals, err := parser.parseSeriesDesc() + if !test.fail && err != nil { + t.Errorf("error in input: \n\n%s\n", test.input) + t.Fatalf("could not parse: %s", err) + } + if test.fail && err != nil { + continue + } + + if test.fail { + if err != nil { + continue + } + t.Errorf("error in input: \n\n%s\n", test.input) + t.Fatalf("failure expected, but passed") + } + + if !reflect.DeepEqual(vals, test.expectedValues) || !reflect.DeepEqual(metric, test.expectedMetric) { + t.Errorf("error in input: \n\n%s\n", test.input) + t.Fatalf("no match\n\nexpected:\n%s %s\ngot: \n%s %s\n", test.expectedMetric, test.expectedValues, metric, vals) + } + } +} From 63219647385ce111c36453d620edb18c37ba05f5 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Mon, 11 May 2015 15:56:35 +0200 Subject: [PATCH 06/13] Add parsing and execution of new test format. This commit adds a new test structure that parses and executes the new testing language. --- promql/engine.go | 11 +- promql/parse_test.go | 2 +- promql/promql_test.go | 10 +- promql/setup_test.go | 3 +- promql/test.go | 507 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 523 insertions(+), 10 deletions(-) create mode 100644 promql/test.go diff --git a/promql/engine.go b/promql/engine.go index 7b53f661d..07799ba32 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -279,20 +279,25 @@ func (ng *Engine) NewRangeQuery(qs string, start, end clientmodel.Timestamp, int if err != nil { return nil, err } + qry := ng.newQuery(expr, start, end, interval) + qry.q = qs + + return qry, nil +} + +func (ng *Engine) newQuery(expr Expr, start, end clientmodel.Timestamp, interval time.Duration) *query { es := &EvalStmt{ Expr: expr, Start: start, End: end, Interval: interval, } - qry := &query{ - q: qs, stmts: Statements{es}, ng: ng, stats: stats.NewTimerGroup(), } - return qry, nil + return qry } // testStmt is an internal helper statement that allows execution diff --git a/promql/parse_test.go b/promql/parse_test.go index ca2ef4555..9940f0468 100644 --- a/promql/parse_test.go +++ b/promql/parse_test.go @@ -1272,7 +1272,7 @@ var testSeries = []struct { }, } -// For these tests only, we use the samallest float64 to signal an omitted value. +// For these tests only, we use the smallest float64 to signal an omitted value. const none = math.SmallestNonzeroFloat64 func newSeq(vals ...float64) (res []sequenceValue) { diff --git a/promql/promql_test.go b/promql/promql_test.go index 93af620d3..4201dacea 100644 --- a/promql/promql_test.go +++ b/promql/promql_test.go @@ -33,13 +33,13 @@ var ( testEvalTime = testStartTime.Add(testSampleInterval * 10) fixturesPath = "fixtures" - reSample = regexp.MustCompile(`^(.*)(?: \=\>|:) (\-?\d+\.?\d*(?:e-?\d+)?|[+-]Inf|NaN) \@\[(\d+)\]$`) - minNormal = math.Float64frombits(0x0010000000000000) // The smallest positive normal value of type float64. + reSample = regexp.MustCompile(`^(.*)(?: \=\>|:) (\-?\d+\.?\d*(?:e-?\d+)?|[+-]Inf|NaN) \@\[(\d+)\]$`) + // minNormal = math.Float64frombits(0x0010000000000000) // The smallest positive normal value of type float64. ) -const ( - epsilon = 0.000001 // Relative error allowed for sample values. -) +// const ( +// epsilon = 0.000001 // Relative error allowed for sample values. +// ) func annotateWithTime(lines []string, timestamp clientmodel.Timestamp) []string { annotatedLines := []string{} diff --git a/promql/setup_test.go b/promql/setup_test.go index 5796d7f87..796c93896 100644 --- a/promql/setup_test.go +++ b/promql/setup_test.go @@ -23,7 +23,8 @@ import ( ) var testSampleInterval = time.Duration(5) * time.Minute -var testStartTime = clientmodel.Timestamp(0) + +// var testStartTime = clientmodel.Timestamp(0) func getTestValueStream(startVal, endVal, stepVal clientmodel.SampleValue, startTime clientmodel.Timestamp) (resultValues metric.Values) { currentTime := startTime diff --git a/promql/test.go b/promql/test.go new file mode 100644 index 000000000..1d3bf6607 --- /dev/null +++ b/promql/test.go @@ -0,0 +1,507 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package promql + +import ( + "fmt" + "io/ioutil" + "math" + "regexp" + "strconv" + "strings" + "testing" + "time" + + clientmodel "github.com/prometheus/client_golang/model" + + "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/storage/local" + "github.com/prometheus/prometheus/storage/metric" + "github.com/prometheus/prometheus/utility" + + testutil "github.com/prometheus/prometheus/utility/test" +) + +var ( + minNormal = math.Float64frombits(0x0010000000000000) // The smallest positive normal value of type float64. + + patSpace = regexp.MustCompile("[\t ]+") + patLoad = regexp.MustCompile(`^load\s+(.+?)$`) + patEvalInstant = regexp.MustCompile(`^eval(?:_(fail|ordered))?\s+instant\s+(?:at\s+(.+?))?\s+(.+)$`) +) + +const ( + testStartTime = clientmodel.Timestamp(0) + epsilon = 0.000001 // Relative error allowed for sample values. + maxErrorCount = 10 +) + +// Test is a sequence of read and write commands that are run +// against a test storage. +type Test struct { + *testing.T + + cmds []testCommand + + storage local.Storage + closeStorage func() + queryEngine *Engine +} + +// NewTest returns an initialized empty Test. +func NewTest(t *testing.T, input string) (*Test, error) { + test := &Test{ + T: t, + cmds: []testCommand{}, + } + err := test.parse(input) + test.clear() + + return test, err +} + +func NewTestFromFile(t *testing.T, filename string) (*Test, error) { + content, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return NewTest(t, string(content)) +} + +func raise(line int, format string, v ...interface{}) error { + return &ParseErr{ + Line: line + 1, + Err: fmt.Errorf(format, v...), + } +} + +func (t *Test) parseLoad(lines []string, i int) (int, *loadCmd, error) { + if !patLoad.MatchString(lines[i]) { + return i, nil, raise(i, "invalid load command. (load )") + } + parts := patLoad.FindStringSubmatch(lines[i]) + + gap, err := utility.StringToDuration(parts[1]) + if err != nil { + return i, nil, raise(i, "invalid step definition %q: %s", parts[1], err) + } + cmd := newLoadCmd(gap) + for i+1 < len(lines) { + i++ + defLine := lines[i] + if len(defLine) == 0 { + i-- + break + } + metric, vals, err := parseSeriesDesc(defLine) + if err != nil { + perr := err.(*ParseErr) + perr.Line = i + 1 + return i, nil, err + } + cmd.set(metric, vals...) + } + return i, cmd, nil +} + +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 ] ") + } + parts := patEvalInstant.FindStringSubmatch(lines[i]) + var ( + mod = parts[1] + at = parts[2] + qry = parts[3] + ) + expr, err := ParseExpr(qry) + if err != nil { + perr := err.(*ParseErr) + perr.Line = i + 1 + perr.Pos += strings.Index(lines[i], qry) + return i, nil, perr + } + + offset, err := utility.StringToDuration(at) + if err != nil { + return i, nil, raise(i, "invalid step definition %q: %s", parts[1], err) + } + ts := testStartTime.Add(offset) + + cmd := newEvalCmd(expr, ts, ts, 0) + switch mod { + case "ordered": + cmd.ordered = true + case "fail": + cmd.fail = true + } + + for j := 1; i+1 < len(lines); j++ { + i++ + defLine := lines[i] + if len(defLine) == 0 { + i-- + break + } + if f, err := parseNumber(defLine); err == nil { + cmd.expect(0, nil, sequenceValue{value: clientmodel.SampleValue(f)}) + break + } + metric, vals, err := parseSeriesDesc(defLine) + if err != nil { + perr := err.(*ParseErr) + perr.Line = i + 1 + return i, nil, err + } + + // Currently, we are not expecting any matrices. + if len(vals) > 1 { + return i, nil, raise(i, "expecting multiple values in instant evaluation not allowed") + } + cmd.expect(j, metric, vals...) + } + return i, cmd, nil +} + +// parse the given command sequence and appends it to the test. +func (t *Test) parse(input string) error { + // Trim lines and remove comments. + lines := strings.Split(input, "\n") + for i, l := range lines { + l = strings.TrimSpace(l) + if strings.HasPrefix(l, "#") { + l = "" + } + lines[i] = l + } + var err error + + // Scan for steps line by line. + for i := 0; i < len(lines); i++ { + l := lines[i] + if len(l) == 0 { + continue + } + var cmd testCommand + + switch c := strings.ToLower(patSpace.Split(l, 2)[0]); { + case c == "clear": + cmd = &clearCmd{} + case c == "load": + i, cmd, err = t.parseLoad(lines, i) + case strings.HasPrefix(c, "eval"): + i, cmd, err = t.parseEval(lines, i) + default: + return raise(i, "invalid command %q", l) + } + if err != nil { + return err + } + t.cmds = append(t.cmds, cmd) + } + return nil +} + +// testCommand is an interface that ensures that only the package internal +// types can be a valid command for a test. +type testCommand interface { + testCmd() +} + +func (*clearCmd) testCmd() {} +func (*loadCmd) testCmd() {} +func (*evalCmd) testCmd() {} + +// loadCmd is a command that loads sequences of sample values for specific +// metrics into the storage. +type loadCmd struct { + gap time.Duration + metrics map[clientmodel.Fingerprint]clientmodel.Metric + defs map[clientmodel.Fingerprint]metric.Values +} + +func newLoadCmd(gap time.Duration) *loadCmd { + return &loadCmd{ + gap: gap, + metrics: map[clientmodel.Fingerprint]clientmodel.Metric{}, + defs: map[clientmodel.Fingerprint]metric.Values{}, + } +} + +func (cmd loadCmd) String() string { + return "load" +} + +// set a sequence of sample values for the given metric. +func (cmd *loadCmd) set(m clientmodel.Metric, vals ...sequenceValue) { + fp := m.Fingerprint() + + samples := make(metric.Values, 0, len(vals)) + ts := testStartTime + for _, v := range vals { + if !v.omitted { + samples = append(samples, metric.SamplePair{ + Timestamp: ts, + Value: v.value, + }) + } + ts = ts.Add(cmd.gap) + } + cmd.defs[fp] = samples + cmd.metrics[fp] = m +} + +// append the defined time series to the storage. +func (cmd *loadCmd) append(a storage.SampleAppender) { + for fp, samples := range cmd.defs { + met := cmd.metrics[fp] + for _, smpl := range samples { + s := &clientmodel.Sample{ + Metric: met, + Value: smpl.Value, + Timestamp: smpl.Timestamp, + } + a.Append(s) + } + } +} + +// evalCmd is a command that evaluates an expression for the given time (range) +// and expects a specific result. +type evalCmd struct { + expr Expr + start, end clientmodel.Timestamp + interval time.Duration + + instant bool + fail, ordered bool + + metrics map[clientmodel.Fingerprint]clientmodel.Metric + expected map[clientmodel.Fingerprint]entry +} + +type entry struct { + pos int + vals []sequenceValue +} + +func (e entry) String() string { + return fmt.Sprintf("%d: %s", e.pos, e.vals) +} + +func newEvalCmd(expr Expr, start, end clientmodel.Timestamp, interval time.Duration) *evalCmd { + return &evalCmd{ + expr: expr, + start: start, + end: end, + interval: interval, + instant: start == end && interval == 0, + + metrics: map[clientmodel.Fingerprint]clientmodel.Metric{}, + expected: map[clientmodel.Fingerprint]entry{}, + } +} + +func (ev *evalCmd) String() string { + return "eval" +} + +// expect adds a new metric with a sequence of values to the set of expected +// results for the query. +func (ev *evalCmd) expect(pos int, m clientmodel.Metric, vals ...sequenceValue) { + if m == nil { + ev.expected[0] = entry{pos: pos, vals: vals} + return + } + fp := m.Fingerprint() + ev.metrics[fp] = m + ev.expected[fp] = entry{pos: pos, vals: vals} +} + +// compareResult compares the result value with the defined expectation. +func (ev *evalCmd) compareResult(result Value) error { + switch val := result.(type) { + case Matrix: + if ev.instant { + return fmt.Errorf("received range result on instant evaluation") + } + seen := map[clientmodel.Fingerprint]bool{} + for pos, v := range val { + fp := v.Metric.Metric.Fingerprint() + if _, ok := ev.metrics[fp]; !ok { + return fmt.Errorf("unexpected metric %s in result", v.Metric.Metric) + } + exp := ev.expected[fp] + if ev.ordered && exp.pos != pos+1 { + return fmt.Errorf("expected metric %s with %v at position %d but was at %d", v.Metric.Metric, exp.vals, exp.pos, pos+1) + } + for i, expVal := range exp.vals { + if !almostEqual(float64(expVal.value), float64(v.Values[i].Value)) { + return fmt.Errorf("expected %v for %s but got %v", expVal, v.Metric.Metric, v.Values) + } + } + seen[fp] = true + } + for fp, expVals := range ev.expected { + if !seen[fp] { + return fmt.Errorf("expected metric %s with %v not found", ev.metrics[fp], expVals) + } + } + + case Vector: + if !ev.instant { + fmt.Errorf("received instant result on range evaluation") + } + seen := map[clientmodel.Fingerprint]bool{} + for pos, v := range val { + fp := v.Metric.Metric.Fingerprint() + if _, ok := ev.metrics[fp]; !ok { + return fmt.Errorf("unexpected metric %s in result", v.Metric.Metric) + } + exp := ev.expected[fp] + if ev.ordered && exp.pos != pos+1 { + return fmt.Errorf("expected metric %s with %v at position %d but was at %d", v.Metric.Metric, exp.vals, exp.pos, pos+1) + } + if !almostEqual(float64(exp.vals[0].value), float64(v.Value)) { + return fmt.Errorf("expected %v for %s but got %v", exp.vals[0].value, v.Metric.Metric, v.Value) + } + + seen[fp] = true + } + for fp, expVals := range ev.expected { + if !seen[fp] { + return fmt.Errorf("expected metric %s with %v not found", ev.metrics[fp], expVals) + } + } + + case *Scalar: + if !almostEqual(float64(ev.expected[0].vals[0].value), float64(val.Value)) { + return fmt.Errorf("expected scalar %v but got %v", val.Value, ev.expected[0].vals[0].value) + } + + default: + panic(fmt.Errorf("promql.Test.compareResult: unexpected result type %T", result)) + } + return nil +} + +// clearCmd is a command that wipes the test's storage state. +type clearCmd struct{} + +func (cmd clearCmd) String() string { + return "clear" +} + +// Run executes the command sequence of the test. Until the maximum error number +// is reached, evaluation errors do not terminate execution. +func (t *Test) Run() error { + for _, cmd := range t.cmds { + err := t.exec(cmd) + // TODO(fabxc): aggregate command errors, yield diffs for result + // comparison errors. + if err != nil { + return err + } + } + return nil +} + +// exec processes a single step of the test +func (t *Test) exec(tc testCommand) error { + switch cmd := tc.(type) { + case *clearCmd: + t.clear() + + case *loadCmd: + cmd.append(t.storage) + t.storage.WaitForIndexing() + + case *evalCmd: + q := t.queryEngine.newQuery(cmd.expr, cmd.start, cmd.end, cmd.interval) + res := q.Exec() + if res.Err != nil { + if cmd.fail { + return nil + } + return fmt.Errorf("error evaluating query: %s", res.Err) + } + if res.Err == nil && cmd.fail { + return fmt.Errorf("expected error evaluating query but got none") + } + + err := cmd.compareResult(res.Value) + if err != nil { + return fmt.Errorf("error in %s %s: %s", cmd, cmd.expr, err) + } + + default: + panic("promql.Test.exec: unknown test command type") + } + return nil +} + +// clear the current test storage of all inserted samples. +func (t *Test) clear() { + if t.closeStorage != nil { + t.closeStorage() + } + if t.queryEngine != nil { + t.queryEngine.Stop() + } + + var closer testutil.Closer + t.storage, closer = local.NewTestStorage(t, 1) + + t.closeStorage = closer.Close + t.queryEngine = NewEngine(t.storage) +} + +func (t *Test) Close() { + t.queryEngine.Stop() + t.closeStorage() +} + +// samplesAlmostEqual returns true if the two sample lines only differ by a +// small relative error in their sample value. +func almostEqual(a, b float64) bool { + // NaN has no equality but for testing we still want to know whether both values + // are NaN. + if math.IsNaN(a) && math.IsNaN(b) { + return true + } + + // Cf. http://floating-point-gui.de/errors/comparison/ + if a == b { + return true + } + + diff := math.Abs(a - b) + + if a == 0 || b == 0 || diff < minNormal { + return diff < epsilon*minNormal + } + return diff/(math.Abs(a)+math.Abs(b)) < epsilon +} + +func parseNumber(s string) (float64, error) { + n, err := strconv.ParseInt(s, 0, 64) + f := float64(n) + if err != nil { + f, err = strconv.ParseFloat(s, 64) + } + if err != nil { + return 0, fmt.Errorf("error parsing number: %s", err) + } + return f, nil +} From 03094eff04a3d860fb0d8c8cfed88ae7fd011ec4 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Tue, 12 May 2015 12:00:28 +0200 Subject: [PATCH 07/13] Migrate parsing error tests. The promql_test checks failure of various bad syntaxed queries. Those are moved into the parser tests as the new testing language only deals with valid queries. --- promql/parse_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/promql/parse_test.go b/promql/parse_test.go index 9940f0468..dfeac7eec 100644 --- a/promql/parse_test.go +++ b/promql/parse_test.go @@ -167,6 +167,10 @@ var testExpr = []struct { input: "((1)", fail: true, errMsg: "unclosed left parenthesis", + }, { + input: "999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", + fail: true, + errMsg: "out of range", }, { input: "(", fail: true, @@ -476,6 +480,14 @@ var testExpr = []struct { input: "foo or on(bar) group_right(baz) bar", fail: true, errMsg: "no grouping allowed for AND and OR operations", + }, { + input: `http_requests{group="production"} / on(instance) group_left cpu_count{type="smp"}`, + fail: true, + errMsg: "unexpected identifier \"cpu_count\" in grouping opts, expected \"(\"", + }, { + input: `http_requests{group="production"} + on(instance) group_left(job,instance) cpu_count{type="smp"}`, + fail: true, + errMsg: "label \"instance\" must not occur in ON and INCLUDE clause at once", }, // Test vector selector. { @@ -662,6 +674,9 @@ var testExpr = []struct { input: `foo[5m] OFFSET 1h30m`, fail: true, errMsg: "bad number or duration syntax: \"1h3\"", + }, { + input: `foo["5m"]`, + fail: true, }, { input: `foo[]`, fail: true, From eba07a7d3d43ff9b95998cde072c98e57e17cb13 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Tue, 12 May 2015 12:21:24 +0200 Subject: [PATCH 08/13] Migrate histogram tests to test language. --- promql/promql_test.go | 184 -------------------------------- promql/setup_test.go | 85 --------------- promql/testdata/histograms.test | 141 ++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 269 deletions(-) create mode 100644 promql/testdata/histograms.test diff --git a/promql/promql_test.go b/promql/promql_test.go index 4201dacea..acc2d8bfd 100644 --- a/promql/promql_test.go +++ b/promql/promql_test.go @@ -627,14 +627,6 @@ func TestExpressions(t *testing.T) { `x{y="testvalue"} => 100 @[%v]`, `label_grouping_test{a="a", b="abb"} => 200 @[%v]`, `label_grouping_test{a="aa", b="bb"} => 100 @[%v]`, - `testhistogram_bucket{le="0.1", start="positive"} => 50 @[%v]`, - `testhistogram_bucket{le=".2", start="positive"} => 70 @[%v]`, - `testhistogram_bucket{le="1e0", start="positive"} => 110 @[%v]`, - `testhistogram_bucket{le="+Inf", start="positive"} => 120 @[%v]`, - `testhistogram_bucket{le="-.2", start="negative"} => 10 @[%v]`, - `testhistogram_bucket{le="-0.1", start="negative"} => 20 @[%v]`, - `testhistogram_bucket{le="0.3", start="negative"} => 20 @[%v]`, - `testhistogram_bucket{le="+Inf", start="negative"} => 30 @[%v]`, `request_duration_seconds_bucket{instance="ins1", job="job1", le="0.1"} => 10 @[%v]`, `request_duration_seconds_bucket{instance="ins1", job="job1", le="0.2"} => 30 @[%v]`, `request_duration_seconds_bucket{instance="ins1", job="job1", le="+Inf"} => 40 @[%v]`, @@ -934,182 +926,6 @@ func TestExpressions(t *testing.T) { `{a="aa", b="bb"} => 100 @[%v]`, }, }, - // Quantile too low. - { - expr: `histogram_quantile(-0.1, testhistogram_bucket)`, - output: []string{ - `{start="positive"} => -Inf @[%v]`, - `{start="negative"} => -Inf @[%v]`, - }, - }, - // Quantile too high. - { - expr: `histogram_quantile(1.01, testhistogram_bucket)`, - output: []string{ - `{start="positive"} => +Inf @[%v]`, - `{start="negative"} => +Inf @[%v]`, - }, - }, - // Quantile value in lowest bucket, which is positive. - { - expr: `histogram_quantile(0, testhistogram_bucket{start="positive"})`, - output: []string{ - `{start="positive"} => 0 @[%v]`, - }, - }, - // Quantile value in lowest bucket, which is negative. - { - expr: `histogram_quantile(0, testhistogram_bucket{start="negative"})`, - output: []string{ - `{start="negative"} => -0.2 @[%v]`, - }, - }, - // Quantile value in highest bucket. - { - expr: `histogram_quantile(1, testhistogram_bucket)`, - output: []string{ - `{start="positive"} => 1 @[%v]`, - `{start="negative"} => 0.3 @[%v]`, - }, - }, - // Finally some useful quantiles. - { - expr: `histogram_quantile(0.2, testhistogram_bucket)`, - output: []string{ - `{start="positive"} => 0.048 @[%v]`, - `{start="negative"} => -0.2 @[%v]`, - }, - }, - { - expr: `histogram_quantile(0.5, testhistogram_bucket)`, - output: []string{ - `{start="positive"} => 0.15 @[%v]`, - `{start="negative"} => -0.15 @[%v]`, - }, - }, - { - expr: `histogram_quantile(0.8, testhistogram_bucket)`, - output: []string{ - `{start="positive"} => 0.72 @[%v]`, - `{start="negative"} => 0.3 @[%v]`, - }, - }, - // More realistic with rates. - { - expr: `histogram_quantile(0.2, rate(testhistogram_bucket[5m]))`, - output: []string{ - `{start="positive"} => 0.048 @[%v]`, - `{start="negative"} => -0.2 @[%v]`, - }, - }, - { - expr: `histogram_quantile(0.5, rate(testhistogram_bucket[5m]))`, - output: []string{ - `{start="positive"} => 0.15 @[%v]`, - `{start="negative"} => -0.15 @[%v]`, - }, - }, - { - expr: `histogram_quantile(0.8, rate(testhistogram_bucket[5m]))`, - output: []string{ - `{start="positive"} => 0.72 @[%v]`, - `{start="negative"} => 0.3 @[%v]`, - }, - }, - // Aggregated histogram: Everything in one. - { - expr: `histogram_quantile(0.3, sum(rate(request_duration_seconds_bucket[5m])) by (le))`, - output: []string{ - `{} => 0.075 @[%v]`, - }, - }, - { - expr: `histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket[5m])) by (le))`, - output: []string{ - `{} => 0.1277777777777778 @[%v]`, - }, - }, - // Aggregated histogram: Everything in one. Now with avg, which does not change anything. - { - expr: `histogram_quantile(0.3, avg(rate(request_duration_seconds_bucket[5m])) by (le))`, - output: []string{ - `{} => 0.075 @[%v]`, - }, - }, - { - expr: `histogram_quantile(0.5, avg(rate(request_duration_seconds_bucket[5m])) by (le))`, - output: []string{ - `{} => 0.12777777777777778 @[%v]`, - }, - }, - // Aggregated histogram: By job. - { - expr: `histogram_quantile(0.3, sum(rate(request_duration_seconds_bucket[5m])) by (le, instance))`, - output: []string{ - `{instance="ins1"} => 0.075 @[%v]`, - `{instance="ins2"} => 0.075 @[%v]`, - }, - }, - { - expr: `histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket[5m])) by (le, instance))`, - output: []string{ - `{instance="ins1"} => 0.1333333333 @[%v]`, - `{instance="ins2"} => 0.125 @[%v]`, - }, - }, - // Aggregated histogram: By instance. - { - expr: `histogram_quantile(0.3, sum(rate(request_duration_seconds_bucket[5m])) by (le, job))`, - output: []string{ - `{job="job1"} => 0.1 @[%v]`, - `{job="job2"} => 0.0642857142857143 @[%v]`, - }, - }, - { - expr: `histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket[5m])) by (le, job))`, - output: []string{ - `{job="job1"} => 0.14 @[%v]`, - `{job="job2"} => 0.1125 @[%v]`, - }, - }, - // Aggregated histogram: By job and instance. - { - expr: `histogram_quantile(0.3, sum(rate(request_duration_seconds_bucket[5m])) by (le, job, instance))`, - output: []string{ - `{instance="ins1", job="job1"} => 0.11 @[%v]`, - `{instance="ins2", job="job1"} => 0.09 @[%v]`, - `{instance="ins1", job="job2"} => 0.06 @[%v]`, - `{instance="ins2", job="job2"} => 0.0675 @[%v]`, - }, - }, - { - expr: `histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket[5m])) by (le, job, instance))`, - output: []string{ - `{instance="ins1", job="job1"} => 0.15 @[%v]`, - `{instance="ins2", job="job1"} => 0.1333333333333333 @[%v]`, - `{instance="ins1", job="job2"} => 0.1 @[%v]`, - `{instance="ins2", job="job2"} => 0.1166666666666667 @[%v]`, - }, - }, - // The unaggregated histogram for comparison. Same result as the previous one. - { - expr: `histogram_quantile(0.3, rate(request_duration_seconds_bucket[5m]))`, - output: []string{ - `{instance="ins1", job="job1"} => 0.11 @[%v]`, - `{instance="ins2", job="job1"} => 0.09 @[%v]`, - `{instance="ins1", job="job2"} => 0.06 @[%v]`, - `{instance="ins2", job="job2"} => 0.0675 @[%v]`, - }, - }, - { - expr: `histogram_quantile(0.5, rate(request_duration_seconds_bucket[5m]))`, - output: []string{ - `{instance="ins1", job="job1"} => 0.15 @[%v]`, - `{instance="ins2", job="job1"} => 0.13333333333333333 @[%v]`, - `{instance="ins1", job="job2"} => 0.1 @[%v]`, - `{instance="ins2", job="job2"} => 0.11666666666666667 @[%v]`, - }, - }, { expr: `12.34e6`, output: []string{`scalar: 12340000 @[%v]`}, diff --git a/promql/setup_test.go b/promql/setup_test.go index 796c93896..a02ee5a66 100644 --- a/promql/setup_test.go +++ b/promql/setup_test.go @@ -209,91 +209,6 @@ var testMatrix = Matrix{ }, Values: getTestValueStream(0, 200, 20, testStartTime), }, - // Two histograms with 4 buckets each (*_sum and *_count not included, - // only buckets). Lowest bucket for one histogram < 0, for the other > - // 0. They have the same name, just separated by label. Not useful in - // practice, but can happen (if clients change bucketing), and the - // server has to cope with it. - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testhistogram_bucket", - "le": "0.1", - "start": "positive", - }, - }, - Values: getTestValueStream(0, 50, 5, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testhistogram_bucket", - "le": ".2", - "start": "positive", - }, - }, - Values: getTestValueStream(0, 70, 7, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testhistogram_bucket", - "le": "1e0", - "start": "positive", - }, - }, - Values: getTestValueStream(0, 110, 11, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testhistogram_bucket", - "le": "+Inf", - "start": "positive", - }, - }, - Values: getTestValueStream(0, 120, 12, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testhistogram_bucket", - "le": "-.2", - "start": "negative", - }, - }, - Values: getTestValueStream(0, 10, 1, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testhistogram_bucket", - "le": "-0.1", - "start": "negative", - }, - }, - Values: getTestValueStream(0, 20, 2, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testhistogram_bucket", - "le": "0.3", - "start": "negative", - }, - }, - Values: getTestValueStream(0, 20, 2, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testhistogram_bucket", - "le": "+Inf", - "start": "negative", - }, - }, - Values: getTestValueStream(0, 30, 3, testStartTime), - }, // Now a more realistic histogram per job and instance to test aggregation. { Metric: clientmodel.COWMetric{ diff --git a/promql/testdata/histograms.test b/promql/testdata/histograms.test new file mode 100644 index 000000000..6d1f16dfa --- /dev/null +++ b/promql/testdata/histograms.test @@ -0,0 +1,141 @@ +# Two histograms with 4 buckets each (x_sum and x_count not included, +# only buckets). Lowest bucket for one histogram < 0, for the other > +# 0. They have the same name, just separated by label. Not useful in +# practice, but can happen (if clients change bucketing), and the +# server has to cope with it. + +# Test histogram. +load 5m + testhistogram_bucket{le="0.1", start="positive"} 0+5x10 + testhistogram_bucket{le=".2", start="positive"} 0+7x10 + testhistogram_bucket{le="1e0", start="positive"} 0+11x10 + testhistogram_bucket{le="+Inf", start="positive"} 0+12x10 + testhistogram_bucket{le="-.2", start="negative"} 0+1x10 + testhistogram_bucket{le="-0.1", start="negative"} 0+2x10 + testhistogram_bucket{le="0.3", start="negative"} 0+2x10 + testhistogram_bucket{le="+Inf", start="negative"} 0+3x10 + + +# Now a more realistic histogram per job and instance to test aggregation. +load 5m + request_duration_seconds_bucket{job="job1", instance="ins1", le="0.1"} 0+1x10 + request_duration_seconds_bucket{job="job1", instance="ins1", le="0.2"} 0+3x10 + request_duration_seconds_bucket{job="job1", instance="ins1", le="+Inf"} 0+4x10 + request_duration_seconds_bucket{job="job1", instance="ins2", le="0.1"} 0+2x10 + request_duration_seconds_bucket{job="job1", instance="ins2", le="0.2"} 0+5x10 + request_duration_seconds_bucket{job="job1", instance="ins2", le="+Inf"} 0+6x10 + request_duration_seconds_bucket{job="job2", instance="ins1", le="0.1"} 0+3x10 + request_duration_seconds_bucket{job="job2", instance="ins1", le="0.2"} 0+4x10 + request_duration_seconds_bucket{job="job2", instance="ins1", le="+Inf"} 0+6x10 + request_duration_seconds_bucket{job="job2", instance="ins2", le="0.1"} 0+4x10 + request_duration_seconds_bucket{job="job2", instance="ins2", le="0.2"} 0+7x10 + request_duration_seconds_bucket{job="job2", instance="ins2", le="+Inf"} 0+9x10 + + +# Quantile too low. +eval instant at 50m histogram_quantile(-0.1, testhistogram_bucket) + {start="positive"} -Inf + {start="negative"} -Inf + +# Quantile too high. +eval instant at 50m histogram_quantile(1.01, testhistogram_bucket) + {start="positive"} +Inf + {start="negative"} +Inf + +# Quantile value in lowest bucket, which is positive. +eval instant at 50m histogram_quantile(0, testhistogram_bucket{start="positive"}) + {start="positive"} 0 + +# Quantile value in lowest bucket, which is negative. +eval instant at 50m histogram_quantile(0, testhistogram_bucket{start="negative"}) + {start="negative"} -0.2 + +# Quantile value in highest bucket. +eval instant at 50m histogram_quantile(1, testhistogram_bucket) + {start="positive"} 1 + {start="negative"} 0.3 + +# Finally some useful quantiles. +eval instant at 50m histogram_quantile(0.2, testhistogram_bucket) + {start="positive"} 0.048 + {start="negative"} -0.2 + + +eval instant at 50m histogram_quantile(0.5, testhistogram_bucket) + {start="positive"} 0.15 + {start="negative"} -0.15 + +eval instant at 50m histogram_quantile(0.8, testhistogram_bucket) + {start="positive"} 0.72 + {start="negative"} 0.3 + +# More realistic with rates. +eval instant at 50m histogram_quantile(0.2, rate(testhistogram_bucket[5m])) + {start="positive"} 0.048 + {start="negative"} -0.2 + +eval instant at 50m histogram_quantile(0.5, rate(testhistogram_bucket[5m])) + {start="positive"} 0.15 + {start="negative"} -0.15 + +eval instant at 50m histogram_quantile(0.8, rate(testhistogram_bucket[5m])) + {start="positive"} 0.72 + {start="negative"} 0.3 + +# Aggregated histogram: Everything in one. +eval instant at 50m histogram_quantile(0.3, sum(rate(request_duration_seconds_bucket[5m])) by (le)) + {} 0.075 + +eval instant at 50m histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket[5m])) by (le)) + {} 0.1277777777777778 + +# Aggregated histogram: Everything in one. Now with avg, which does not change anything. +eval instant at 50m histogram_quantile(0.3, avg(rate(request_duration_seconds_bucket[5m])) by (le)) + {} 0.075 + +eval instant at 50m histogram_quantile(0.5, avg(rate(request_duration_seconds_bucket[5m])) by (le)) + {} 0.12777777777777778 + +# Aggregated histogram: By job. +eval instant at 50m histogram_quantile(0.3, sum(rate(request_duration_seconds_bucket[5m])) by (le, instance)) + {instance="ins1"} 0.075 + {instance="ins2"} 0.075 + +eval instant at 50m histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket[5m])) by (le, instance)) + {instance="ins1"} 0.1333333333 + {instance="ins2"} 0.125 + +# Aggregated histogram: By instance. +eval instant at 50m histogram_quantile(0.3, sum(rate(request_duration_seconds_bucket[5m])) by (le, job)) + {job="job1"} 0.1 + {job="job2"} 0.0642857142857143 + +eval instant at 50m histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket[5m])) by (le, job)) + {job="job1"} 0.14 + {job="job2"} 0.1125 + +# Aggregated histogram: By job and instance. +eval instant at 50m histogram_quantile(0.3, sum(rate(request_duration_seconds_bucket[5m])) by (le, job, instance)) + {instance="ins1", job="job1"} 0.11 + {instance="ins2", job="job1"} 0.09 + {instance="ins1", job="job2"} 0.06 + {instance="ins2", job="job2"} 0.0675 + +eval instant at 50m histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket[5m])) by (le, job, instance)) + {instance="ins1", job="job1"} 0.15 + {instance="ins2", job="job1"} 0.1333333333333333 + {instance="ins1", job="job2"} 0.1 + {instance="ins2", job="job2"} 0.1166666666666667 + +# The unaggregated histogram for comparison. Same result as the previous one. +eval instant at 50m histogram_quantile(0.3, rate(request_duration_seconds_bucket[5m])) + {instance="ins1", job="job1"} 0.11 + {instance="ins2", job="job1"} 0.09 + {instance="ins1", job="job2"} 0.06 + {instance="ins2", job="job2"} 0.0675 + +eval instant at 50m histogram_quantile(0.5, rate(request_duration_seconds_bucket[5m])) + {instance="ins1", job="job1"} 0.15 + {instance="ins2", job="job1"} 0.13333333333333333 + {instance="ins1", job="job2"} 0.1 + {instance="ins2", job="job2"} 0.11666666666666667 \ No newline at end of file From 3c22eded97e8b87cc9ebf178e187b8e72472befc Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Tue, 12 May 2015 12:25:03 +0200 Subject: [PATCH 09/13] Migrate literal tests to testing language. --- promql/promql_test.go | 80 ----------------------------------- promql/testdata/literals.test | 56 ++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 80 deletions(-) create mode 100644 promql/testdata/literals.test diff --git a/promql/promql_test.go b/promql/promql_test.go index acc2d8bfd..92ebeee9b 100644 --- a/promql/promql_test.go +++ b/promql/promql_test.go @@ -926,86 +926,6 @@ func TestExpressions(t *testing.T) { `{a="aa", b="bb"} => 100 @[%v]`, }, }, - { - expr: `12.34e6`, - output: []string{`scalar: 12340000 @[%v]`}, - }, - { - expr: `12.34e+6`, - output: []string{`scalar: 12340000 @[%v]`}, - }, - { - expr: `12.34e-6`, - output: []string{`scalar: 0.00001234 @[%v]`}, - }, - { - expr: `1+1`, - output: []string{`scalar: 2 @[%v]`}, - }, - { - expr: `1-1`, - output: []string{`scalar: 0 @[%v]`}, - }, - { - expr: `1 - -1`, - output: []string{`scalar: 2 @[%v]`}, - }, - { - expr: `.2`, - output: []string{`scalar: 0.2 @[%v]`}, - }, - { - expr: `+0.2`, - output: []string{`scalar: 0.2 @[%v]`}, - }, - { - expr: `-0.2e-6`, - output: []string{`scalar: -0.0000002 @[%v]`}, - }, - { - expr: `+Inf`, - output: []string{`scalar: +Inf @[%v]`}, - }, - { - expr: `inF`, - output: []string{`scalar: +Inf @[%v]`}, - }, - { - expr: `-inf`, - output: []string{`scalar: -Inf @[%v]`}, - }, - { - expr: `NaN`, - output: []string{`scalar: NaN @[%v]`}, - }, - { - expr: `nan`, - output: []string{`scalar: NaN @[%v]`}, - }, - { - expr: `2.`, - output: []string{`scalar: 2 @[%v]`}, - }, - { - expr: `999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999`, - shouldFail: true, - }, - { - expr: `1 / 0`, - output: []string{`scalar: +Inf @[%v]`}, - }, - { - expr: `-1 / 0`, - output: []string{`scalar: -Inf @[%v]`}, - }, - { - expr: `0 / 0`, - output: []string{`scalar: NaN @[%v]`}, - }, - { - expr: `1 % 0`, - output: []string{`scalar: NaN @[%v]`}, - }, { expr: `http_requests{group="canary", instance="0", job="api-server"} / 0`, output: []string{ diff --git a/promql/testdata/literals.test b/promql/testdata/literals.test new file mode 100644 index 000000000..ffca00e1c --- /dev/null +++ b/promql/testdata/literals.test @@ -0,0 +1,56 @@ +eval instant at 50m 12.34e6 + 12340000 + +eval instant at 50m 12.34e+6 + 12340000 + +eval instant at 50m 12.34e-6 + 0.00001234 + +eval instant at 50m 1+1 + 2 + +eval instant at 50m 1-1 + 0 + +eval instant at 50m 1 - -1 + 2 + +eval instant at 50m .2 + 0.2 + +eval instant at 50m +0.2 + 0.2 + +eval instant at 50m -0.2e-6 + -0.0000002 + +eval instant at 50m +Inf + +Inf + +eval instant at 50m inF + +Inf + +eval instant at 50m -inf + -Inf + +eval instant at 50m NaN + NaN + +eval instant at 50m nan + NaN + +eval instant at 50m 2. + 2 + +eval instant at 50m 1 / 0 + +Inf + +eval instant at 50m -1 / 0 + -Inf + +eval instant at 50m 0 / 0 + NaN + +eval instant at 50m 1 % 0 + NaN \ No newline at end of file From 71ef7ab405532a5ff185c7bc102dda23c5351458 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Tue, 12 May 2015 13:33:17 +0200 Subject: [PATCH 10/13] Migrate remaining vector evaluation tests to new testing language. --- promql/promql_test.go | 1038 ----------------------------------- promql/testdata/legacy.test | 638 +++++++++++++++++++++ 2 files changed, 638 insertions(+), 1038 deletions(-) create mode 100644 promql/testdata/legacy.test diff --git a/promql/promql_test.go b/promql/promql_test.go index 92ebeee9b..0a7f45271 100644 --- a/promql/promql_test.go +++ b/promql/promql_test.go @@ -111,1044 +111,6 @@ func newTestStorage(t testing.TB) (storage local.Storage, closer test.Closer) { return storage, closer } -func TestExpressions(t *testing.T) { - // Labels in expected output need to be alphabetically sorted. - expressionTests := []struct { - expr string - output []string - shouldFail bool - checkOrder bool - }{ - { - expr: `SUM(http_requests)`, - output: []string{`{} => 3600 @[%v]`}, - }, { - expr: `SUM(http_requests{instance="0"}) BY(job)`, - output: []string{ - `{job="api-server"} => 400 @[%v]`, - `{job="app-server"} => 1200 @[%v]`, - }, - }, { - expr: `SUM(http_requests{instance="0"}) BY(job) KEEPING_EXTRA`, - output: []string{ - `{instance="0", job="api-server"} => 400 @[%v]`, - `{instance="0", job="app-server"} => 1200 @[%v]`, - }, - }, { - expr: `SUM(http_requests) BY (job)`, - output: []string{ - `{job="api-server"} => 1000 @[%v]`, - `{job="app-server"} => 2600 @[%v]`, - }, - }, { - // Non-existent labels mentioned in BY-clauses shouldn't propagate to output. - expr: `SUM(http_requests) BY (job, nonexistent)`, - output: []string{ - `{job="api-server"} => 1000 @[%v]`, - `{job="app-server"} => 2600 @[%v]`, - }, - }, { - expr: ` - # Test comment. - SUM(http_requests) BY # comments shouldn't have any effect - (job) # another comment`, - output: []string{ - `{job="api-server"} => 1000 @[%v]`, - `{job="app-server"} => 2600 @[%v]`, - }, - }, { - expr: `COUNT(http_requests) BY (job)`, - output: []string{ - `{job="api-server"} => 4 @[%v]`, - `{job="app-server"} => 4 @[%v]`, - }, - }, { - expr: `SUM(http_requests) BY (job, group)`, - output: []string{ - `{group="canary", job="api-server"} => 700 @[%v]`, - `{group="canary", job="app-server"} => 1500 @[%v]`, - `{group="production", job="api-server"} => 300 @[%v]`, - `{group="production", job="app-server"} => 1100 @[%v]`, - }, - }, { - expr: `AVG(http_requests) BY (job)`, - output: []string{ - `{job="api-server"} => 250 @[%v]`, - `{job="app-server"} => 650 @[%v]`, - }, - }, { - expr: `MIN(http_requests) BY (job)`, - output: []string{ - `{job="api-server"} => 100 @[%v]`, - `{job="app-server"} => 500 @[%v]`, - }, - }, { - expr: `MAX(http_requests) BY (job)`, - output: []string{ - `{job="api-server"} => 400 @[%v]`, - `{job="app-server"} => 800 @[%v]`, - }, - }, { - expr: `SUM(http_requests) BY (job) - COUNT(http_requests) BY (job)`, - output: []string{ - `{job="api-server"} => 996 @[%v]`, - `{job="app-server"} => 2596 @[%v]`, - }, - }, { - expr: `2 - SUM(http_requests) BY (job)`, - output: []string{ - `{job="api-server"} => -998 @[%v]`, - `{job="app-server"} => -2598 @[%v]`, - }, - }, { - expr: `1000 / SUM(http_requests) BY (job)`, - output: []string{ - `{job="api-server"} => 1 @[%v]`, - `{job="app-server"} => 0.38461538461538464 @[%v]`, - }, - }, { - expr: `SUM(http_requests) BY (job) - 2`, - output: []string{ - `{job="api-server"} => 998 @[%v]`, - `{job="app-server"} => 2598 @[%v]`, - }, - }, { - expr: `SUM(http_requests) BY (job) % 3`, - output: []string{ - `{job="api-server"} => 1 @[%v]`, - `{job="app-server"} => 2 @[%v]`, - }, - }, { - expr: `SUM(http_requests) BY (job) / 0`, - output: []string{ - `{job="api-server"} => +Inf @[%v]`, - `{job="app-server"} => +Inf @[%v]`, - }, - }, { - expr: `SUM(http_requests) BY (job) > 1000`, - output: []string{ - `{job="app-server"} => 2600 @[%v]`, - }, - }, { - expr: `1000 < SUM(http_requests) BY (job)`, - output: []string{ - `{job="app-server"} => 1000 @[%v]`, - }, - }, { - expr: `SUM(http_requests) BY (job) <= 1000`, - output: []string{ - `{job="api-server"} => 1000 @[%v]`, - }, - }, { - expr: `SUM(http_requests) BY (job) != 1000`, - output: []string{ - `{job="app-server"} => 2600 @[%v]`, - }, - }, { - expr: `SUM(http_requests) BY (job) == 1000`, - output: []string{ - `{job="api-server"} => 1000 @[%v]`, - }, - }, { - expr: `SUM(http_requests) BY (job) + SUM(http_requests) BY (job)`, - output: []string{ - `{job="api-server"} => 2000 @[%v]`, - `{job="app-server"} => 5200 @[%v]`, - }, - }, { - expr: `http_requests{job="api-server", group="canary"}`, - output: []string{ - `http_requests{group="canary", instance="0", job="api-server"} => 300 @[%v]`, - `http_requests{group="canary", instance="1", job="api-server"} => 400 @[%v]`, - }, - }, { - expr: `http_requests{job="api-server", group="canary"} + rate(http_requests{job="api-server"}[5m]) * 5 * 60`, - output: []string{ - `{group="canary", instance="0", job="api-server"} => 330 @[%v]`, - `{group="canary", instance="1", job="api-server"} => 440 @[%v]`, - }, - }, { - expr: `rate(http_requests[25m]) * 25 * 60`, - output: []string{ - `{group="canary", instance="0", job="api-server"} => 150 @[%v]`, - `{group="canary", instance="0", job="app-server"} => 350 @[%v]`, - `{group="canary", instance="1", job="api-server"} => 200 @[%v]`, - `{group="canary", instance="1", job="app-server"} => 400 @[%v]`, - `{group="production", instance="0", job="api-server"} => 50 @[%v]`, - `{group="production", instance="0", job="app-server"} => 249.99999999999997 @[%v]`, - `{group="production", instance="1", job="api-server"} => 100 @[%v]`, - `{group="production", instance="1", job="app-server"} => 300 @[%v]`, - }, - }, { - expr: `delta(http_requests[25m], 1)`, - output: []string{ - `{group="canary", instance="0", job="api-server"} => 150 @[%v]`, - `{group="canary", instance="0", job="app-server"} => 350 @[%v]`, - `{group="canary", instance="1", job="api-server"} => 200 @[%v]`, - `{group="canary", instance="1", job="app-server"} => 400 @[%v]`, - `{group="production", instance="0", job="api-server"} => 50 @[%v]`, - `{group="production", instance="0", job="app-server"} => 250 @[%v]`, - `{group="production", instance="1", job="api-server"} => 100 @[%v]`, - `{group="production", instance="1", job="app-server"} => 300 @[%v]`, - }, - }, - { - expr: `sort(http_requests)`, - output: []string{ - `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, - `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, - `http_requests{group="canary", instance="0", job="api-server"} => 300 @[%v]`, - `http_requests{group="canary", instance="1", job="api-server"} => 400 @[%v]`, - `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, - `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, - `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, - `http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`, - }, - checkOrder: true, - }, { - expr: `sort(0 / round(http_requests, 400) + http_requests)`, - output: []string{ - `{group="production", instance="0", job="api-server"} => NaN @[%v]`, - `{group="production", instance="1", job="api-server"} => 200 @[%v]`, - `{group="canary", instance="0", job="api-server"} => 300 @[%v]`, - `{group="canary", instance="1", job="api-server"} => 400 @[%v]`, - `{group="production", instance="0", job="app-server"} => 500 @[%v]`, - `{group="production", instance="1", job="app-server"} => 600 @[%v]`, - `{group="canary", instance="0", job="app-server"} => 700 @[%v]`, - `{group="canary", instance="1", job="app-server"} => 800 @[%v]`, - }, - checkOrder: true, - }, { - expr: `sort_desc(http_requests)`, - output: []string{ - `http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`, - `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, - `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, - `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, - `http_requests{group="canary", instance="1", job="api-server"} => 400 @[%v]`, - `http_requests{group="canary", instance="0", job="api-server"} => 300 @[%v]`, - `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, - `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, - }, - checkOrder: true, - }, - { - expr: `topk(3, http_requests)`, - output: []string{ - `http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`, - `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, - `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, - }, - checkOrder: true, - }, { - expr: `topk(5, http_requests{group="canary",job="app-server"})`, - output: []string{ - `http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`, - `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, - }, - checkOrder: true, - }, { - expr: `bottomk(3, http_requests)`, - output: []string{ - `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, - `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, - `http_requests{group="canary", instance="0", job="api-server"} => 300 @[%v]`, - }, - checkOrder: true, - }, { - expr: `bottomk(5, http_requests{group="canary",job="app-server"})`, - output: []string{ - `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, - `http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`, - }, - checkOrder: true, - }, - { - // Single-letter label names and values. - expr: `x{y="testvalue"}`, - output: []string{ - `x{y="testvalue"} => 100 @[%v]`, - }, - }, { - // Lower-cased aggregation operators should work too. - expr: `sum(http_requests) by (job) + min(http_requests) by (job) + max(http_requests) by (job) + avg(http_requests) by (job)`, - output: []string{ - `{job="app-server"} => 4550 @[%v]`, - `{job="api-server"} => 1750 @[%v]`, - }, - }, { - // Deltas should be adjusted for target interval vs. samples under target interval. - expr: `delta(http_requests{group="canary", instance="1", job="app-server"}[18m])`, - output: []string{`{group="canary", instance="1", job="app-server"} => 288 @[%v]`}, - }, { - // Deltas should perform the same operation when 2nd argument is 0. - expr: `delta(http_requests{group="canary", instance="1", job="app-server"}[18m], 0)`, - output: []string{`{group="canary", instance="1", job="app-server"} => 288 @[%v]`}, - }, { - // Rates should calculate per-second rates. - expr: `rate(http_requests{group="canary", instance="1", job="app-server"}[60m])`, - output: []string{`{group="canary", instance="1", job="app-server"} => 0.26666666666666666 @[%v]`}, - }, - { - // Deriv should return the same as rate in simple cases. - expr: `deriv(http_requests{group="canary", instance="1", job="app-server"}[60m])`, - output: []string{`{group="canary", instance="1", job="app-server"} => 0.26666666666666666 @[%v]`}, - }, - { - // Counter resets at in the middle of range are handled correctly by rate(). - expr: `rate(testcounter_reset_middle[60m])`, - output: []string{`{} => 0.03 @[%v]`}, - }, { - // Counter resets at end of range are ignored by rate(). - expr: `rate(testcounter_reset_end[5m])`, - output: []string{`{} => 0 @[%v]`}, - }, - { - // Deriv should return correct result. - expr: `deriv(testcounter_reset_middle[100m])`, - output: []string{`{} => 0.010606060606060607 @[%v]`}, - }, - { - // count_scalar for a non-empty vector should return scalar element count. - expr: `count_scalar(http_requests)`, - output: []string{`scalar: 8 @[%v]`}, - }, { - // count_scalar for an empty vector should return scalar 0. - expr: `count_scalar(nonexistent)`, - output: []string{`scalar: 0 @[%v]`}, - }, { - // Empty expressions shouldn't parse. - expr: ``, - shouldFail: true, - }, { - // Interval durations can't be in quotes. - expr: `http_requests["1m"]`, - shouldFail: true, - }, { - // Binop arguments need to be scalar or vector. - expr: `http_requests - http_requests[1m]`, - shouldFail: true, - }, { - expr: `http_requests{group!="canary"}`, - output: []string{ - `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, - `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, - `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, - `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, - }, - }, { - expr: `http_requests{job=~"server",group!="canary"}`, - output: []string{ - `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, - `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, - `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, - `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, - }, - }, { - expr: `http_requests{job!~"api",group!="canary"}`, - output: []string{ - `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, - `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, - }, - }, { - expr: `count_scalar(http_requests{job=~"^server$"})`, - output: []string{`scalar: 0 @[%v]`}, - }, { - expr: `http_requests{group="production",job=~"^api"}`, - output: []string{ - `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, - `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, - }, - }, - { - expr: `abs(-1 * http_requests{group="production",job="api-server"})`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 100 @[%v]`, - `{group="production", instance="1", job="api-server"} => 200 @[%v]`, - }, - }, - { - expr: `floor(0.004 * http_requests{group="production",job="api-server"})`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 0 @[%v]`, - `{group="production", instance="1", job="api-server"} => 0 @[%v]`, - }, - }, - { - expr: `ceil(0.004 * http_requests{group="production",job="api-server"})`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 1 @[%v]`, - `{group="production", instance="1", job="api-server"} => 1 @[%v]`, - }, - }, - { - expr: `round(0.004 * http_requests{group="production",job="api-server"})`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 0 @[%v]`, - `{group="production", instance="1", job="api-server"} => 1 @[%v]`, - }, - }, - { // Round should correctly handle negative numbers. - expr: `round(-1 * (0.004 * http_requests{group="production",job="api-server"}))`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 0 @[%v]`, - `{group="production", instance="1", job="api-server"} => -1 @[%v]`, - }, - }, - { // Round should round half up. - expr: `round(0.005 * http_requests{group="production",job="api-server"})`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 1 @[%v]`, - `{group="production", instance="1", job="api-server"} => 1 @[%v]`, - }, - }, - { - expr: `round(-1 * (0.005 * http_requests{group="production",job="api-server"}))`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 0 @[%v]`, - `{group="production", instance="1", job="api-server"} => -1 @[%v]`, - }, - }, - { - expr: `round(1 + 0.005 * http_requests{group="production",job="api-server"})`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 2 @[%v]`, - `{group="production", instance="1", job="api-server"} => 2 @[%v]`, - }, - }, - { - expr: `round(-1 * (1 + 0.005 * http_requests{group="production",job="api-server"}))`, - output: []string{ - `{group="production", instance="0", job="api-server"} => -1 @[%v]`, - `{group="production", instance="1", job="api-server"} => -2 @[%v]`, - }, - }, - { // Round should accept the number to round nearest to. - expr: `round(0.0005 * http_requests{group="production",job="api-server"}, 0.1)`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 0.1 @[%v]`, - `{group="production", instance="1", job="api-server"} => 0.1 @[%v]`, - }, - }, - { - expr: `round(2.1 + 0.0005 * http_requests{group="production",job="api-server"}, 0.1)`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 2.2 @[%v]`, - `{group="production", instance="1", job="api-server"} => 2.2 @[%v]`, - }, - }, - { - expr: `round(5.2 + 0.0005 * http_requests{group="production",job="api-server"}, 0.1)`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 5.3 @[%v]`, - `{group="production", instance="1", job="api-server"} => 5.3 @[%v]`, - }, - }, - { // Round should work correctly with negative numbers and multiple decimal places. - expr: `round(-1 * (5.2 + 0.0005 * http_requests{group="production",job="api-server"}), 0.1)`, - output: []string{ - `{group="production", instance="0", job="api-server"} => -5.2 @[%v]`, - `{group="production", instance="1", job="api-server"} => -5.3 @[%v]`, - }, - }, - { // Round should work correctly with big toNearests. - expr: `round(0.025 * http_requests{group="production",job="api-server"}, 5)`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 5 @[%v]`, - `{group="production", instance="1", job="api-server"} => 5 @[%v]`, - }, - }, - { - expr: `round(0.045 * http_requests{group="production",job="api-server"}, 5)`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 5 @[%v]`, - `{group="production", instance="1", job="api-server"} => 10 @[%v]`, - }, - }, - { - expr: `avg_over_time(http_requests{group="production",job="api-server"}[1h])`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 50 @[%v]`, - `{group="production", instance="1", job="api-server"} => 100 @[%v]`, - }, - }, - { - expr: `count_over_time(http_requests{group="production",job="api-server"}[1h])`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 11 @[%v]`, - `{group="production", instance="1", job="api-server"} => 11 @[%v]`, - }, - }, - { - expr: `max_over_time(http_requests{group="production",job="api-server"}[1h])`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 100 @[%v]`, - `{group="production", instance="1", job="api-server"} => 200 @[%v]`, - }, - }, - { - expr: `min_over_time(http_requests{group="production",job="api-server"}[1h])`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 0 @[%v]`, - `{group="production", instance="1", job="api-server"} => 0 @[%v]`, - }, - }, - { - expr: `sum_over_time(http_requests{group="production",job="api-server"}[1h])`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 550 @[%v]`, - `{group="production", instance="1", job="api-server"} => 1100 @[%v]`, - }, - }, - { - expr: `time()`, - output: []string{`scalar: 3000 @[%v]`}, - }, - { - expr: `drop_common_labels(http_requests{group="production",job="api-server"})`, - output: []string{ - `http_requests{instance="0"} => 100 @[%v]`, - `http_requests{instance="1"} => 200 @[%v]`, - }, - }, - { - expr: `{` + string(clientmodel.MetricNameLabel) + `=~".*"}`, - output: []string{ - `http_requests{group="canary", instance="0", job="api-server"} => 300 @[%v]`, - `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, - `http_requests{group="canary", instance="1", job="api-server"} => 400 @[%v]`, - `http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`, - `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, - `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, - `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, - `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, - `testcounter_reset_end => 0 @[%v]`, - `testcounter_reset_middle => 50 @[%v]`, - `x{y="testvalue"} => 100 @[%v]`, - `label_grouping_test{a="a", b="abb"} => 200 @[%v]`, - `label_grouping_test{a="aa", b="bb"} => 100 @[%v]`, - `request_duration_seconds_bucket{instance="ins1", job="job1", le="0.1"} => 10 @[%v]`, - `request_duration_seconds_bucket{instance="ins1", job="job1", le="0.2"} => 30 @[%v]`, - `request_duration_seconds_bucket{instance="ins1", job="job1", le="+Inf"} => 40 @[%v]`, - `request_duration_seconds_bucket{instance="ins2", job="job1", le="0.1"} => 20 @[%v]`, - `request_duration_seconds_bucket{instance="ins2", job="job1", le="0.2"} => 50 @[%v]`, - `request_duration_seconds_bucket{instance="ins2", job="job1", le="+Inf"} => 60 @[%v]`, - `request_duration_seconds_bucket{instance="ins1", job="job2", le="0.1"} => 30 @[%v]`, - `request_duration_seconds_bucket{instance="ins1", job="job2", le="0.2"} => 40 @[%v]`, - `request_duration_seconds_bucket{instance="ins1", job="job2", le="+Inf"} => 60 @[%v]`, - `request_duration_seconds_bucket{instance="ins2", job="job2", le="0.1"} => 40 @[%v]`, - `request_duration_seconds_bucket{instance="ins2", job="job2", le="0.2"} => 70 @[%v]`, - `request_duration_seconds_bucket{instance="ins2", job="job2", le="+Inf"} => 90 @[%v]`, - `vector_matching_a{l="x"} => 10 @[%v]`, - `vector_matching_a{l="y"} => 20 @[%v]`, - `vector_matching_b{l="x"} => 40 @[%v]`, - `cpu_count{instance="1", type="smp"} => 200 @[%v]`, - `cpu_count{instance="0", type="smp"} => 100 @[%v]`, - `cpu_count{instance="0", type="numa"} => 300 @[%v]`, - }, - }, - { - expr: `{job=~"server", job!~"api"}`, - output: []string{ - `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, - `http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`, - `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, - `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, - }, - }, - { - // Test alternative "by"-clause order. - expr: `sum by (group) (http_requests{job="api-server"})`, - output: []string{ - `{group="canary"} => 700 @[%v]`, - `{group="production"} => 300 @[%v]`, - }, - }, - { - // Test alternative "by"-clause order with "keeping_extra". - expr: `sum by (group) keeping_extra (http_requests{job="api-server"})`, - output: []string{ - `{group="canary", job="api-server"} => 700 @[%v]`, - `{group="production", job="api-server"} => 300 @[%v]`, - }, - }, - { - // Test both alternative "by"-clause orders in one expression. - // Public health warning: stick to one form within an expression (or even - // in an organization), or risk serious user confusion. - expr: `sum(sum by (group) keeping_extra (http_requests{job="api-server"})) by (job)`, - output: []string{ - `{job="api-server"} => 1000 @[%v]`, - }, - }, - { - expr: `http_requests{group="canary"} and http_requests{instance="0"}`, - output: []string{ - `http_requests{group="canary", instance="0", job="api-server"} => 300 @[%v]`, - `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, - }, - }, - { - expr: `(http_requests{group="canary"} + 1) and http_requests{instance="0"}`, - output: []string{ - `{group="canary", instance="0", job="api-server"} => 301 @[%v]`, - `{group="canary", instance="0", job="app-server"} => 701 @[%v]`, - }, - }, - { - expr: `(http_requests{group="canary"} + 1) and on(instance, job) http_requests{instance="0", group="production"}`, - output: []string{ - `{group="canary", instance="0", job="api-server"} => 301 @[%v]`, - `{group="canary", instance="0", job="app-server"} => 701 @[%v]`, - }, - }, - { - expr: `(http_requests{group="canary"} + 1) and on(instance) http_requests{instance="0", group="production"}`, - output: []string{ - `{group="canary", instance="0", job="api-server"} => 301 @[%v]`, - `{group="canary", instance="0", job="app-server"} => 701 @[%v]`, - }, - }, - { - expr: `http_requests{group="canary"} or http_requests{group="production"}`, - output: []string{ - `http_requests{group="canary", instance="0", job="api-server"} => 300 @[%v]`, - `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, - `http_requests{group="canary", instance="1", job="api-server"} => 400 @[%v]`, - `http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`, - `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, - `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, - `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, - `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, - }, - }, - { - // On overlap the rhs samples must be dropped. - expr: `(http_requests{group="canary"} + 1) or http_requests{instance="1"}`, - output: []string{ - `{group="canary", instance="0", job="api-server"} => 301 @[%v]`, - `{group="canary", instance="0", job="app-server"} => 701 @[%v]`, - `{group="canary", instance="1", job="api-server"} => 401 @[%v]`, - `{group="canary", instance="1", job="app-server"} => 801 @[%v]`, - `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, - `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, - }, - }, - { - // Matching only on instance excludes everything that has instance=0/1 but includes - // entries without the instance label. - expr: `(http_requests{group="canary"} + 1) or on(instance) (http_requests or cpu_count or vector_matching_a)`, - output: []string{ - `{group="canary", instance="0", job="api-server"} => 301 @[%v]`, - `{group="canary", instance="0", job="app-server"} => 701 @[%v]`, - `{group="canary", instance="1", job="api-server"} => 401 @[%v]`, - `{group="canary", instance="1", job="app-server"} => 801 @[%v]`, - `vector_matching_a{l="x"} => 10 @[%v]`, - `vector_matching_a{l="y"} => 20 @[%v]`, - }, - }, - { - expr: `http_requests{group="canary"} / on(instance,job) http_requests{group="production"}`, - output: []string{ - `{instance="0", job="api-server"} => 3 @[%v]`, - `{instance="0", job="app-server"} => 1.4 @[%v]`, - `{instance="1", job="api-server"} => 2 @[%v]`, - `{instance="1", job="app-server"} => 1.3333333333333333 @[%v]`, - }, - }, - { - // Include labels must guarantee uniquely identifiable time series. - expr: `http_requests{group="production"} / on(instance) group_left(group) cpu_count{type="smp"}`, - shouldFail: true, - }, - { - // Many-to-many matching is not allowed. - expr: `http_requests{group="production"} / on(instance) group_left(job,type) cpu_count`, - shouldFail: true, - }, - { - // Many-to-one matching must be explicit. - expr: `http_requests{group="production"} / on(instance) cpu_count{type="smp"}`, - shouldFail: true, - }, - { - expr: `http_requests{group="production"} / on(instance) group_left(job) cpu_count{type="smp"}`, - output: []string{ - `{instance="1", job="api-server"} => 1 @[%v]`, - `{instance="0", job="app-server"} => 5 @[%v]`, - `{instance="1", job="app-server"} => 3 @[%v]`, - `{instance="0", job="api-server"} => 1 @[%v]`, - }, - }, - { - // Ensure sidedness of grouping preserves operand sides. - expr: `cpu_count{type="smp"} / on(instance) group_right(job) http_requests{group="production"}`, - output: []string{ - `{instance="1", job="app-server"} => 0.3333333333333333 @[%v]`, - `{instance="0", job="app-server"} => 0.2 @[%v]`, - `{instance="1", job="api-server"} => 1 @[%v]`, - `{instance="0", job="api-server"} => 1 @[%v]`, - }, - }, - { - // Include labels from both sides. - expr: `http_requests{group="production"} / on(instance) group_left(job) cpu_count{type="smp"}`, - output: []string{ - `{instance="1", job="api-server"} => 1 @[%v]`, - `{instance="0", job="app-server"} => 5 @[%v]`, - `{instance="1", job="app-server"} => 3 @[%v]`, - `{instance="0", job="api-server"} => 1 @[%v]`, - }, - }, - { - expr: `http_requests{group="production"} < on(instance,job) http_requests{group="canary"}`, - output: []string{ - `{instance="1", job="app-server"} => 600 @[%v]`, - `{instance="0", job="app-server"} => 500 @[%v]`, - `{instance="1", job="api-server"} => 200 @[%v]`, - `{instance="0", job="api-server"} => 100 @[%v]`, - }, - }, - { - expr: `http_requests{group="production"} > on(instance,job) http_requests{group="canary"}`, - output: []string{}, - }, - { - expr: `http_requests{group="production"} == on(instance,job) http_requests{group="canary"}`, - output: []string{}, - }, - { - expr: `http_requests > on(instance) group_left(group,job) cpu_count{type="smp"}`, - output: []string{ - `{group="canary", instance="0", job="app-server"} => 700 @[%v]`, - `{group="canary", instance="1", job="app-server"} => 800 @[%v]`, - `{group="canary", instance="0", job="api-server"} => 300 @[%v]`, - `{group="canary", instance="1", job="api-server"} => 400 @[%v]`, - `{group="production", instance="0", job="app-server"} => 500 @[%v]`, - `{group="production", instance="1", job="app-server"} => 600 @[%v]`, - }, - }, - { - expr: `http_requests / on(instance) 3`, - shouldFail: true, - }, - { - expr: `3 / on(instance) http_requests_total`, - shouldFail: true, - }, - { - expr: `3 / on(instance) 3`, - shouldFail: true, - }, - { - // Missing label list for grouping mod. - expr: `http_requests{group="production"} / on(instance) group_left cpu_count{type="smp"}`, - shouldFail: true, - }, - { - // No group mod allowed for logical operations. - expr: `http_requests{group="production"} or on(instance) group_left(type) cpu_count{type="smp"}`, - shouldFail: true, - }, - { - // No group mod allowed for logical operations. - expr: `http_requests{group="production"} and on(instance) group_left(type) cpu_count{type="smp"}`, - shouldFail: true, - }, - { - // No duplicate use of label. - expr: `http_requests{group="production"} + on(instance) group_left(job,instance) cpu_count{type="smp"}`, - shouldFail: true, - }, - { - expr: `{l="x"} + on(__name__) {l="y"}`, - output: []string{ - `vector_matching_a => 30 @[%v]`, - }, - }, - { - expr: `absent(nonexistent)`, - output: []string{ - `{} => 1 @[%v]`, - }, - }, - { - expr: `absent(nonexistent{job="testjob", instance="testinstance", method=~".*"})`, - output: []string{ - `{instance="testinstance", job="testjob"} => 1 @[%v]`, - }, - }, - { - expr: `count_scalar(absent(http_requests))`, - output: []string{ - `scalar: 0 @[%v]`, - }, - }, - { - expr: `count_scalar(absent(sum(http_requests)))`, - output: []string{ - `scalar: 0 @[%v]`, - }, - }, - { - expr: `absent(sum(nonexistent{job="testjob", instance="testinstance"}))`, - output: []string{ - `{} => 1 @[%v]`, - }, - }, - { - expr: `http_requests{group="production",job="api-server"} offset 5m`, - output: []string{ - `http_requests{group="production", instance="0", job="api-server"} => 90 @[%v]`, - `http_requests{group="production", instance="1", job="api-server"} => 180 @[%v]`, - }, - }, - { - expr: `rate(http_requests{group="production",job="api-server"}[10m] offset 5m)`, - output: []string{ - `{group="production", instance="0", job="api-server"} => 0.03333333333333333 @[%v]`, - `{group="production", instance="1", job="api-server"} => 0.06666666666666667 @[%v]`, - }, - }, - { - expr: `rate(http_requests[10m]) offset 5m`, - shouldFail: true, - }, - { - expr: `sum(http_requests) offset 5m`, - shouldFail: true, - }, - // Regression test for missing separator byte in labelsToGroupingKey. - { - expr: `sum(label_grouping_test) by (a, b)`, - output: []string{ - `{a="a", b="abb"} => 200 @[%v]`, - `{a="aa", b="bb"} => 100 @[%v]`, - }, - }, - { - expr: `http_requests{group="canary", instance="0", job="api-server"} / 0`, - output: []string{ - `{group="canary", instance="0", job="api-server"} => +Inf @[%v]`, - }, - }, - { - expr: `-1 * http_requests{group="canary", instance="0", job="api-server"} / 0`, - output: []string{ - `{group="canary", instance="0", job="api-server"} => -Inf @[%v]`, - }, - }, - { - expr: `0 * http_requests{group="canary", instance="0", job="api-server"} / 0`, - output: []string{ - `{group="canary", instance="0", job="api-server"} => NaN @[%v]`, - }, - }, - { - expr: `0 * http_requests{group="canary", instance="0", job="api-server"} % 0`, - output: []string{ - `{group="canary", instance="0", job="api-server"} => NaN @[%v]`, - }, - }, - { - expr: `exp(vector_matching_a)`, - output: []string{ - `{l="x"} => 22026.465794806718 @[%v]`, - `{l="y"} => 485165195.4097903 @[%v]`, - }, - }, - { - expr: `exp(vector_matching_a - 10)`, - output: []string{ - `{l="y"} => 22026.465794806718 @[%v]`, - `{l="x"} => 1 @[%v]`, - }, - }, - { - expr: `exp(vector_matching_a - 20)`, - output: []string{ - `{l="x"} => 4.5399929762484854e-05 @[%v]`, - `{l="y"} => 1 @[%v]`, - }, - }, - { - expr: `ln(vector_matching_a)`, - output: []string{ - `{l="x"} => 2.302585092994046 @[%v]`, - `{l="y"} => 2.995732273553991 @[%v]`, - }, - }, - { - expr: `ln(vector_matching_a - 10)`, - output: []string{ - `{l="y"} => 2.302585092994046 @[%v]`, - `{l="x"} => -Inf @[%v]`, - }, - }, - { - expr: `ln(vector_matching_a - 20)`, - output: []string{ - `{l="y"} => -Inf @[%v]`, - `{l="x"} => NaN @[%v]`, - }, - }, - { - expr: `exp(ln(vector_matching_a))`, - output: []string{ - `{l="y"} => 20 @[%v]`, - `{l="x"} => 10 @[%v]`, - }, - }, - { - expr: `sqrt(vector_matching_a)`, - output: []string{ - `{l="x"} => 3.1622776601683795 @[%v]`, - `{l="y"} => 4.47213595499958 @[%v]`, - }, - }, - { - expr: `log2(vector_matching_a)`, - output: []string{ - `{l="x"} => 3.3219280948873626 @[%v]`, - `{l="y"} => 4.321928094887363 @[%v]`, - }, - }, - { - expr: `log2(vector_matching_a - 10)`, - output: []string{ - `{l="y"} => 3.3219280948873626 @[%v]`, - `{l="x"} => -Inf @[%v]`, - }, - }, - { - expr: `log2(vector_matching_a - 20)`, - output: []string{ - `{l="x"} => NaN @[%v]`, - `{l="y"} => -Inf @[%v]`, - }, - }, - { - expr: `log10(vector_matching_a)`, - output: []string{ - `{l="x"} => 1 @[%v]`, - `{l="y"} => 1.301029995663981 @[%v]`, - }, - }, - { - expr: `log10(vector_matching_a - 10)`, - output: []string{ - `{l="y"} => 1 @[%v]`, - `{l="x"} => -Inf @[%v]`, - }, - }, - { - expr: `log10(vector_matching_a - 20)`, - output: []string{ - `{l="x"} => NaN @[%v]`, - `{l="y"} => -Inf @[%v]`, - }, - }, - { - expr: `stddev(http_requests)`, - output: []string{ - `{} => 229.12878474779 @[%v]`, - }, - }, - { - expr: `stddev by (instance)(http_requests)`, - output: []string{ - `{instance="0"} => 223.60679774998 @[%v]`, - `{instance="1"} => 223.60679774998 @[%v]`, - }, - }, - { - expr: `stdvar(http_requests)`, - output: []string{ - `{} => 52500 @[%v]`, - }, - }, - { - expr: `stdvar by (instance)(http_requests)`, - output: []string{ - `{instance="0"} => 50000 @[%v]`, - `{instance="1"} => 50000 @[%v]`, - }, - }, - } - - storage, closer := newTestStorage(t) - defer closer.Close() - - engine := NewEngine(storage) - - for i, exprTest := range expressionTests { - expectedLines := annotateWithTime(exprTest.output, testEvalTime) - - query, err := engine.NewInstantQuery(exprTest.expr, testEvalTime) - - if err != nil { - if !exprTest.shouldFail { - t.Errorf("%d. Error during parsing: %v", i, err) - t.Errorf("%d. Expression: %v", i, exprTest.expr) - } - continue - } - - failed := false - - res := query.Exec() - if res.Err != nil { - if !exprTest.shouldFail { - t.Errorf("%d. Error evaluating query: %s", res.Err) - t.Errorf("%d. Expression: %v", i, exprTest.expr) - } - continue - } - if exprTest.shouldFail { - t.Errorf("%d. Expression should fail but did not", i) - continue - } - resultLines := strings.Split(res.String(), "\n") - // resultStr := ast.EvalToString(testExpr, testEvalTime, ast.Text, storage, stats.NewTimerGroup()) - // resultLines := strings.Split(resultStr, "\n") - - if len(exprTest.output) == 0 && strings.Trim(res.String(), "\n") == "" { - // expected and received empty vector, everything is fine - continue - } else if len(exprTest.output) != len(resultLines) { - t.Errorf("%d. Number of samples in expected and actual output don't match", i) - failed = true - } - - if exprTest.checkOrder { - for j, expectedSample := range expectedLines { - if resultLines[j] != expectedSample { - t.Errorf("%d.%d. Expected sample '%v', got '%v'", i, j, resultLines[j], expectedSample) - failed = true - } - } - } else { - for j, expectedSample := range expectedLines { - found := false - for _, actualSample := range resultLines { - if samplesAlmostEqual(actualSample, expectedSample) { - found = true - } - } - if !found { - t.Errorf("%d.%d. Couldn't find expected sample in output: '%v'", i, j, expectedSample) - failed = true - } - } - } - - if failed { - t.Errorf("%d. Expression: %v\n%v", i, exprTest.expr, vectorComparisonString(expectedLines, resultLines)) - } - - } -} - func TestRangedEvaluationRegressions(t *testing.T) { scenarios := []struct { in Matrix diff --git a/promql/testdata/legacy.test b/promql/testdata/legacy.test new file mode 100644 index 000000000..d44409031 --- /dev/null +++ b/promql/testdata/legacy.test @@ -0,0 +1,638 @@ +load 5m + http_requests{job="api-server", instance="0", group="production"} 0+10x10 + http_requests{job="api-server", instance="1", group="production"} 0+20x10 + http_requests{job="api-server", instance="0", group="canary"} 0+30x10 + http_requests{job="api-server", instance="1", group="canary"} 0+40x10 + http_requests{job="app-server", instance="0", group="production"} 0+50x10 + http_requests{job="app-server", instance="1", group="production"} 0+60x10 + http_requests{job="app-server", instance="0", group="canary"} 0+70x10 + http_requests{job="app-server", instance="1", group="canary"} 0+80x10 + +load 5m + x{y="testvalue"} 0+10x10 + +load 5m + testcounter_reset_middle 0+10x4 0+10x5 + testcounter_reset_end 0+10x9 0 10 + +load 5m + label_grouping_test{a="aa", b="bb"} 0+10x10 + label_grouping_test{a="a", b="abb"} 0+20x10 + +load 5m + vector_matching_a{l="x"} 0+1x100 + vector_matching_a{l="y"} 0+2x50 + vector_matching_b{l="x"} 0+4x25 + +load 5m + cpu_count{instance="0", type="numa"} 0+30x10 + cpu_count{instance="0", type="smp"} 0+10x20 + cpu_count{instance="1", type="smp"} 0+20x10 + + +eval instant at 50m SUM(http_requests) + {} 3600 + +eval instant at 50m SUM(http_requests{instance="0"}) BY(job) + {job="api-server"} 400 + {job="app-server"} 1200 + +eval instant at 50m SUM(http_requests{instance="0"}) BY(job) KEEPING_EXTRA + {instance="0", job="api-server"} 400 + {instance="0", job="app-server"} 1200 + +eval instant at 50m SUM(http_requests) BY (job) + {job="api-server"} 1000 + {job="app-server"} 2600 + +# Non-existent labels mentioned in BY-clauses shouldn't propagate to output. +eval instant at 50m SUM(http_requests) BY (job, nonexistent) + {job="api-server"} 1000 + {job="app-server"} 2600 + + +eval instant at 50m COUNT(http_requests) BY (job) + {job="api-server"} 4 + {job="app-server"} 4 + + +eval instant at 50m SUM(http_requests) BY (job, group) + {group="canary", job="api-server"} 700 + {group="canary", job="app-server"} 1500 + {group="production", job="api-server"} 300 + {group="production", job="app-server"} 1100 + + +eval instant at 50m AVG(http_requests) BY (job) + {job="api-server"} 250 + {job="app-server"} 650 + + +eval instant at 50m MIN(http_requests) BY (job) + {job="api-server"} 100 + {job="app-server"} 500 + + +eval instant at 50m MAX(http_requests) BY (job) + {job="api-server"} 400 + {job="app-server"} 800 + + +eval instant at 50m SUM(http_requests) BY (job) - COUNT(http_requests) BY (job) + {job="api-server"} 996 + {job="app-server"} 2596 + + +eval instant at 50m 2 - SUM(http_requests) BY (job) + {job="api-server"} -998 + {job="app-server"} -2598 + + +eval instant at 50m 1000 / SUM(http_requests) BY (job) + {job="api-server"} 1 + {job="app-server"} 0.38461538461538464 + + +eval instant at 50m SUM(http_requests) BY (job) - 2 + {job="api-server"} 998 + {job="app-server"} 2598 + + +eval instant at 50m SUM(http_requests) BY (job) % 3 + {job="api-server"} 1 + {job="app-server"} 2 + + +eval instant at 50m SUM(http_requests) BY (job) / 0 + {job="api-server"} +Inf + {job="app-server"} +Inf + + +eval instant at 50m SUM(http_requests) BY (job) > 1000 + {job="app-server"} 2600 + + +eval instant at 50m 1000 < SUM(http_requests) BY (job) + {job="app-server"} 1000 + + +eval instant at 50m SUM(http_requests) BY (job) <= 1000 + {job="api-server"} 1000 + + +eval instant at 50m SUM(http_requests) BY (job) != 1000 + {job="app-server"} 2600 + + +eval instant at 50m SUM(http_requests) BY (job) == 1000 + {job="api-server"} 1000 + + +eval instant at 50m SUM(http_requests) BY (job) + SUM(http_requests) BY (job) + {job="api-server"} 2000 + {job="app-server"} 5200 + + +eval instant at 50m http_requests{job="api-server", group="canary"} + http_requests{group="canary", instance="0", job="api-server"} 300 + http_requests{group="canary", instance="1", job="api-server"} 400 + + +eval instant at 50m http_requests{job="api-server", group="canary"} + rate(http_requests{job="api-server"}[5m]) * 5 * 60 + {group="canary", instance="0", job="api-server"} 330 + {group="canary", instance="1", job="api-server"} 440 + + +eval instant at 50m rate(http_requests[25m]) * 25 * 60 + {group="canary", instance="0", job="api-server"} 150 + {group="canary", instance="0", job="app-server"} 350 + {group="canary", instance="1", job="api-server"} 200 + {group="canary", instance="1", job="app-server"} 400 + {group="production", instance="0", job="api-server"} 50 + {group="production", instance="0", job="app-server"} 249.99999999999997 + {group="production", instance="1", job="api-server"} 100 + {group="production", instance="1", job="app-server"} 300 + +eval instant at 50m delta(http_requests[25m], 1) + {group="canary", instance="0", job="api-server"} 150 + {group="canary", instance="0", job="app-server"} 350 + {group="canary", instance="1", job="api-server"} 200 + {group="canary", instance="1", job="app-server"} 400 + {group="production", instance="0", job="api-server"} 50 + {group="production", instance="0", job="app-server"} 250 + {group="production", instance="1", job="api-server"} 100 + {group="production", instance="1", job="app-server"} 300 + +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 + http_requests{group="production", instance="0", job="app-server"} 500 + http_requests{group="production", instance="1", job="app-server"} 600 + http_requests{group="canary", instance="0", job="app-server"} 700 + http_requests{group="canary", instance="1", job="app-server"} 800 + +eval_ordered instant at 50m sort(0 / round(http_requests, 400) + http_requests) + {group="production", instance="0", job="api-server"} NaN + {group="production", instance="1", job="api-server"} 200 + {group="canary", instance="0", job="api-server"} 300 + {group="canary", instance="1", job="api-server"} 400 + {group="production", instance="0", job="app-server"} 500 + {group="production", instance="1", job="app-server"} 600 + {group="canary", instance="0", job="app-server"} 700 + {group="canary", instance="1", job="app-server"} 800 + + +eval_ordered instant at 50m sort_desc(http_requests) + http_requests{group="canary", instance="1", job="app-server"} 800 + http_requests{group="canary", instance="0", job="app-server"} 700 + http_requests{group="production", instance="1", job="app-server"} 600 + http_requests{group="production", instance="0", job="app-server"} 500 + http_requests{group="canary", instance="1", job="api-server"} 400 + http_requests{group="canary", instance="0", job="api-server"} 300 + http_requests{group="production", instance="1", job="api-server"} 200 + http_requests{group="production", instance="0", job="api-server"} 100 + +eval_ordered instant at 50m topk(3, http_requests) + http_requests{group="canary", instance="1", job="app-server"} 800 + http_requests{group="canary", instance="0", job="app-server"} 700 + http_requests{group="production", instance="1", job="app-server"} 600 + +eval_ordered instant at 50m topk(5, http_requests{group="canary",job="app-server"}) + http_requests{group="canary", instance="1", job="app-server"} 800 + http_requests{group="canary", instance="0", job="app-server"} 700 + +eval_ordered instant at 50m bottomk(3, 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 + +eval_ordered instant at 50m bottomk(5, http_requests{group="canary",job="app-server"}) + http_requests{group="canary", instance="0", job="app-server"} 700 + http_requests{group="canary", instance="1", job="app-server"} 800 + + +# Single-letter label names and values. +eval instant at 50m x{y="testvalue"} + x{y="testvalue"} 100 + + +# Lower-cased aggregation operators should work too. +eval instant at 50m sum(http_requests) by (job) + min(http_requests) by (job) + max(http_requests) by (job) + avg(http_requests) by (job) + {job="app-server"} 4550 + {job="api-server"} 1750 + + +# Deltas should be adjusted for target interval vs. samples under target interval. +eval instant at 50m delta(http_requests{group="canary", instance="1", job="app-server"}[18m]) + {group="canary", instance="1", job="app-server"} 288 + + +# Deltas should perform the same operation when 2nd argument is 0. +eval instant at 50m delta(http_requests{group="canary", instance="1", job="app-server"}[18m], 0) + {group="canary", instance="1", job="app-server"} 288 + + +# Rates should calculate per-second rates. +eval instant at 50m rate(http_requests{group="canary", instance="1", job="app-server"}[60m]) + {group="canary", instance="1", job="app-server"} 0.26666666666666666 + +# Deriv should return the same as rate in simple cases. +eval instant at 50m deriv(http_requests{group="canary", instance="1", job="app-server"}[60m]) + {group="canary", instance="1", job="app-server"} 0.26666666666666666 + +# Counter resets at in the middle of range are handled correctly by rate(). +eval instant at 50m rate(testcounter_reset_middle[60m]) + {} 0.03 + + +# Counter resets at end of range are ignored by rate(). +eval instant at 50m rate(testcounter_reset_end[5m]) + {} 0 + +# Deriv should return correct result. +eval instant at 50m deriv(testcounter_reset_middle[100m]) + {} 0.010606060606060607 + +# count_scalar for a non-empty vector should return scalar element count. +eval instant at 50m count_scalar(http_requests) + 8 + +# count_scalar for an empty vector should return scalar 0. +eval instant at 50m count_scalar(nonexistent) + 0 + +eval instant at 50m http_requests{group!="canary"} + http_requests{group="production", instance="1", job="app-server"} 600 + http_requests{group="production", instance="0", job="app-server"} 500 + http_requests{group="production", instance="1", job="api-server"} 200 + http_requests{group="production", instance="0", job="api-server"} 100 + +eval instant at 50m http_requests{job=~"server",group!="canary"} + http_requests{group="production", instance="1", job="app-server"} 600 + http_requests{group="production", instance="0", job="app-server"} 500 + http_requests{group="production", instance="1", job="api-server"} 200 + http_requests{group="production", instance="0", job="api-server"} 100 + +eval instant at 50m http_requests{job!~"api",group!="canary"} + http_requests{group="production", instance="1", job="app-server"} 600 + http_requests{group="production", instance="0", job="app-server"} 500 + +eval instant at 50m count_scalar(http_requests{job=~"^server$"}) + 0 + +eval instant at 50m http_requests{group="production",job=~"^api"} + http_requests{group="production", instance="0", job="api-server"} 100 + http_requests{group="production", instance="1", job="api-server"} 200 + +eval instant at 50m abs(-1 * http_requests{group="production",job="api-server"}) + {group="production", instance="0", job="api-server"} 100 + {group="production", instance="1", job="api-server"} 200 + +eval instant at 50m floor(0.004 * http_requests{group="production",job="api-server"}) + {group="production", instance="0", job="api-server"} 0 + {group="production", instance="1", job="api-server"} 0 + +eval instant at 50m ceil(0.004 * http_requests{group="production",job="api-server"}) + {group="production", instance="0", job="api-server"} 1 + {group="production", instance="1", job="api-server"} 1 + +eval instant at 50m round(0.004 * http_requests{group="production",job="api-server"}) + {group="production", instance="0", job="api-server"} 0 + {group="production", instance="1", job="api-server"} 1 + +# Round should correctly handle negative numbers. +eval instant at 50m round(-1 * (0.004 * http_requests{group="production",job="api-server"})) + {group="production", instance="0", job="api-server"} 0 + {group="production", instance="1", job="api-server"} -1 + +# Round should round half up. +eval instant at 50m round(0.005 * http_requests{group="production",job="api-server"}) + {group="production", instance="0", job="api-server"} 1 + {group="production", instance="1", job="api-server"} 1 + +eval instant at 50m round(-1 * (0.005 * http_requests{group="production",job="api-server"})) + {group="production", instance="0", job="api-server"} 0 + {group="production", instance="1", job="api-server"} -1 + +eval instant at 50m round(1 + 0.005 * http_requests{group="production",job="api-server"}) + {group="production", instance="0", job="api-server"} 2 + {group="production", instance="1", job="api-server"} 2 + +eval instant at 50m round(-1 * (1 + 0.005 * http_requests{group="production",job="api-server"})) + {group="production", instance="0", job="api-server"} -1 + {group="production", instance="1", job="api-server"} -2 + +# Round should accept the number to round nearest to. +eval instant at 50m round(0.0005 * http_requests{group="production",job="api-server"}, 0.1) + {group="production", instance="0", job="api-server"} 0.1 + {group="production", instance="1", job="api-server"} 0.1 + +eval instant at 50m round(2.1 + 0.0005 * http_requests{group="production",job="api-server"}, 0.1) + {group="production", instance="0", job="api-server"} 2.2 + {group="production", instance="1", job="api-server"} 2.2 + +eval instant at 50m round(5.2 + 0.0005 * http_requests{group="production",job="api-server"}, 0.1) + {group="production", instance="0", job="api-server"} 5.3 + {group="production", instance="1", job="api-server"} 5.3 + +# Round should work correctly with negative numbers and multiple decimal places. +eval instant at 50m round(-1 * (5.2 + 0.0005 * http_requests{group="production",job="api-server"}), 0.1) + {group="production", instance="0", job="api-server"} -5.2 + {group="production", instance="1", job="api-server"} -5.3 + +# Round should work correctly with big toNearests. +eval instant at 50m round(0.025 * http_requests{group="production",job="api-server"}, 5) + {group="production", instance="0", job="api-server"} 5 + {group="production", instance="1", job="api-server"} 5 + +eval instant at 50m round(0.045 * http_requests{group="production",job="api-server"}, 5) + {group="production", instance="0", job="api-server"} 5 + {group="production", instance="1", job="api-server"} 10 + +eval instant at 50m avg_over_time(http_requests{group="production",job="api-server"}[1h]) + {group="production", instance="0", job="api-server"} 50 + {group="production", instance="1", job="api-server"} 100 + +eval instant at 50m count_over_time(http_requests{group="production",job="api-server"}[1h]) + {group="production", instance="0", job="api-server"} 11 + {group="production", instance="1", job="api-server"} 11 + +eval instant at 50m max_over_time(http_requests{group="production",job="api-server"}[1h]) + {group="production", instance="0", job="api-server"} 100 + {group="production", instance="1", job="api-server"} 200 + +eval instant at 50m min_over_time(http_requests{group="production",job="api-server"}[1h]) + {group="production", instance="0", job="api-server"} 0 + {group="production", instance="1", job="api-server"} 0 + +eval instant at 50m sum_over_time(http_requests{group="production",job="api-server"}[1h]) + {group="production", instance="0", job="api-server"} 550 + {group="production", instance="1", job="api-server"} 1100 + +eval instant at 50m time() + 3000 + +eval instant at 50m drop_common_labels(http_requests{group="production",job="api-server"}) + http_requests{instance="0"} 100 + http_requests{instance="1"} 200 + +eval instant at 50m {__name__=~".*"} + http_requests{group="canary", instance="0", job="api-server"} 300 + http_requests{group="canary", instance="0", job="app-server"} 700 + http_requests{group="canary", instance="1", job="api-server"} 400 + http_requests{group="canary", instance="1", job="app-server"} 800 + http_requests{group="production", instance="0", job="api-server"} 100 + http_requests{group="production", instance="0", job="app-server"} 500 + http_requests{group="production", instance="1", job="api-server"} 200 + http_requests{group="production", instance="1", job="app-server"} 600 + testcounter_reset_end 0 + testcounter_reset_middle 50 + x{y="testvalue"} 100 + label_grouping_test{a="a", b="abb"} 200 + label_grouping_test{a="aa", b="bb"} 100 + vector_matching_a{l="x"} 10 + vector_matching_a{l="y"} 20 + vector_matching_b{l="x"} 40 + cpu_count{instance="1", type="smp"} 200 + cpu_count{instance="0", type="smp"} 100 + cpu_count{instance="0", type="numa"} 300 + + +eval instant at 50m {job=~"server", job!~"api"} + http_requests{group="canary", instance="0", job="app-server"} 700 + http_requests{group="canary", instance="1", job="app-server"} 800 + http_requests{group="production", instance="0", job="app-server"} 500 + http_requests{group="production", instance="1", job="app-server"} 600 + +# Test alternative "by"-clause order. +eval instant at 50m sum by (group) (http_requests{job="api-server"}) + {group="canary"} 700 + {group="production"} 300 + +# Test alternative "by"-clause order with "keeping_extra". +eval instant at 50m sum by (group) keeping_extra (http_requests{job="api-server"}) + {group="canary", job="api-server"} 700 + {group="production", job="api-server"} 300 + +# Test both alternative "by"-clause orders in one expression. +# Public health warning: stick to one form within an expression (or even +# in an organization), or risk serious user confusion. +eval instant at 50m sum(sum by (group) keeping_extra (http_requests{job="api-server"})) by (job) + {job="api-server"} 1000 + +eval instant at 50m http_requests{group="canary"} and http_requests{instance="0"} + http_requests{group="canary", instance="0", job="api-server"} 300 + http_requests{group="canary", instance="0", job="app-server"} 700 + +eval instant at 50m (http_requests{group="canary"} + 1) and http_requests{instance="0"} + {group="canary", instance="0", job="api-server"} 301 + {group="canary", instance="0", job="app-server"} 701 + +eval instant at 50m (http_requests{group="canary"} + 1) and on(instance, job) http_requests{instance="0", group="production"} + {group="canary", instance="0", job="api-server"} 301 + {group="canary", instance="0", job="app-server"} 701 + +eval instant at 50m (http_requests{group="canary"} + 1) and on(instance) http_requests{instance="0", group="production"} + {group="canary", instance="0", job="api-server"} 301 + {group="canary", instance="0", job="app-server"} 701 + +eval instant at 50m http_requests{group="canary"} or http_requests{group="production"} + http_requests{group="canary", instance="0", job="api-server"} 300 + http_requests{group="canary", instance="0", job="app-server"} 700 + http_requests{group="canary", instance="1", job="api-server"} 400 + http_requests{group="canary", instance="1", job="app-server"} 800 + http_requests{group="production", instance="0", job="api-server"} 100 + http_requests{group="production", instance="0", job="app-server"} 500 + http_requests{group="production", instance="1", job="api-server"} 200 + http_requests{group="production", instance="1", job="app-server"} 600 + +# On overlap the rhs samples must be dropped. +eval instant at 50m (http_requests{group="canary"} + 1) or http_requests{instance="1"} + {group="canary", instance="0", job="api-server"} 301 + {group="canary", instance="0", job="app-server"} 701 + {group="canary", instance="1", job="api-server"} 401 + {group="canary", instance="1", job="app-server"} 801 + http_requests{group="production", instance="1", job="api-server"} 200 + http_requests{group="production", instance="1", job="app-server"} 600 + +# Matching only on instance excludes everything that has instance=0/1 but includes +# entries without the instance label. +eval instant at 50m (http_requests{group="canary"} + 1) or on(instance) (http_requests or cpu_count or vector_matching_a) + {group="canary", instance="0", job="api-server"} 301 + {group="canary", instance="0", job="app-server"} 701 + {group="canary", instance="1", job="api-server"} 401 + {group="canary", instance="1", job="app-server"} 801 + vector_matching_a{l="x"} 10 + vector_matching_a{l="y"} 20 + +eval instant at 50m http_requests{group="canary"} / on(instance,job) http_requests{group="production"} + {instance="0", job="api-server"} 3 + {instance="0", job="app-server"} 1.4 + {instance="1", job="api-server"} 2 + {instance="1", job="app-server"} 1.3333333333333333 + +# Include labels must guarantee uniquely identifiable time series. +eval_fail instant at 50m http_requests{group="production"} / on(instance) group_left(group) cpu_count{type="smp"} + +# Many-to-many matching is not allowed. +eval_fail instant at 50m http_requests{group="production"} / on(instance) group_left(job,type) cpu_count + +# Many-to-one matching must be explicit. +eval_fail instant at 50m http_requests{group="production"} / on(instance) cpu_count{type="smp"} + +eval instant at 50m http_requests{group="production"} / on(instance) group_left(job) cpu_count{type="smp"} + {instance="1", job="api-server"} 1 + {instance="0", job="app-server"} 5 + {instance="1", job="app-server"} 3 + {instance="0", job="api-server"} 1 + +# Ensure sidedness of grouping preserves operand sides. +eval instant at 50m cpu_count{type="smp"} / on(instance) group_right(job) http_requests{group="production"} + {instance="1", job="app-server"} 0.3333333333333333 + {instance="0", job="app-server"} 0.2 + {instance="1", job="api-server"} 1 + {instance="0", job="api-server"} 1 + +# Include labels from both sides. +eval instant at 50m http_requests{group="production"} / on(instance) group_left(job) cpu_count{type="smp"} + {instance="1", job="api-server"} 1 + {instance="0", job="app-server"} 5 + {instance="1", job="app-server"} 3 + {instance="0", job="api-server"} 1 + +eval instant at 50m http_requests{group="production"} < on(instance,job) http_requests{group="canary"} + {instance="1", job="app-server"} 600 + {instance="0", job="app-server"} 500 + {instance="1", job="api-server"} 200 + {instance="0", job="api-server"} 100 + + +eval instant at 50m http_requests{group="production"} > on(instance,job) http_requests{group="canary"} + # no output + +eval instant at 50m http_requests{group="production"} == on(instance,job) http_requests{group="canary"} + # no output + +eval instant at 50m http_requests > on(instance) group_left(group,job) cpu_count{type="smp"} + {group="canary", instance="0", job="app-server"} 700 + {group="canary", instance="1", job="app-server"} 800 + {group="canary", instance="0", job="api-server"} 300 + {group="canary", instance="1", job="api-server"} 400 + {group="production", instance="0", job="app-server"} 500 + {group="production", instance="1", job="app-server"} 600 + +eval instant at 50m {l="x"} + on(__name__) {l="y"} + vector_matching_a 30 + +eval instant at 50m absent(nonexistent) + {} 1 + + +eval instant at 50m absent(nonexistent{job="testjob", instance="testinstance", method=~".x"}) + {instance="testinstance", job="testjob"} 1 + +eval instant at 50m count_scalar(absent(http_requests)) + 0 + +eval instant at 50m count_scalar(absent(sum(http_requests))) + 0 + +eval instant at 50m absent(sum(nonexistent{job="testjob", instance="testinstance"})) + {} 1 + +eval instant at 50m http_requests{group="production",job="api-server"} offset 5m + http_requests{group="production", instance="0", job="api-server"} 90 + http_requests{group="production", instance="1", job="api-server"} 180 + +eval instant at 50m rate(http_requests{group="production",job="api-server"}[10m] offset 5m) + {group="production", instance="0", job="api-server"} 0.03333333333333333 + {group="production", instance="1", job="api-server"} 0.06666666666666667 + +# Regression test for missing separator byte in labelsToGroupingKey. +eval instant at 50m sum(label_grouping_test) by (a, b) + {a="a", b="abb"} 200 + {a="aa", b="bb"} 100 + +eval instant at 50m http_requests{group="canary", instance="0", job="api-server"} / 0 + {group="canary", instance="0", job="api-server"} +Inf + +eval instant at 50m -1 * http_requests{group="canary", instance="0", job="api-server"} / 0 + {group="canary", instance="0", job="api-server"} -Inf + +eval instant at 50m 0 * http_requests{group="canary", instance="0", job="api-server"} / 0 + {group="canary", instance="0", job="api-server"} NaN + +eval instant at 50m 0 * http_requests{group="canary", instance="0", job="api-server"} % 0 + {group="canary", instance="0", job="api-server"} NaN + +eval instant at 50m exp(vector_matching_a) + {l="x"} 22026.465794806718 + {l="y"} 485165195.4097903 + +eval instant at 50m exp(vector_matching_a - 10) + {l="y"} 22026.465794806718 + {l="x"} 1 + +eval instant at 50m exp(vector_matching_a - 20) + {l="x"} 4.5399929762484854e-05 + {l="y"} 1 + +eval instant at 50m ln(vector_matching_a) + {l="x"} 2.302585092994046 + {l="y"} 2.995732273553991 + +eval instant at 50m ln(vector_matching_a - 10) + {l="y"} 2.302585092994046 + {l="x"} -Inf + +eval instant at 50m ln(vector_matching_a - 20) + {l="y"} -Inf + {l="x"} NaN + +eval instant at 50m exp(ln(vector_matching_a)) + {l="y"} 20 + {l="x"} 10 + +eval instant at 50m sqrt(vector_matching_a) + {l="x"} 3.1622776601683795 + {l="y"} 4.47213595499958 + +eval instant at 50m log2(vector_matching_a) + {l="x"} 3.3219280948873626 + {l="y"} 4.321928094887363 + +eval instant at 50m log2(vector_matching_a - 10) + {l="y"} 3.3219280948873626 + {l="x"} -Inf + +eval instant at 50m log2(vector_matching_a - 20) + {l="x"} NaN + {l="y"} -Inf + +eval instant at 50m log10(vector_matching_a) + {l="x"} 1 + {l="y"} 1.301029995663981 + +eval instant at 50m log10(vector_matching_a - 10) + {l="y"} 1 + {l="x"} -Inf + +eval instant at 50m log10(vector_matching_a - 20) + {l="x"} NaN + {l="y"} -Inf + +eval instant at 50m stddev(http_requests) + {} 229.12878474779 + +eval instant at 50m stddev by (instance)(http_requests) + {instance="0"} 223.60679774998 + {instance="1"} 223.60679774998 + +eval instant at 50m stdvar(http_requests) + {} 52500 + +eval instant at 50m stdvar by (instance)(http_requests) + {instance="0"} 50000 + {instance="1"} 50000 From 0d3012a60556e25261fc7eea5ab5e043fabd2f9e Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Tue, 12 May 2015 13:41:57 +0200 Subject: [PATCH 11/13] Migrate matrix tests, remove old test files. --- promql/parse.go | 2 +- promql/promql_test.go | 341 +-------------------------- promql/setup_test.go | 402 -------------------------------- promql/testdata/histograms.test | 2 +- promql/testdata/legacy.test | 35 +++ promql/testdata/literals.test | 2 +- 6 files changed, 50 insertions(+), 734 deletions(-) delete mode 100644 promql/setup_test.go diff --git a/promql/parse.go b/promql/parse.go index 65f94a430..653535162 100644 --- a/promql/parse.go +++ b/promql/parse.go @@ -120,7 +120,7 @@ func (p *parser) parseExpr() (expr Expr, err error) { return } -// sequenceValue is a omittable value in a sequence of time series values. +// sequenceValue is an omittable value in a sequence of time series values. type sequenceValue struct { value clientmodel.SampleValue omitted bool diff --git a/promql/promql_test.go b/promql/promql_test.go index 0a7f45271..14d1634ab 100644 --- a/promql/promql_test.go +++ b/promql/promql_test.go @@ -1,4 +1,4 @@ -// Copyright 2013 The Prometheus Authors +// Copyright 2015 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -14,341 +14,24 @@ package promql import ( - "fmt" - "math" - "regexp" - "strconv" - "strings" + "path/filepath" "testing" - "time" - - clientmodel "github.com/prometheus/client_golang/model" - - "github.com/prometheus/prometheus/storage/local" - "github.com/prometheus/prometheus/storage/metric" - "github.com/prometheus/prometheus/utility/test" ) -var ( - testEvalTime = testStartTime.Add(testSampleInterval * 10) - fixturesPath = "fixtures" - - reSample = regexp.MustCompile(`^(.*)(?: \=\>|:) (\-?\d+\.?\d*(?:e-?\d+)?|[+-]Inf|NaN) \@\[(\d+)\]$`) - // minNormal = math.Float64frombits(0x0010000000000000) // The smallest positive normal value of type float64. -) - -// const ( -// epsilon = 0.000001 // Relative error allowed for sample values. -// ) - -func annotateWithTime(lines []string, timestamp clientmodel.Timestamp) []string { - annotatedLines := []string{} - for _, line := range lines { - annotatedLines = append(annotatedLines, fmt.Sprintf(line, timestamp)) - } - return annotatedLines -} - -func vectorComparisonString(expected []string, actual []string) string { - separator := "\n--------------\n" - return fmt.Sprintf("Expected:%v%v%v\nActual:%v%v%v ", - separator, - strings.Join(expected, "\n"), - separator, - separator, - strings.Join(actual, "\n"), - separator) -} - -// samplesAlmostEqual returns true if the two sample lines only differ by a -// small relative error in their sample value. -func samplesAlmostEqual(a, b string) bool { - if a == b { - // Fast path if strings are equal. - return true - } - aMatches := reSample.FindStringSubmatch(a) - if aMatches == nil { - panic(fmt.Errorf("sample %q did not match regular expression", a)) - } - bMatches := reSample.FindStringSubmatch(b) - if bMatches == nil { - panic(fmt.Errorf("sample %q did not match regular expression", b)) - } - if aMatches[1] != bMatches[1] { - return false // Labels don't match. - } - if aMatches[3] != bMatches[3] { - return false // Timestamps don't match. - } - // If we are here, we have the diff in the floats. - // We have to check if they are almost equal. - aVal, err := strconv.ParseFloat(aMatches[2], 64) +func TestEvaluations(t *testing.T) { + files, err := filepath.Glob("testdata/*.test") if err != nil { - panic(err) + t.Fatal(err) } - bVal, err := strconv.ParseFloat(bMatches[2], 64) - if err != nil { - panic(err) - } - - // Cf. http://floating-point-gui.de/errors/comparison/ - if aVal == bVal { - return true - } - - diff := math.Abs(aVal - bVal) - - if aVal == 0 || bVal == 0 || diff < minNormal { - return diff < epsilon*minNormal - } - return diff/(math.Abs(aVal)+math.Abs(bVal)) < epsilon -} - -func newTestStorage(t testing.TB) (storage local.Storage, closer test.Closer) { - storage, closer = local.NewTestStorage(t, 1) - storeMatrix(storage, testMatrix) - return storage, closer -} - -func TestRangedEvaluationRegressions(t *testing.T) { - scenarios := []struct { - in Matrix - out Matrix - expr string - }{ - { - // Testing COWMetric behavior in drop_common_labels. - in: Matrix{ - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testmetric", - "testlabel": "1", - }, - }, - Values: metric.Values{ - { - Timestamp: testStartTime, - Value: 1, - }, - { - Timestamp: testStartTime.Add(time.Hour), - Value: 1, - }, - }, - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testmetric", - "testlabel": "2", - }, - }, - Values: metric.Values{ - { - Timestamp: testStartTime.Add(time.Hour), - Value: 2, - }, - }, - }, - }, - out: Matrix{ - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testmetric", - }, - }, - Values: metric.Values{ - { - Timestamp: testStartTime, - Value: 1, - }, - }, - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testmetric", - "testlabel": "1", - }, - }, - Values: metric.Values{ - { - Timestamp: testStartTime.Add(time.Hour), - Value: 1, - }, - }, - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testmetric", - "testlabel": "2", - }, - }, - Values: metric.Values{ - { - Timestamp: testStartTime.Add(time.Hour), - Value: 2, - }, - }, - }, - }, - expr: "drop_common_labels(testmetric)", - }, - { - // Testing COWMetric behavior in vector aggregation. - in: Matrix{ - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testmetric", - "testlabel": "1", - }, - }, - Values: metric.Values{ - { - Timestamp: testStartTime, - Value: 1, - }, - { - Timestamp: testStartTime.Add(time.Hour), - Value: 1, - }, - }, - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testmetric", - "testlabel": "2", - }, - }, - Values: metric.Values{ - { - Timestamp: testStartTime, - Value: 2, - }, - }, - }, - }, - out: Matrix{ - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{}, - }, - Values: metric.Values{ - { - Timestamp: testStartTime, - Value: 3, - }, - }, - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - "testlabel": "1", - }, - }, - Values: metric.Values{ - { - Timestamp: testStartTime.Add(time.Hour), - Value: 1, - }, - }, - }, - }, - expr: "sum(testmetric) keeping_extra", - }, - { - // Testing metric fingerprint grouping behavior. - in: Matrix{ - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testmetric", - "aa": "bb", - }, - }, - Values: metric.Values{ - { - Timestamp: testStartTime, - Value: 1, - }, - }, - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testmetric", - "a": "abb", - }, - }, - Values: metric.Values{ - { - Timestamp: testStartTime, - Value: 2, - }, - }, - }, - }, - out: Matrix{ - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testmetric", - "aa": "bb", - }, - }, - Values: metric.Values{ - { - Timestamp: testStartTime, - Value: 1, - }, - }, - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testmetric", - "a": "abb", - }, - }, - Values: metric.Values{ - { - Timestamp: testStartTime, - Value: 2, - }, - }, - }, - }, - expr: "testmetric", - }, - } - - for i, s := range scenarios { - storage, closer := local.NewTestStorage(t, 1) - storeMatrix(storage, s.in) - - engine := NewEngine(storage) - query, err := engine.NewRangeQuery(s.expr, testStartTime, testStartTime.Add(time.Hour), time.Hour) + for _, fn := range files { + test, err := NewTestFromFile(t, fn) if err != nil { - t.Errorf("%d. Error in expression %q", i, s.expr) - t.Fatalf("%d. Error parsing expression: %v", i, err) + t.Errorf("error creating test for %s: %s", fn, err) } - res := query.Exec() - if res.Err != nil { - t.Errorf("%d. Error in expression %q", i, s.expr) - t.Fatalf("%d. Error evaluating expression: %v", i, err) + err = test.Run() + if err != nil { + t.Errorf("error running test %s: %s", fn, err) } - - if res.String() != s.out.String() { - t.Errorf("%d. Error in expression %q", i, s.expr) - t.Fatalf("%d. Expression: %s\n\ngot:\n=====\n%v\n====\n\nwant:\n=====\n%v\n=====\n", i, s.expr, res.String(), s.out.String()) - } - - closer.Close() + test.Close() } } diff --git a/promql/setup_test.go b/promql/setup_test.go deleted file mode 100644 index a02ee5a66..000000000 --- a/promql/setup_test.go +++ /dev/null @@ -1,402 +0,0 @@ -// Copyright 2013 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package promql - -import ( - "time" - - clientmodel "github.com/prometheus/client_golang/model" - - "github.com/prometheus/prometheus/storage/local" - "github.com/prometheus/prometheus/storage/metric" -) - -var testSampleInterval = time.Duration(5) * time.Minute - -// var testStartTime = clientmodel.Timestamp(0) - -func getTestValueStream(startVal, endVal, stepVal clientmodel.SampleValue, startTime clientmodel.Timestamp) (resultValues metric.Values) { - currentTime := startTime - for currentVal := startVal; currentVal <= endVal; currentVal += stepVal { - sample := metric.SamplePair{ - Value: currentVal, - Timestamp: currentTime, - } - resultValues = append(resultValues, sample) - currentTime = currentTime.Add(testSampleInterval) - } - return resultValues -} - -func getTestVectorFromTestMatrix(matrix Matrix) Vector { - vector := Vector{} - for _, sampleStream := range matrix { - lastSample := sampleStream.Values[len(sampleStream.Values)-1] - vector = append(vector, &Sample{ - Metric: sampleStream.Metric, - Value: lastSample.Value, - Timestamp: lastSample.Timestamp, - }) - } - return vector -} - -func storeMatrix(storage local.Storage, matrix Matrix) { - pendingSamples := clientmodel.Samples{} - for _, sampleStream := range matrix { - for _, sample := range sampleStream.Values { - pendingSamples = append(pendingSamples, &clientmodel.Sample{ - Metric: sampleStream.Metric.Metric, - Value: sample.Value, - Timestamp: sample.Timestamp, - }) - } - } - for _, s := range pendingSamples { - storage.Append(s) - } - storage.WaitForIndexing() -} - -var testVector = getTestVectorFromTestMatrix(testMatrix) - -var testMatrix = Matrix{ - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "http_requests", - clientmodel.JobLabel: "api-server", - "instance": "0", - "group": "production", - }, - }, - Values: getTestValueStream(0, 100, 10, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "http_requests", - clientmodel.JobLabel: "api-server", - "instance": "1", - "group": "production", - }, - }, - Values: getTestValueStream(0, 200, 20, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "http_requests", - clientmodel.JobLabel: "api-server", - "instance": "0", - "group": "canary", - }, - }, - Values: getTestValueStream(0, 300, 30, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "http_requests", - clientmodel.JobLabel: "api-server", - "instance": "1", - "group": "canary", - }, - }, - Values: getTestValueStream(0, 400, 40, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "http_requests", - clientmodel.JobLabel: "app-server", - "instance": "0", - "group": "production", - }, - }, - Values: getTestValueStream(0, 500, 50, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "http_requests", - clientmodel.JobLabel: "app-server", - "instance": "1", - "group": "production", - }, - }, - Values: getTestValueStream(0, 600, 60, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "http_requests", - clientmodel.JobLabel: "app-server", - "instance": "0", - "group": "canary", - }, - }, - Values: getTestValueStream(0, 700, 70, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "http_requests", - clientmodel.JobLabel: "app-server", - "instance": "1", - "group": "canary", - }, - }, - Values: getTestValueStream(0, 800, 80, testStartTime), - }, - // Single-letter metric and label names. - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "x", - "y": "testvalue", - }, - }, - Values: getTestValueStream(0, 100, 10, testStartTime), - }, - // Counter reset in the middle of range. - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testcounter_reset_middle", - }, - }, - Values: append(getTestValueStream(0, 40, 10, testStartTime), getTestValueStream(0, 50, 10, testStartTime.Add(testSampleInterval*5))...), - }, - // Counter reset at the end of range. - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "testcounter_reset_end", - }, - }, - Values: append(getTestValueStream(0, 90, 10, testStartTime), getTestValueStream(0, 0, 10, testStartTime.Add(testSampleInterval*10))...), - }, - // For label-key grouping regression test. - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "label_grouping_test", - "a": "aa", - "b": "bb", - }, - }, - Values: getTestValueStream(0, 100, 10, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "label_grouping_test", - "a": "a", - "b": "abb", - }, - }, - Values: getTestValueStream(0, 200, 20, testStartTime), - }, - // Now a more realistic histogram per job and instance to test aggregation. - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "request_duration_seconds_bucket", - clientmodel.JobLabel: "job1", - "instance": "ins1", - "le": "0.1", - }, - }, - Values: getTestValueStream(0, 10, 1, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "request_duration_seconds_bucket", - clientmodel.JobLabel: "job1", - "instance": "ins1", - "le": "0.2", - }, - }, - Values: getTestValueStream(0, 30, 3, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "request_duration_seconds_bucket", - clientmodel.JobLabel: "job1", - "instance": "ins1", - "le": "+Inf", - }, - }, - Values: getTestValueStream(0, 40, 4, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "request_duration_seconds_bucket", - clientmodel.JobLabel: "job1", - "instance": "ins2", - "le": "0.1", - }, - }, - Values: getTestValueStream(0, 20, 2, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "request_duration_seconds_bucket", - clientmodel.JobLabel: "job1", - "instance": "ins2", - "le": "0.2", - }, - }, - Values: getTestValueStream(0, 50, 5, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "request_duration_seconds_bucket", - clientmodel.JobLabel: "job1", - "instance": "ins2", - "le": "+Inf", - }, - }, - Values: getTestValueStream(0, 60, 6, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "request_duration_seconds_bucket", - clientmodel.JobLabel: "job2", - "instance": "ins1", - "le": "0.1", - }, - }, - Values: getTestValueStream(0, 30, 3, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "request_duration_seconds_bucket", - clientmodel.JobLabel: "job2", - "instance": "ins1", - "le": "0.2", - }, - }, - Values: getTestValueStream(0, 40, 4, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "request_duration_seconds_bucket", - clientmodel.JobLabel: "job2", - "instance": "ins1", - "le": "+Inf", - }, - }, - Values: getTestValueStream(0, 60, 6, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "request_duration_seconds_bucket", - clientmodel.JobLabel: "job2", - "instance": "ins2", - "le": "0.1", - }, - }, - Values: getTestValueStream(0, 40, 4, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "request_duration_seconds_bucket", - clientmodel.JobLabel: "job2", - "instance": "ins2", - "le": "0.2", - }, - }, - Values: getTestValueStream(0, 70, 7, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "request_duration_seconds_bucket", - clientmodel.JobLabel: "job2", - "instance": "ins2", - "le": "+Inf", - }, - }, - Values: getTestValueStream(0, 90, 9, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "vector_matching_a", - "l": "x", - }, - }, - Values: getTestValueStream(0, 100, 1, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "vector_matching_a", - "l": "y", - }, - }, - Values: getTestValueStream(0, 100, 2, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "vector_matching_b", - "l": "x", - }, - }, - Values: getTestValueStream(0, 100, 4, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "cpu_count", - "instance": "0", - "type": "numa", - }, - }, - Values: getTestValueStream(0, 500, 30, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "cpu_count", - "instance": "0", - "type": "smp", - }, - }, - Values: getTestValueStream(0, 200, 10, testStartTime), - }, - { - Metric: clientmodel.COWMetric{ - Metric: clientmodel.Metric{ - clientmodel.MetricNameLabel: "cpu_count", - "instance": "1", - "type": "smp", - }, - }, - Values: getTestValueStream(0, 200, 20, testStartTime), - }, -} diff --git a/promql/testdata/histograms.test b/promql/testdata/histograms.test index 6d1f16dfa..2478c3484 100644 --- a/promql/testdata/histograms.test +++ b/promql/testdata/histograms.test @@ -138,4 +138,4 @@ eval instant at 50m histogram_quantile(0.5, rate(request_duration_seconds_bucket {instance="ins1", job="job1"} 0.15 {instance="ins2", job="job1"} 0.13333333333333333 {instance="ins1", job="job2"} 0.1 - {instance="ins2", job="job2"} 0.11666666666666667 \ No newline at end of file + {instance="ins2", job="job2"} 0.11666666666666667 diff --git a/promql/testdata/legacy.test b/promql/testdata/legacy.test index d44409031..62b141565 100644 --- a/promql/testdata/legacy.test +++ b/promql/testdata/legacy.test @@ -636,3 +636,38 @@ eval instant at 50m stdvar(http_requests) eval instant at 50m stdvar by (instance)(http_requests) {instance="0"} 50000 {instance="1"} 50000 + + +# Matrix tests. + +clear +load 1h + testmetric{testlabel="1"} 1 1 + testmetric{testlabel="2"} _ 2 + +eval instant at 0h drop_common_labels(testmetric) + testmetric 1 + +eval instant at 1h drop_common_labels(testmetric) + testmetric{testlabel="1"} 1 + testmetric{testlabel="2"} 2 + +clear +load 1h + testmetric{testlabel="1"} 1 1 + testmetric{testlabel="2"} 2 _ + +eval instant at 0h sum(testmetric) keeping_extra + {} 3 + +eval instant at 1h sum(testmetric) keeping_extra + {testlabel="1"} 1 + +clear +load 1h + testmetric{aa="bb"} 1 + testmetric{a="abb"} 2 + +eval instant at 0h testmetric + testmetric{aa="bb"} 1 + testmetric{a="abb"} 2 diff --git a/promql/testdata/literals.test b/promql/testdata/literals.test index ffca00e1c..cc86f0c9b 100644 --- a/promql/testdata/literals.test +++ b/promql/testdata/literals.test @@ -53,4 +53,4 @@ eval instant at 50m 0 / 0 NaN eval instant at 50m 1 % 0 - NaN \ No newline at end of file + NaN From 3e914a8cb1054a499a282201f3da1770211c70cf Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 19 May 2015 02:44:40 +0900 Subject: [PATCH 12/13] fix graph links with path prefix --- console_libraries/prom.lib | 2 +- rules/alerting.go | 7 ++++--- rules/recording.go | 7 ++++--- rules/rules.go | 2 +- web/templates/alerts.html | 2 +- web/templates/status.html | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/console_libraries/prom.lib b/console_libraries/prom.lib index f80ea58b7..72ea238e5 100644 --- a/console_libraries/prom.lib +++ b/console_libraries/prom.lib @@ -40,7 +40,7 @@ renderTemplate is the name of the template to use to render the value. */}} {{ define "prom_query_drilldown" }} {{ $expr := .arg0 }}{{ $suffix := (or .arg1 "") }}{{ $renderTemplate := (or .arg2 "__prom_query_drilldown_noop") }} -{{ with query $expr }}{{tmpl $renderTemplate ( . | first | value )}}{{ $suffix }}{{ else }}-{{ end }} +{{ with query $expr }}{{tmpl $renderTemplate ( . | first | value )}}{{ $suffix }}{{ else }}-{{ end }} {{ end }} {{ define "prom_path" }}/consoles/{{ .Path }}?{{ range $param, $value := .Params }}{{ $param }}={{ $value }}&{{ end }}{{ end }}" diff --git a/rules/alerting.go b/rules/alerting.go index 8a558aeca..65cfab407 100644 --- a/rules/alerting.go +++ b/rules/alerting.go @@ -17,6 +17,7 @@ import ( "fmt" "html/template" "reflect" + "strings" "sync" "time" @@ -213,16 +214,16 @@ func (rule *AlertingRule) String() string { } // HTMLSnippet returns an HTML snippet representing this alerting rule. -func (rule *AlertingRule) HTMLSnippet() template.HTML { +func (rule *AlertingRule) HTMLSnippet(pathPrefix string) template.HTML { alertMetric := clientmodel.Metric{ clientmodel.MetricNameLabel: AlertMetricName, AlertNameLabel: clientmodel.LabelValue(rule.name), } return template.HTML(fmt.Sprintf( `ALERT %s IF %s FOR %s WITH %s`, - utility.GraphLinkForExpression(alertMetric.String()), + pathPrefix+strings.TrimLeft(utility.GraphLinkForExpression(alertMetric.String()), "/"), rule.name, - utility.GraphLinkForExpression(rule.Vector.String()), + pathPrefix+strings.TrimLeft(utility.GraphLinkForExpression(rule.Vector.String()), "/"), rule.Vector, utility.DurationToString(rule.holdDuration), rule.Labels)) diff --git a/rules/recording.go b/rules/recording.go index 2402fa84f..d9d2b0028 100644 --- a/rules/recording.go +++ b/rules/recording.go @@ -17,6 +17,7 @@ import ( "fmt" "html/template" "reflect" + "strings" clientmodel "github.com/prometheus/client_golang/model" @@ -85,13 +86,13 @@ func (rule RecordingRule) String() string { } // HTMLSnippet returns an HTML snippet representing this rule. -func (rule RecordingRule) HTMLSnippet() template.HTML { +func (rule RecordingRule) HTMLSnippet(pathPrefix string) template.HTML { ruleExpr := rule.vector.String() return template.HTML(fmt.Sprintf( `%s%s = %s`, - utility.GraphLinkForExpression(rule.name), + pathPrefix+strings.TrimLeft(utility.GraphLinkForExpression(rule.name), "/"), rule.name, rule.labels, - utility.GraphLinkForExpression(ruleExpr), + pathPrefix+strings.TrimLeft(utility.GraphLinkForExpression(ruleExpr), "/"), ruleExpr)) } diff --git a/rules/rules.go b/rules/rules.go index 9485bc533..2af1d5c61 100644 --- a/rules/rules.go +++ b/rules/rules.go @@ -37,5 +37,5 @@ type Rule interface { String() string // HTMLSnippet returns a human-readable string representation of the rule, // decorated with HTML elements for use the web frontend. - HTMLSnippet() template.HTML + HTMLSnippet(pathPrefix string) template.HTML } diff --git a/web/templates/alerts.html b/web/templates/alerts.html index 903ef7cd0..202b67c1c 100644 --- a/web/templates/alerts.html +++ b/web/templates/alerts.html @@ -17,7 +17,7 @@
- {{.HTMLSnippet}} + {{.HTMLSnippet pathPrefix}} Silence All Children…
{{if $activeAlerts}} diff --git a/web/templates/status.html b/web/templates/status.html index 91332c601..b7de91bf2 100644 --- a/web/templates/status.html +++ b/web/templates/status.html @@ -28,7 +28,7 @@
{{.Config}}

Rules

-
{{range .RuleManager.Rules}}{{.HTMLSnippet}}
{{end}}
+
{{range .RuleManager.Rules}}{{.HTMLSnippet pathPrefix}}
{{end}}

Targets

From 80986e0dda68e0f71c5c488431e0b3514545f9f9 Mon Sep 17 00:00:00 2001 From: Alan Braithwaite Date: Tue, 19 May 2015 22:50:04 -0700 Subject: [PATCH 13/13] Makefile: allow building from source without git This allows for building from a source tarball/zip using VERSION=$VERSION make build --- Makefile.INCLUDE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.INCLUDE b/Makefile.INCLUDE index d2d710fa2..1a1912853 100644 --- a/Makefile.INCLUDE +++ b/Makefile.INCLUDE @@ -15,7 +15,7 @@ .SUFFIXES: -VERSION=$(shell cat `git rev-parse --show-toplevel`/VERSION) +VERSION?=$(shell cat `git rev-parse --show-toplevel`/VERSION) OS=$(shell uname) ARCH=$(shell uname -m)