diff --git a/rules/ast/functions.go b/rules/ast/functions.go index 4cf5047c9..4af4abcca 100644 --- a/rules/ast/functions.go +++ b/rules/ast/functions.go @@ -17,6 +17,8 @@ import ( "errors" "fmt" "github.com/prometheus/prometheus/model" + "github.com/prometheus/prometheus/utility" + "sort" "time" ) @@ -62,17 +64,17 @@ func (function *Function) CheckArgTypes(args []Node) error { return nil } -// === time() === +// === time() model.SampleValue === func timeImpl(timestamp time.Time, view *viewAdapter, args []Node) interface{} { return model.SampleValue(time.Now().Unix()) } -// === count(vector VectorNode) === +// === count(vector VectorNode) model.SampleValue === func countImpl(timestamp time.Time, view *viewAdapter, args []Node) interface{} { return model.SampleValue(len(args[0].(VectorNode).Eval(timestamp, view))) } -// === delta(matrix MatrixNode, isCounter ScalarNode) === +// === delta(matrix MatrixNode, isCounter ScalarNode) Vector === func deltaImpl(timestamp time.Time, view *viewAdapter, args []Node) interface{} { matrixNode := args[0].(MatrixNode) isCounter := int(args[1].(ScalarNode).Eval(timestamp, view)) @@ -108,7 +110,7 @@ func deltaImpl(timestamp time.Time, view *viewAdapter, args []Node) interface{} return resultVector } -// === rate(node *MatrixNode) === +// === rate(node *MatrixNode) Vector === func rateImpl(timestamp time.Time, view *viewAdapter, args []Node) interface{} { args = append(args, &ScalarLiteral{value: 1}) vector := deltaImpl(timestamp, view, args).(Vector) @@ -123,7 +125,43 @@ func rateImpl(timestamp time.Time, view *viewAdapter, args []Node) interface{} { return vector } -// === sampleVectorImpl() === +type vectorByValueSorter struct { + vector Vector +} + +func (sorter vectorByValueSorter) Len() int { + return len(sorter.vector) +} + +func (sorter vectorByValueSorter) Less(i, j int) (less bool) { + return sorter.vector[i].Value < sorter.vector[j].Value +} + +func (sorter vectorByValueSorter) Swap(i, j int) { + sorter.vector[i], sorter.vector[j] = sorter.vector[j], sorter.vector[i] +} + +// === sort(node *VectorNode) Vector === +func sortImpl(timestamp time.Time, view *viewAdapter, args []Node) interface{} { + byValueSorter := vectorByValueSorter{ + vector: args[0].(VectorNode).Eval(timestamp, view), + } + sort.Sort(byValueSorter) + return byValueSorter.vector +} + +// === sortDesc(node *VectorNode) Vector === +func sortDescImpl(timestamp time.Time, view *viewAdapter, args []Node) interface{} { + descByValueSorter := utility.ReverseSorter{ + vectorByValueSorter{ + vector: args[0].(VectorNode).Eval(timestamp, view), + }, + } + sort.Sort(descByValueSorter) + return descByValueSorter.Interface.(vectorByValueSorter).vector +} + +// === sampleVectorImpl() Vector === func sampleVectorImpl(timestamp time.Time, view *viewAdapter, args []Node) interface{} { return Vector{ model.Sample{ @@ -197,12 +235,6 @@ func sampleVectorImpl(timestamp time.Time, view *viewAdapter, args []Node) inter } var functions = map[string]*Function{ - "time": { - name: "time", - argTypes: []ExprType{}, - returnType: SCALAR, - callFn: timeImpl, - }, "count": { name: "count", argTypes: []ExprType{VECTOR}, @@ -227,6 +259,24 @@ var functions = map[string]*Function{ returnType: VECTOR, callFn: sampleVectorImpl, }, + "sort": { + name: "sort", + argTypes: []ExprType{VECTOR}, + returnType: VECTOR, + callFn: sortImpl, + }, + "sort_desc": { + name: "sort_desc", + argTypes: []ExprType{VECTOR}, + returnType: VECTOR, + callFn: sortDescImpl, + }, + "time": { + name: "time", + argTypes: []ExprType{}, + returnType: SCALAR, + callFn: timeImpl, + }, } func GetFunction(name string) (*Function, error) { diff --git a/rules/ast/printer.go b/rules/ast/printer.go index 2864361dc..c7b77fc80 100644 --- a/rules/ast/printer.go +++ b/rules/ast/printer.go @@ -88,7 +88,6 @@ func (vector Vector) String() string { strings.Join(labelStrings, ","), sample.Value, sample.Timestamp)) } - sort.Strings(metricStrings) return strings.Join(metricStrings, "\n") } diff --git a/rules/rules_test.go b/rules/rules_test.go index 1b928ffd9..a972de4e2 100644 --- a/rules/rules_test.go +++ b/rules/rules_test.go @@ -25,12 +25,12 @@ import ( var testEvalTime = testStartTime.Add(testDuration5m * 10) -// Expected output needs to be alphabetically sorted (labels within one line -// must be sorted and lines between each other must be sorted too). +// Labels in expected output need to be alphabetically sorted. var expressionTests = []struct { expr string output []string shouldFail bool + checkOrder bool fullRanges int intervalRanges int }{ @@ -179,6 +179,36 @@ var expressionTests = []struct { }, fullRanges: 8, intervalRanges: 0, + }, { + 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, + fullRanges: 0, + intervalRanges: 8, + }, { + 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, + fullRanges: 0, + intervalRanges: 8, }, { expr: "x{y='testvalue'}", output: []string{ @@ -232,7 +262,7 @@ func TestExpressions(t *testing.T) { storeMatrix(tieredStorage, testMatrix) tieredStorage.Flush() - for _, exprTest := range expressionTests { + for i, exprTest := range expressionTests { expectedLines := annotateWithTime(exprTest.output) testExpr, err := LoadExprFromString(exprTest.expr) @@ -241,47 +271,56 @@ func TestExpressions(t *testing.T) { if exprTest.shouldFail { continue } - t.Errorf("Error during parsing: %v", err) - t.Errorf("Expression: %v", exprTest.expr) + t.Errorf("%d Error during parsing: %v", i, err) + t.Errorf("%d Expression: %v", i, exprTest.expr) } else { if exprTest.shouldFail { - t.Errorf("Test should fail, but didn't") + t.Errorf("%d Test should fail, but didn't", i) } failed := false resultStr := ast.EvalToString(testExpr, testEvalTime, ast.TEXT) resultLines := strings.Split(resultStr, "\n") if len(exprTest.output) != len(resultLines) { - t.Errorf("Number of samples in expected and actual output don't match") + t.Errorf("%d Number of samples in expected and actual output don't match", i) failed = true } - for _, expectedSample := range expectedLines { - found := false - for _, actualSample := range resultLines { - if actualSample == expectedSample { - found = 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 } } - if !found { - t.Errorf("Couldn't find expected sample in output: '%v'", - expectedSample) - failed = true + } else { + for j, expectedSample := range expectedLines { + found := false + for _, actualSample := range resultLines { + if actualSample == expectedSample { + found = true + } + } + if !found { + t.Errorf("%d.%d Couldn't find expected sample in output: '%v'", i, j, expectedSample) + failed = true + } } } analyzer := ast.NewQueryAnalyzer() analyzer.AnalyzeQueries(testExpr) if exprTest.fullRanges != len(analyzer.FullRanges) { - t.Errorf("Count of full ranges didn't match: %v vs %v", exprTest.fullRanges, len(analyzer.FullRanges)) + t.Errorf("%d Count of full ranges didn't match: %v vs %v", i, exprTest.fullRanges, len(analyzer.FullRanges)) failed = true } if exprTest.intervalRanges != len(analyzer.IntervalRanges) { - t.Errorf("Count of stepped ranges didn't match: %v vs %v", exprTest.intervalRanges, len(analyzer.IntervalRanges)) + t.Errorf("%d Count of interval ranges didn't match: %v vs %v", i, exprTest.intervalRanges, len(analyzer.IntervalRanges)) failed = true } if failed { - t.Errorf("Expression: %v\n%v", exprTest.expr, vectorComparisonString(expectedLines, resultLines)) + t.Errorf("%d Expression: %v\n%v", i, exprTest.expr, vectorComparisonString(expectedLines, resultLines)) } } } diff --git a/utility/sort.go b/utility/sort.go new file mode 100644 index 000000000..1ecf4deae --- /dev/null +++ b/utility/sort.go @@ -0,0 +1,31 @@ +// Copyright 2013 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utility + +import ( + "sort" +) + +// ReverseSorter embeds a sort.Interface value and implements a reverse sort +// over that value. +type ReverseSorter struct { + // This embedded interface permits ReverseSorter to use the methods of + // another Interface implementation. + sort.Interface +} + +// Less returns the opposite of the embedded implementation's Less method. +func (r ReverseSorter) Less(i, j int) bool { + return r.Interface.Less(j, i) +}