func (serv MetricsService) Query(w http.ResponseWriter, r *http.Request) { setAccessControlHeaders(w) params := http_utils.GetQueryParams(r) expr := params.Get("expr") asText := params.Get("asText") var format ast.OutputFormat // BUG(julius): Use Content-Type negotiation. if asText == "" { format = ast.JSON w.Header().Set("Content-Type", "application/json") } else { format = ast.TEXT w.Header().Set("Content-Type", "text/plain") } exprNode, err := rules.LoadExprFromString(expr) if err != nil { fmt.Fprint(w, ast.ErrorToJSON(err)) return } timestamp := clientmodel.TimestampFromTime(serv.time.Now()) queryStats := stats.NewTimerGroup() result := ast.EvalToString(exprNode, timestamp, format, serv.Storage, queryStats) glog.Infof("Instant query: %s\nQuery stats:\n%s\n", expr, queryStats) fmt.Fprint(w, result) }
func (serv MetricsService) Query(expr string, formatJson string) (result string) { exprNode, err := rules.LoadExprFromString(expr) if err != nil { return ast.ErrorToJSON(err) } timestamp := serv.time.Now() rb := serv.ResponseBuilder() var format ast.OutputFormat if formatJson != "" { format = ast.JSON rb.SetContentType(gorest.Application_Json) } else { format = ast.TEXT rb.SetContentType(gorest.Text_Plain) } return ast.EvalToString(exprNode, timestamp, format) }
func TestExpressions(t *testing.T) { temporaryDirectory := test.NewTemporaryDirectory("rule_expression_tests", t) defer temporaryDirectory.Close() tieredStorage, err := metric.NewTieredStorage(5000, 5000, 100, time.Second*30, time.Second*1, time.Second*20, temporaryDirectory.Path()) if err != nil { t.Fatalf("Error opening storage: %s", err) } go tieredStorage.Serve() ast.SetStorage(tieredStorage) storeMatrix(tieredStorage, testMatrix) tieredStorage.Flush() for i, exprTest := range expressionTests { expectedLines := annotateWithTime(exprTest.output) testExpr, err := LoadExprFromString(exprTest.expr) if err != nil { if exprTest.shouldFail { continue } t.Errorf("%d Error during parsing: %v", i, err) t.Errorf("%d Expression: %v", i, exprTest.expr) } else { if exprTest.shouldFail { 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("%d Number of samples in expected and actual output don't match", i) failed = true } if exprTest.checkOrder { for j, expectedSample := range expectedLines { if resultLines[j] != expectedSample { t.Errorf("%d.%d Expected sample '%v', got '%v'", i, j, resultLines[j], expectedSample) failed = true } } } else { for j, expectedSample := range expectedLines { found := false for _, actualSample := range resultLines { if 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("%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("%d Count of interval ranges didn't match: %v vs %v", i, exprTest.intervalRanges, len(analyzer.IntervalRanges)) failed = true } if failed { t.Errorf("%d Expression: %v\n%v", i, exprTest.expr, vectorComparisonString(expectedLines, resultLines)) } } } }
func TestExpressions(t *testing.T) { // Labels in expected output need to be alphabetically sorted. var expressionTests = []struct { expr string output []string shouldFail bool checkOrder bool fullRanges int intervalRanges int }{ { expr: `SUM(http_requests)`, output: []string{`http_requests => 3600 @[%v]`}, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests{instance="0"}) BY(job)`, output: []string{ `http_requests{job="api-server"} => 400 @[%v]`, `http_requests{job="app-server"} => 1200 @[%v]`, }, fullRanges: 0, intervalRanges: 4, }, { expr: `SUM(http_requests{instance="0"}) BY(job) KEEPING_EXTRA`, output: []string{ `http_requests{instance="0", job="api-server"} => 400 @[%v]`, `http_requests{instance="0", job="app-server"} => 1200 @[%v]`, }, fullRanges: 0, intervalRanges: 4, }, { expr: `SUM(http_requests) BY (job)`, output: []string{ `http_requests{job="api-server"} => 1000 @[%v]`, `http_requests{job="app-server"} => 2600 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { // Non-existent labels mentioned in BY-clauses shouldn't propagate to output. expr: `SUM(http_requests) BY (job, nonexistent)`, output: []string{ `http_requests{job="api-server"} => 1000 @[%v]`, `http_requests{job="app-server"} => 2600 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: ` // Test comment. SUM(http_requests) BY /* comments shouldn't have any effect */ (job) // another comment`, output: []string{ `http_requests{job="api-server"} => 1000 @[%v]`, `http_requests{job="app-server"} => 2600 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `COUNT(http_requests) BY (job)`, output: []string{ `http_requests{job="api-server"} => 4 @[%v]`, `http_requests{job="app-server"} => 4 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job, group)`, output: []string{ `http_requests{group="canary", job="api-server"} => 700 @[%v]`, `http_requests{group="canary", job="app-server"} => 1500 @[%v]`, `http_requests{group="production", job="api-server"} => 300 @[%v]`, `http_requests{group="production", job="app-server"} => 1100 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `AVG(http_requests) BY (job)`, output: []string{ `http_requests{job="api-server"} => 250 @[%v]`, `http_requests{job="app-server"} => 650 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `MIN(http_requests) BY (job)`, output: []string{ `http_requests{job="api-server"} => 100 @[%v]`, `http_requests{job="app-server"} => 500 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `MAX(http_requests) BY (job)`, output: []string{ `http_requests{job="api-server"} => 400 @[%v]`, `http_requests{job="app-server"} => 800 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) - COUNT(http_requests) BY (job)`, output: []string{ `http_requests{job="api-server"} => 996 @[%v]`, `http_requests{job="app-server"} => 2596 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `2 - SUM(http_requests) BY (job)`, output: []string{ `http_requests{job="api-server"} => -998 @[%v]`, `http_requests{job="app-server"} => -2598 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `1000 / SUM(http_requests) BY (job)`, output: []string{ `http_requests{job="api-server"} => 1 @[%v]`, `http_requests{job="app-server"} => 0.38461538461538464 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) - 2`, output: []string{ `http_requests{job="api-server"} => 998 @[%v]`, `http_requests{job="app-server"} => 2598 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) % 3`, output: []string{ `http_requests{job="api-server"} => 1 @[%v]`, `http_requests{job="app-server"} => 2 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) / 0`, output: []string{ `http_requests{job="api-server"} => +Inf @[%v]`, `http_requests{job="app-server"} => +Inf @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) > 1000`, output: []string{ `http_requests{job="app-server"} => 2600 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `1000 < SUM(http_requests) BY (job)`, output: []string{ `http_requests{job="app-server"} => 1000 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) <= 1000`, output: []string{ `http_requests{job="api-server"} => 1000 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) != 1000`, output: []string{ `http_requests{job="app-server"} => 2600 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) == 1000`, output: []string{ `http_requests{job="api-server"} => 1000 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) + SUM(http_requests) BY (job)`, output: []string{ `http_requests{job="api-server"} => 2000 @[%v]`, `http_requests{job="app-server"} => 5200 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `http_requests{job="api-server", group="canary"}`, output: []string{ `http_requests{group="canary", instance="0", job="api-server"} => 300 @[%v]`, `http_requests{group="canary", instance="1", job="api-server"} => 400 @[%v]`, }, fullRanges: 0, intervalRanges: 2, }, { expr: `http_requests{job="api-server", group="canary"} + delta(http_requests{job="api-server"}[5m], 1)`, output: []string{ `http_requests{group="canary", instance="0", job="api-server"} => 330 @[%v]`, `http_requests{group="canary", instance="1", job="api-server"} => 440 @[%v]`, }, fullRanges: 4, intervalRanges: 0, }, { expr: `delta(http_requests[25m], 1)`, output: []string{ `http_requests{group="canary", instance="0", job="api-server"} => 150 @[%v]`, `http_requests{group="canary", instance="0", job="app-server"} => 350 @[%v]`, `http_requests{group="canary", instance="1", job="api-server"} => 200 @[%v]`, `http_requests{group="canary", instance="1", job="app-server"} => 400 @[%v]`, `http_requests{group="production", instance="0", job="api-server"} => 50 @[%v]`, `http_requests{group="production", instance="0", job="app-server"} => 250 @[%v]`, `http_requests{group="production", instance="1", job="api-server"} => 100 @[%v]`, `http_requests{group="production", instance="1", job="app-server"} => 300 @[%v]`, }, 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, }, { // Single-letter label names and values. expr: `x{y="testvalue"}`, output: []string{ `x{y="testvalue"} => 100 @[%v]`, }, fullRanges: 0, intervalRanges: 1, }, { // Lower-cased aggregation operators should work too. expr: `sum(http_requests) by (job) + min(http_requests) by (job) + max(http_requests) by (job) + avg(http_requests) by (job)`, output: []string{ `http_requests{job="app-server"} => 4550 @[%v]`, `http_requests{job="api-server"} => 1750 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { // Deltas should be adjusted for target interval vs. samples under target interval. expr: `delta(http_requests{group="canary", instance="1", job="app-server"}[18m], 1)`, output: []string{`http_requests{group="canary", instance="1", job="app-server"} => 288 @[%v]`}, fullRanges: 1, intervalRanges: 0, }, { // Rates should transform per-interval deltas to per-second rates. expr: `rate(http_requests{group="canary", instance="1", job="app-server"}[10m])`, output: []string{`http_requests{group="canary", instance="1", job="app-server"} => 0.26666666666666666 @[%v]`}, fullRanges: 1, intervalRanges: 0, }, { // Counter resets in middle of range are ignored by delta() if counter == 1. expr: `delta(testcounter_reset_middle[50m], 1)`, output: []string{`testcounter_reset_middle => 90 @[%v]`}, fullRanges: 1, intervalRanges: 0, }, { // Counter resets in middle of range are not ignored by delta() if counter == 0. expr: `delta(testcounter_reset_middle[50m], 0)`, output: []string{`testcounter_reset_middle => 50 @[%v]`}, fullRanges: 1, intervalRanges: 0, }, { // Counter resets at end of range are ignored by delta() if counter == 1. expr: `delta(testcounter_reset_end[5m], 1)`, output: []string{`testcounter_reset_end => 0 @[%v]`}, fullRanges: 1, intervalRanges: 0, }, { // Counter resets at end of range are not ignored by delta() if counter == 0. expr: `delta(testcounter_reset_end[5m], 0)`, output: []string{`testcounter_reset_end => -90 @[%v]`}, fullRanges: 1, intervalRanges: 0, }, { // count_scalar for a non-empty vector should return scalar element count. expr: `count_scalar(http_requests)`, output: []string{`scalar: 8 @[%v]`}, fullRanges: 0, intervalRanges: 8, }, { // count_scalar for an empty vector should return scalar 0. expr: `count_scalar(nonexistent)`, output: []string{`scalar: 0 @[%v]`}, fullRanges: 0, intervalRanges: 0, }, { // Empty expressions shouldn"t parse. expr: ``, shouldFail: true, }, { // Interval durations can"t be in quotes. expr: `http_requests["1m"]`, shouldFail: true, }, { // Binop arguments need to be scalar or vector. expr: `http_requests - http_requests[1m]`, shouldFail: true, }, { expr: `http_requests{group!="canary"}`, output: []string{ `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, }, fullRanges: 0, intervalRanges: 4, }, { expr: `http_requests{job=~"server",group!="canary"}`, output: []string{ `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, }, fullRanges: 0, intervalRanges: 4, }, { expr: `http_requests{job!~"api",group!="canary"}`, output: []string{ `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, }, fullRanges: 0, intervalRanges: 2, }, { expr: `count_scalar(http_requests{job=~"^server$"})`, output: []string{`scalar: 0 @[%v]`}, fullRanges: 0, intervalRanges: 0, }, { expr: `http_requests{group="production",job=~"^api"}`, output: []string{ `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, }, fullRanges: 0, intervalRanges: 2, }, } tieredStorage, closer := newTestStorage(t) defer closer.Close() tieredStorage.Flush() for i, exprTest := range expressionTests { expectedLines := annotateWithTime(exprTest.output, testEvalTime) testExpr, err := LoadExprFromString(exprTest.expr) if err != nil { if exprTest.shouldFail { continue } t.Errorf("%d. Error during parsing: %v", i, err) t.Errorf("%d. Expression: %v", i, exprTest.expr) } else { if exprTest.shouldFail { t.Errorf("%d. Test should fail, but didn't", i) } failed := false resultStr := ast.EvalToString(testExpr, testEvalTime, ast.TEXT, tieredStorage, stats.NewTimerGroup()) resultLines := strings.Split(resultStr, "\n") if len(exprTest.output) != len(resultLines) { t.Errorf("%d. Number of samples in expected and actual output don't match", i) failed = true } if exprTest.checkOrder { for j, expectedSample := range expectedLines { if resultLines[j] != expectedSample { t.Errorf("%d.%d. Expected sample '%v', got '%v'", i, j, resultLines[j], expectedSample) failed = true } } } else { for j, expectedSample := range expectedLines { found := false for _, actualSample := range resultLines { if 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(tieredStorage) analyzer.AnalyzeQueries(testExpr) if 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("%d. Count of interval ranges didn't match: %v vs %v", i, exprTest.intervalRanges, len(analyzer.IntervalRanges)) failed = true } if failed { t.Errorf("%d. Expression: %v\n%v", i, exprTest.expr, vectorComparisonString(expectedLines, resultLines)) } } } }
func TestExpressions(t *testing.T) { // Labels in expected output need to be alphabetically sorted. expressionTests := []struct { expr string output []string shouldFail bool checkOrder bool fullRanges int intervalRanges int }{ { expr: `SUM(http_requests)`, output: []string{`{} => 3600 @[%v]`}, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests{instance="0"}) BY(job)`, output: []string{ `{job="api-server"} => 400 @[%v]`, `{job="app-server"} => 1200 @[%v]`, }, fullRanges: 0, intervalRanges: 4, }, { expr: `SUM(http_requests{instance="0"}) BY(job) KEEPING_EXTRA`, output: []string{ `{instance="0", job="api-server"} => 400 @[%v]`, `{instance="0", job="app-server"} => 1200 @[%v]`, }, fullRanges: 0, intervalRanges: 4, }, { expr: `SUM(http_requests) BY (job)`, output: []string{ `{job="api-server"} => 1000 @[%v]`, `{job="app-server"} => 2600 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { // Non-existent labels mentioned in BY-clauses shouldn't propagate to output. expr: `SUM(http_requests) BY (job, nonexistent)`, output: []string{ `{job="api-server"} => 1000 @[%v]`, `{job="app-server"} => 2600 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: ` // Test comment. SUM(http_requests) BY /* comments shouldn't have any effect */ (job) // another comment`, output: []string{ `{job="api-server"} => 1000 @[%v]`, `{job="app-server"} => 2600 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `COUNT(http_requests) BY (job)`, output: []string{ `{job="api-server"} => 4 @[%v]`, `{job="app-server"} => 4 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job, group)`, output: []string{ `{group="canary", job="api-server"} => 700 @[%v]`, `{group="canary", job="app-server"} => 1500 @[%v]`, `{group="production", job="api-server"} => 300 @[%v]`, `{group="production", job="app-server"} => 1100 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `AVG(http_requests) BY (job)`, output: []string{ `{job="api-server"} => 250 @[%v]`, `{job="app-server"} => 650 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `MIN(http_requests) BY (job)`, output: []string{ `{job="api-server"} => 100 @[%v]`, `{job="app-server"} => 500 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `MAX(http_requests) BY (job)`, output: []string{ `{job="api-server"} => 400 @[%v]`, `{job="app-server"} => 800 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) - COUNT(http_requests) BY (job)`, output: []string{ `{job="api-server"} => 996 @[%v]`, `{job="app-server"} => 2596 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `2 - SUM(http_requests) BY (job)`, output: []string{ `{job="api-server"} => -998 @[%v]`, `{job="app-server"} => -2598 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `1000 / SUM(http_requests) BY (job)`, output: []string{ `{job="api-server"} => 1 @[%v]`, `{job="app-server"} => 0.38461538461538464 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) - 2`, output: []string{ `{job="api-server"} => 998 @[%v]`, `{job="app-server"} => 2598 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) % 3`, output: []string{ `{job="api-server"} => 1 @[%v]`, `{job="app-server"} => 2 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) / 0`, output: []string{ `{job="api-server"} => +Inf @[%v]`, `{job="app-server"} => +Inf @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) > 1000`, output: []string{ `{job="app-server"} => 2600 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `1000 < SUM(http_requests) BY (job)`, output: []string{ `{job="app-server"} => 1000 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) <= 1000`, output: []string{ `{job="api-server"} => 1000 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) != 1000`, output: []string{ `{job="app-server"} => 2600 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) == 1000`, output: []string{ `{job="api-server"} => 1000 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `SUM(http_requests) BY (job) + SUM(http_requests) BY (job)`, output: []string{ `{job="api-server"} => 2000 @[%v]`, `{job="app-server"} => 5200 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `http_requests{job="api-server", group="canary"}`, output: []string{ `http_requests{group="canary", instance="0", job="api-server"} => 300 @[%v]`, `http_requests{group="canary", instance="1", job="api-server"} => 400 @[%v]`, }, fullRanges: 0, intervalRanges: 2, }, { expr: `http_requests{job="api-server", group="canary"} + rate(http_requests{job="api-server"}[5m]) * 5 * 60`, output: []string{ `{group="canary", instance="0", job="api-server"} => 330 @[%v]`, `{group="canary", instance="1", job="api-server"} => 440 @[%v]`, }, fullRanges: 4, intervalRanges: 0, }, { expr: `rate(http_requests[25m]) * 25 * 60`, output: []string{ `{group="canary", instance="0", job="api-server"} => 150 @[%v]`, `{group="canary", instance="0", job="app-server"} => 350 @[%v]`, `{group="canary", instance="1", job="api-server"} => 200 @[%v]`, `{group="canary", instance="1", job="app-server"} => 400 @[%v]`, `{group="production", instance="0", job="api-server"} => 50 @[%v]`, `{group="production", instance="0", job="app-server"} => 249.99999999999997 @[%v]`, `{group="production", instance="1", job="api-server"} => 100 @[%v]`, `{group="production", instance="1", job="app-server"} => 300 @[%v]`, }, fullRanges: 8, intervalRanges: 0, }, { expr: `delta(http_requests[25m], 1)`, output: []string{ `{group="canary", instance="0", job="api-server"} => 150 @[%v]`, `{group="canary", instance="0", job="app-server"} => 350 @[%v]`, `{group="canary", instance="1", job="api-server"} => 200 @[%v]`, `{group="canary", instance="1", job="app-server"} => 400 @[%v]`, `{group="production", instance="0", job="api-server"} => 50 @[%v]`, `{group="production", instance="0", job="app-server"} => 250 @[%v]`, `{group="production", instance="1", job="api-server"} => 100 @[%v]`, `{group="production", instance="1", job="app-server"} => 300 @[%v]`, }, 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: `topk(3, http_requests)`, output: []string{ `http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`, `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, }, checkOrder: true, fullRanges: 0, intervalRanges: 8, }, { expr: `topk(5, http_requests{group="canary",job="app-server"})`, output: []string{ `http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`, `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, }, checkOrder: true, fullRanges: 0, intervalRanges: 2, }, { expr: `bottomk(3, http_requests)`, output: []string{ `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, `http_requests{group="canary", instance="0", job="api-server"} => 300 @[%v]`, }, checkOrder: true, fullRanges: 0, intervalRanges: 8, }, { expr: `bottomk(5, http_requests{group="canary",job="app-server"})`, output: []string{ `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, `http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`, }, checkOrder: true, fullRanges: 0, intervalRanges: 2, }, { // Single-letter label names and values. expr: `x{y="testvalue"}`, output: []string{ `x{y="testvalue"} => 100 @[%v]`, }, fullRanges: 0, intervalRanges: 1, }, { // Lower-cased aggregation operators should work too. expr: `sum(http_requests) by (job) + min(http_requests) by (job) + max(http_requests) by (job) + avg(http_requests) by (job)`, output: []string{ `{job="app-server"} => 4550 @[%v]`, `{job="api-server"} => 1750 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { // Deltas should be adjusted for target interval vs. samples under target interval. expr: `delta(http_requests{group="canary", instance="1", job="app-server"}[18m])`, output: []string{`{group="canary", instance="1", job="app-server"} => 288 @[%v]`}, fullRanges: 1, intervalRanges: 0, }, { // Deltas should perform the same operation when 2nd argument is 0. expr: `delta(http_requests{group="canary", instance="1", job="app-server"}[18m], 0)`, output: []string{`{group="canary", instance="1", job="app-server"} => 288 @[%v]`}, fullRanges: 1, intervalRanges: 0, }, { // Rates should calculate per-second rates. expr: `rate(http_requests{group="canary", instance="1", job="app-server"}[60m])`, output: []string{`{group="canary", instance="1", job="app-server"} => 0.26666666666666666 @[%v]`}, fullRanges: 1, intervalRanges: 0, }, { // Deriv should return the same as rate in simple cases. expr: `deriv(http_requests{group="canary", instance="1", job="app-server"}[60m])`, output: []string{`{group="canary", instance="1", job="app-server"} => 0.26666666666666666 @[%v]`}, fullRanges: 1, intervalRanges: 0, }, { // Counter resets at in the middle of range are handled correctly by rate(). expr: `rate(testcounter_reset_middle[60m])`, output: []string{`{} => 0.03 @[%v]`}, fullRanges: 1, intervalRanges: 0, }, { // Counter resets at end of range are ignored by rate(). expr: `rate(testcounter_reset_end[5m])`, output: []string{`{} => 0 @[%v]`}, fullRanges: 1, intervalRanges: 0, }, { // Deriv should return correct result. expr: `deriv(testcounter_reset_middle[100m])`, output: []string{`{} => 0.010606060606060607 @[%v]`}, fullRanges: 1, intervalRanges: 0, }, { // count_scalar for a non-empty vector should return scalar element count. expr: `count_scalar(http_requests)`, output: []string{`scalar: 8 @[%v]`}, fullRanges: 0, intervalRanges: 8, }, { // count_scalar for an empty vector should return scalar 0. expr: `count_scalar(nonexistent)`, output: []string{`scalar: 0 @[%v]`}, fullRanges: 0, intervalRanges: 0, }, { // Empty expressions shouldn't parse. expr: ``, shouldFail: true, }, { // Interval durations can't be in quotes. expr: `http_requests["1m"]`, shouldFail: true, }, { // Binop arguments need to be scalar or vector. expr: `http_requests - http_requests[1m]`, shouldFail: true, }, { expr: `http_requests{group!="canary"}`, output: []string{ `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, }, fullRanges: 0, intervalRanges: 4, }, { expr: `http_requests{job=~"server",group!="canary"}`, output: []string{ `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, }, fullRanges: 0, intervalRanges: 4, }, { expr: `http_requests{job!~"api",group!="canary"}`, output: []string{ `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, }, fullRanges: 0, intervalRanges: 2, }, { expr: `count_scalar(http_requests{job=~"^server$"})`, output: []string{`scalar: 0 @[%v]`}, fullRanges: 0, intervalRanges: 0, }, { expr: `http_requests{group="production",job=~"^api"}`, output: []string{ `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, }, fullRanges: 0, intervalRanges: 2, }, { expr: `abs(-1 * http_requests{group="production",job="api-server"})`, output: []string{ `{group="production", instance="0", job="api-server"} => 100 @[%v]`, `{group="production", instance="1", job="api-server"} => 200 @[%v]`, }, fullRanges: 0, intervalRanges: 2, }, { expr: `avg_over_time(http_requests{group="production",job="api-server"}[1h])`, output: []string{ `{group="production", instance="0", job="api-server"} => 50 @[%v]`, `{group="production", instance="1", job="api-server"} => 100 @[%v]`, }, fullRanges: 2, intervalRanges: 0, }, { expr: `count_over_time(http_requests{group="production",job="api-server"}[1h])`, output: []string{ `{group="production", instance="0", job="api-server"} => 11 @[%v]`, `{group="production", instance="1", job="api-server"} => 11 @[%v]`, }, fullRanges: 2, intervalRanges: 0, }, { expr: `max_over_time(http_requests{group="production",job="api-server"}[1h])`, output: []string{ `{group="production", instance="0", job="api-server"} => 100 @[%v]`, `{group="production", instance="1", job="api-server"} => 200 @[%v]`, }, fullRanges: 2, intervalRanges: 0, }, { expr: `min_over_time(http_requests{group="production",job="api-server"}[1h])`, output: []string{ `{group="production", instance="0", job="api-server"} => 0 @[%v]`, `{group="production", instance="1", job="api-server"} => 0 @[%v]`, }, fullRanges: 2, intervalRanges: 0, }, { expr: `sum_over_time(http_requests{group="production",job="api-server"}[1h])`, output: []string{ `{group="production", instance="0", job="api-server"} => 550 @[%v]`, `{group="production", instance="1", job="api-server"} => 1100 @[%v]`, }, fullRanges: 2, intervalRanges: 0, }, { expr: `time()`, output: []string{`scalar: 3000 @[%v]`}, fullRanges: 0, intervalRanges: 0, }, { expr: `drop_common_labels(http_requests{group="production",job="api-server"})`, output: []string{ `http_requests{instance="0"} => 100 @[%v]`, `http_requests{instance="1"} => 200 @[%v]`, }, fullRanges: 0, intervalRanges: 2, }, { expr: `{` + string(clientmodel.MetricNameLabel) + `=~".*"}`, output: []string{ `http_requests{group="canary", instance="0", job="api-server"} => 300 @[%v]`, `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, `http_requests{group="canary", instance="1", job="api-server"} => 400 @[%v]`, `http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`, `http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`, `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, `http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`, `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, `testcounter_reset_end => 0 @[%v]`, `testcounter_reset_middle => 50 @[%v]`, `x{y="testvalue"} => 100 @[%v]`, }, fullRanges: 0, intervalRanges: 11, }, { expr: `{job=~"server", job!~"api"}`, output: []string{ `http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`, `http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`, `http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`, `http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`, }, fullRanges: 0, intervalRanges: 4, }, { // Test alternative "by"-clause order. expr: `sum by (group) (http_requests{job="api-server"})`, output: []string{ `{group="canary"} => 700 @[%v]`, `{group="production"} => 300 @[%v]`, }, fullRanges: 0, intervalRanges: 4, }, { // Test alternative "by"-clause order with "keeping_extra". expr: `sum by (group) keeping_extra (http_requests{job="api-server"})`, output: []string{ `{group="canary", job="api-server"} => 700 @[%v]`, `{group="production", job="api-server"} => 300 @[%v]`, }, fullRanges: 0, intervalRanges: 4, }, { // Test both alternative "by"-clause orders in one expression. // Public health warning: stick to one form within an expression (or even // in an organization), or risk serious user confusion. expr: `sum(sum by (group) keeping_extra (http_requests{job="api-server"})) by (job)`, output: []string{ `{job="api-server"} => 1000 @[%v]`, }, fullRanges: 0, intervalRanges: 4, }, { expr: `absent(nonexistent)`, output: []string{ `{} => 1 @[%v]`, }, fullRanges: 0, intervalRanges: 0, }, { expr: `absent(nonexistent{job="testjob", instance="testinstance", method=~".*"})`, output: []string{ `{instance="testinstance", job="testjob"} => 1 @[%v]`, }, fullRanges: 0, intervalRanges: 0, }, { expr: `count_scalar(absent(http_requests))`, output: []string{ `scalar: 0 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `count_scalar(absent(sum(http_requests)))`, output: []string{ `scalar: 0 @[%v]`, }, fullRanges: 0, intervalRanges: 8, }, { expr: `absent(sum(nonexistent{job="testjob", instance="testinstance"}))`, output: []string{ `{} => 1 @[%v]`, }, fullRanges: 0, intervalRanges: 0, }, } storage, closer := newTestStorage(t) defer closer.Close() for i, exprTest := range expressionTests { expectedLines := annotateWithTime(exprTest.output, testEvalTime) testExpr, err := LoadExprFromString(exprTest.expr) if err != nil { if exprTest.shouldFail { continue } t.Errorf("%d. Error during parsing: %v", i, err) t.Errorf("%d. Expression: %v", i, exprTest.expr) } else { if exprTest.shouldFail { t.Errorf("%d. Test should fail, but didn't", i) } failed := false resultStr := ast.EvalToString(testExpr, testEvalTime, ast.Text, storage, stats.NewTimerGroup()) resultLines := strings.Split(resultStr, "\n") if len(exprTest.output) != len(resultLines) { t.Errorf("%d. Number of samples in expected and actual output don't match", i) failed = true } if exprTest.checkOrder { for j, expectedSample := range expectedLines { if resultLines[j] != expectedSample { t.Errorf("%d.%d. Expected sample '%v', got '%v'", i, j, resultLines[j], expectedSample) failed = true } } } else { for j, expectedSample := range expectedLines { found := false for _, actualSample := range resultLines { if 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(storage) ast.Walk(analyzer, testExpr) if 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("%d. Count of interval ranges didn't match: %v vs %v", i, exprTest.intervalRanges, len(analyzer.IntervalRanges)) failed = true } if failed { t.Errorf("%d. Expression: %v\n%v", i, exprTest.expr, vectorComparisonString(expectedLines, resultLines)) } } } }