Merge branch 'master' into fabxc/servdisc

pull/708/head
Fabian Reinartz 2015-05-20 14:12:34 +02:00
commit e2ed921505
19 changed files with 1877 additions and 2286 deletions

View File

@ -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)

View File

@ -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") }}
<a class="prom_query_drilldown" href="{{ graphLink $expr }}">{{ with query $expr }}{{tmpl $renderTemplate ( . | first | value )}}{{ $suffix }}{{ else }}-{{ end }}</a>
<a class="prom_query_drilldown" href="{{ pathPrefix }}{{ graphLink $expr }}">{{ with query $expr }}{{tmpl $renderTemplate ( . | first | value )}}{{ $suffix }}{{ else }}-{{ end }}</a>
{{ end }}
{{ define "prom_path" }}/consoles/{{ .Path }}?{{ range $param, $value := .Params }}{{ $param }}={{ $value }}&amp;{{ end }}{{ end }}"

View File

@ -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
@ -592,8 +597,14 @@ func (ev *evaluator) eval(expr Expr) Value {
}
case lt == ExprVector && rt == ExprVector:
switch e.Op {
case itemLAND:
return ev.vectorAnd(lhs.(Vector), rhs.(Vector), e.VectorMatching)
case itemLOR:
return ev.vectorOr(lhs.(Vector), rhs.(Vector), e.VectorMatching)
default:
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)
@ -698,109 +709,171 @@ func (ev *evaluator) matrixSelectorBounds(node *MatrixSelector) Matrix {
return Matrix(sampleStreams)
}
// vectorBinop evaluates a binary operation between two vector values.
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.
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[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[sigf(ls.Metric)]; ok {
result = append(result, ls)
}
}
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")
}
sigf := signatureFunc(matching.On...)
var result Vector
leftSigs := map[uint64]struct{}{}
// Add everything from the left-hand-side vector.
for _, ls := range lhs {
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[sigf(rs.Metric)]; !ok {
result = append(result, rs)
}
}
return result
}
// 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]; matching.Card != CardManyToMany && 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)
// 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
}
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 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)
value, keep := vectorElemBinop(op, vl, vr)
if !keep {
continue
}
metric := resultMetric(ls.Metric, op, resultLabels...)
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)")
}
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")
}
insertedSigs[insertSig] = struct{}{}
}
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}
}
}
ns := &Sample{
result = append(result, &Sample{
Metric: metric,
Value: value,
Timestamp: ev.Timestamp,
})
}
result = append(result, ns)
added[hash] = added[hash] // Set existance to true.
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)
}
}
// 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)
// 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 result
return clientmodel.COWMetric{Metric: m, Copied: false}
}
// vectorScalarBinop evaluates a binary operation between a vector and a scalar.
@ -1018,64 +1091,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,

View File

@ -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 {
@ -277,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.
@ -450,21 +461,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
@ -544,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)
@ -551,6 +551,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:
@ -650,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
@ -689,6 +717,9 @@ Loop:
break Loop
}
}
if l.seriesDesc && l.peek() != '{' {
return lexValueSequence
}
return lexStatements
}

View File

@ -14,6 +14,7 @@
package promql
import (
"fmt"
"reflect"
"testing"
)
@ -22,6 +23,7 @@ var tests = []struct {
input string
expected []item
fail bool
seriesDesc bool // Whether to lex a series description.
}{
// Test common stuff.
{
@ -354,6 +356,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
@ -361,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 {
@ -370,20 +409,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
}

View File

@ -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 an 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)

View File

@ -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,
@ -1216,3 +1231,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 smallest 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)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,486 +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),
},
// 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{
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),
},
}

507
promql/test.go Normal file
View File

@ -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 <step:duration>)")
}
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 <offset:duration>] <query>")
}
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
}

141
promql/testdata/histograms.test vendored Normal file
View File

@ -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

673
promql/testdata/legacy.test vendored Normal file
View File

@ -0,0 +1,673 @@
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
# 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

56
promql/testdata/literals.test vendored Normal file
View File

@ -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

View File

@ -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 <a href="%s">%s</a> IF <a href="%s">%s</a> 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))

View File

@ -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(
`<a href="%s">%s</a>%s = <a href="%s">%s</a>`,
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))
}

View File

@ -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
}

View File

@ -17,7 +17,7 @@
<tr class="alert_details">
<td>
<div class="alert_description">
<span class="label alert_rule">{{.HTMLSnippet}}</span>
<span class="label alert_rule">{{.HTMLSnippet pathPrefix}}</span>
<a href="#" class="silence_children_link">Silence All Children&hellip;</a>
</div>
{{if $activeAlerts}}

View File

@ -28,7 +28,7 @@
<pre>{{.Config}}</pre>
<h2>Rules</h2>
<pre>{{range .RuleManager.Rules}}{{.HTMLSnippet}}<br/>{{end}}</pre>
<pre>{{range .RuleManager.Rules}}{{.HTMLSnippet pathPrefix}}<br/>{{end}}</pre>
<h2>Targets</h2>
<table class="table table-condensed table-bordered table-striped table-hover">