| 
									
										
										
										
											2013-02-07 18:49:04 +08:00
										 |  |  | // 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.
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | package rules | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 	"github.com/prometheus/prometheus/model" | 
					
						
							| 
									
										
										
										
											2013-01-28 01:49:45 +08:00
										 |  |  | 	"github.com/prometheus/prometheus/rules/ast" | 
					
						
							| 
									
										
										
										
											2013-06-03 23:07:03 +08:00
										 |  |  | 	"github.com/prometheus/prometheus/stats" | 
					
						
							| 
									
										
										
										
											2013-02-09 01:03:26 +08:00
										 |  |  | 	"github.com/prometheus/prometheus/storage/metric" | 
					
						
							| 
									
										
										
										
											2013-03-27 01:02:58 +08:00
										 |  |  | 	"github.com/prometheus/prometheus/utility/test" | 
					
						
							| 
									
										
										
										
											2013-04-23 06:26:59 +08:00
										 |  |  | 	"path" | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 	"strings" | 
					
						
							|  |  |  | 	"testing" | 
					
						
							| 
									
										
										
										
											2013-03-22 01:06:15 +08:00
										 |  |  | 	"time" | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-04-23 06:26:59 +08:00
										 |  |  | var ( | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 	testEvalTime = testStartTime.Add(testSampleInterval * 10) | 
					
						
							| 
									
										
										
										
											2013-04-23 06:26:59 +08:00
										 |  |  | 	fixturesPath = "fixtures" | 
					
						
							|  |  |  | ) | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | func annotateWithTime(lines []string, timestamp time.Time) []string { | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 	annotatedLines := []string{} | 
					
						
							|  |  |  | 	for _, line := range lines { | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 		annotatedLines = append(annotatedLines, fmt.Sprintf(line, timestamp)) | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	return annotatedLines | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func vectorComparisonString(expected []string, actual []string) string { | 
					
						
							|  |  |  | 	separator := "\n--------------\n" | 
					
						
							|  |  |  | 	return fmt.Sprintf("Expected:%v%v%v\nActual:%v%v%v ", | 
					
						
							|  |  |  | 		separator, | 
					
						
							|  |  |  | 		strings.Join(expected, "\n"), | 
					
						
							|  |  |  | 		separator, | 
					
						
							|  |  |  | 		separator, | 
					
						
							|  |  |  | 		strings.Join(actual, "\n"), | 
					
						
							|  |  |  | 		separator) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-05-14 22:55:18 +08:00
										 |  |  | type testTieredStorageCloser struct { | 
					
						
							|  |  |  | 	storage   *metric.TieredStorage | 
					
						
							|  |  |  | 	directory test.Closer | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (t testTieredStorageCloser) Close() { | 
					
						
							|  |  |  | 	t.storage.Close() | 
					
						
							|  |  |  | 	t.directory.Close() | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // This is copied from storage/metric/helpers_test.go, which is unfortunate but
 | 
					
						
							|  |  |  | // presently required to make things work.
 | 
					
						
							|  |  |  | func NewTestTieredStorage(t test.Tester) (storage *metric.TieredStorage, closer test.Closer) { | 
					
						
							|  |  |  | 	var directory test.TemporaryDirectory | 
					
						
							|  |  |  | 	directory = test.NewTemporaryDirectory("test_tiered_storage", t) | 
					
						
							| 
									
										
										
										
											2013-05-14 22:17:49 +08:00
										 |  |  | 	storage, err := metric.NewTieredStorage(2500, 1000, 5*time.Second, 0*time.Second, directory.Path()) | 
					
						
							| 
									
										
										
										
											2013-05-14 22:55:18 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		if storage != nil { | 
					
						
							|  |  |  | 			storage.Close() | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		directory.Close() | 
					
						
							|  |  |  | 		t.Fatalf("Error creating storage: %s", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if storage == nil { | 
					
						
							|  |  |  | 		directory.Close() | 
					
						
							|  |  |  | 		t.Fatalf("storage == nil") | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2013-06-06 16:42:21 +08:00
										 |  |  | 	started := make(chan bool) | 
					
						
							|  |  |  | 	go storage.Serve(started) | 
					
						
							|  |  |  | 	<-started | 
					
						
							| 
									
										
										
										
											2013-05-14 22:55:18 +08:00
										 |  |  | 	closer = &testTieredStorageCloser{ | 
					
						
							|  |  |  | 		storage:   storage, | 
					
						
							|  |  |  | 		directory: directory, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-05-03 00:27:12 +08:00
										 |  |  | func newTestStorage(t test.Tester) (storage *metric.TieredStorage, closer test.Closer) { | 
					
						
							| 
									
										
										
										
											2013-05-14 22:55:18 +08:00
										 |  |  | 	storage, closer = NewTestTieredStorage(t) | 
					
						
							| 
									
										
										
										
											2013-05-03 00:27:12 +08:00
										 |  |  | 	if storage == nil { | 
					
						
							|  |  |  | 		t.Fatal("storage == nil") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	storeMatrix(*storage, testMatrix) | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 	return | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-05-08 22:35:16 +08:00
										 |  |  | func TestExpressions(t *testing.T) { | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 	// 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) BY (job)", | 
					
						
							|  |  |  | 			output: []string{ | 
					
						
							|  |  |  | 				"http_requests{job='api-server'} => 1000 @[%v]", | 
					
						
							|  |  |  | 				"http_requests{job='app-server'} => 2600 @[%v]", | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			fullRanges:     0, | 
					
						
							|  |  |  | 			intervalRanges: 8, | 
					
						
							| 
									
										
										
										
											2013-05-08 22:35:16 +08:00
										 |  |  | 		}, { | 
					
						
							|  |  |  | 			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, | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 		}, { | 
					
						
							|  |  |  | 			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, | 
					
						
							|  |  |  | 		}, { | 
					
						
							| 
									
										
										
										
											2013-05-08 22:35:16 +08:00
										 |  |  | 			expr: "SUM(http_requests) BY (job) - COUNT(http_requests) BY (job)", | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 			output: []string{ | 
					
						
							| 
									
										
										
										
											2013-05-08 22:35:16 +08:00
										 |  |  | 				"http_requests{job='api-server'} => 996 @[%v]", | 
					
						
							|  |  |  | 				"http_requests{job='app-server'} => 2596 @[%v]", | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 			}, | 
					
						
							|  |  |  | 			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: "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, | 
					
						
							| 
									
										
										
										
											2013-05-12 08:03:16 +08:00
										 |  |  | 		}, { | 
					
						
							| 
									
										
										
										
											2013-05-28 23:36:53 +08:00
										 |  |  | 			// 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]"}, | 
					
						
							| 
									
										
										
										
											2013-05-12 08:03:16 +08:00
										 |  |  | 			fullRanges:     1, | 
					
						
							|  |  |  | 			intervalRanges: 0, | 
					
						
							|  |  |  | 		}, { | 
					
						
							| 
									
										
										
										
											2013-05-28 23:36:53 +08:00
										 |  |  | 			// 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]"}, | 
					
						
							| 
									
										
										
										
											2013-05-12 08:03:16 +08:00
										 |  |  | 			fullRanges:     1, | 
					
						
							|  |  |  | 			intervalRanges: 0, | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 		}, { | 
					
						
							|  |  |  | 			// Empty expressions shouldn't parse.
 | 
					
						
							|  |  |  | 			expr:       "", | 
					
						
							|  |  |  | 			shouldFail: true, | 
					
						
							|  |  |  | 		}, { | 
					
						
							|  |  |  | 			// Subtracting a vector from a scalar is not supported.
 | 
					
						
							|  |  |  | 			expr:       "1 - http_requests", | 
					
						
							|  |  |  | 			shouldFail: true, | 
					
						
							|  |  |  | 		}, { | 
					
						
							|  |  |  | 			// Interval durations can't be in quotes.
 | 
					
						
							|  |  |  | 			expr:       "http_requests['1m']", | 
					
						
							|  |  |  | 			shouldFail: true, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2013-03-22 01:06:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 	tieredStorage, closer := newTestStorage(t) | 
					
						
							|  |  |  | 	defer closer.Close() | 
					
						
							| 
									
										
										
										
											2013-03-22 01:06:15 +08:00
										 |  |  | 	tieredStorage.Flush() | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-04-10 16:44:13 +08:00
										 |  |  | 	for i, exprTest := range expressionTests { | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 		expectedLines := annotateWithTime(exprTest.output, testEvalTime) | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-01-11 08:17:37 +08:00
										 |  |  | 		testExpr, err := LoadExprFromString(exprTest.expr) | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							| 
									
										
										
										
											2013-01-11 08:17:37 +08:00
										 |  |  | 			if exprTest.shouldFail { | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 				continue | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 			t.Errorf("%d. Error during parsing: %v", i, err) | 
					
						
							|  |  |  | 			t.Errorf("%d. Expression: %v", i, exprTest.expr) | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 		} else { | 
					
						
							| 
									
										
										
										
											2013-03-22 01:06:15 +08:00
										 |  |  | 			if exprTest.shouldFail { | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 				t.Errorf("%d. Test should fail, but didn't", i) | 
					
						
							| 
									
										
										
										
											2013-03-22 01:06:15 +08:00
										 |  |  | 			} | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 			failed := false | 
					
						
							| 
									
										
										
										
											2013-06-03 23:07:03 +08:00
										 |  |  | 			resultStr := ast.EvalToString(testExpr, testEvalTime, ast.TEXT, tieredStorage, stats.NewTimerGroup()) | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 			resultLines := strings.Split(resultStr, "\n") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-01-11 08:17:37 +08:00
										 |  |  | 			if len(exprTest.output) != len(resultLines) { | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 				t.Errorf("%d. Number of samples in expected and actual output don't match", i) | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 				failed = true | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2013-04-10 16:44:13 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			if exprTest.checkOrder { | 
					
						
							|  |  |  | 				for j, expectedSample := range expectedLines { | 
					
						
							|  |  |  | 					if resultLines[j] != expectedSample { | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 						t.Errorf("%d.%d. Expected sample '%v', got '%v'", i, j, resultLines[j], expectedSample) | 
					
						
							| 
									
										
										
										
											2013-04-10 16:44:13 +08:00
										 |  |  | 						failed = true | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 					} | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2013-04-10 16:44:13 +08:00
										 |  |  | 			} else { | 
					
						
							|  |  |  | 				for j, expectedSample := range expectedLines { | 
					
						
							|  |  |  | 					found := false | 
					
						
							|  |  |  | 					for _, actualSample := range resultLines { | 
					
						
							|  |  |  | 						if actualSample == expectedSample { | 
					
						
							|  |  |  | 							found = true | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 					if !found { | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 						t.Errorf("%d.%d. Couldn't find expected sample in output: '%v'", i, j, expectedSample) | 
					
						
							| 
									
										
										
										
											2013-04-10 16:44:13 +08:00
										 |  |  | 						failed = true | 
					
						
							|  |  |  | 					} | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2013-03-22 01:06:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-05-07 19:15:10 +08:00
										 |  |  | 			analyzer := ast.NewQueryAnalyzer(tieredStorage) | 
					
						
							| 
									
										
										
										
											2013-03-22 01:06:15 +08:00
										 |  |  | 			analyzer.AnalyzeQueries(testExpr) | 
					
						
							|  |  |  | 			if exprTest.fullRanges != len(analyzer.FullRanges) { | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 				t.Errorf("%d. Count of full ranges didn't match: %v vs %v", i, exprTest.fullRanges, len(analyzer.FullRanges)) | 
					
						
							| 
									
										
										
										
											2013-03-22 01:06:15 +08:00
										 |  |  | 				failed = true | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if exprTest.intervalRanges != len(analyzer.IntervalRanges) { | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 				t.Errorf("%d. Count of interval ranges didn't match: %v vs %v", i, exprTest.intervalRanges, len(analyzer.IntervalRanges)) | 
					
						
							| 
									
										
										
										
											2013-03-22 01:06:15 +08:00
										 |  |  | 				failed = true | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 			if failed { | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 				t.Errorf("%d. Expression: %v\n%v", i, exprTest.expr, vectorComparisonString(expectedLines, resultLines)) | 
					
						
							| 
									
										
										
										
											2013-01-08 06:24:26 +08:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2013-04-23 06:26:59 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | var ruleTests = []struct { | 
					
						
							|  |  |  | 	inputFile         string | 
					
						
							|  |  |  | 	shouldFail        bool | 
					
						
							|  |  |  | 	errContains       string | 
					
						
							|  |  |  | 	numRecordingRules int | 
					
						
							|  |  |  | 	numAlertingRules  int | 
					
						
							|  |  |  | }{ | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		inputFile:         "empty.rules", | 
					
						
							|  |  |  | 		numRecordingRules: 0, | 
					
						
							|  |  |  | 		numAlertingRules:  0, | 
					
						
							|  |  |  | 	}, { | 
					
						
							|  |  |  | 		inputFile:         "mixed.rules", | 
					
						
							|  |  |  | 		numRecordingRules: 2, | 
					
						
							|  |  |  | 		numAlertingRules:  2, | 
					
						
							|  |  |  | 	}, | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		inputFile:   "syntax_error.rules", | 
					
						
							|  |  |  | 		shouldFail:  true, | 
					
						
							|  |  |  | 		errContains: "Error parsing rules at line 3", | 
					
						
							|  |  |  | 	}, | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		inputFile:   "non_vector.rules", | 
					
						
							|  |  |  | 		shouldFail:  true, | 
					
						
							|  |  |  | 		errContains: "does not evaluate to vector type", | 
					
						
							|  |  |  | 	}, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func TestRules(t *testing.T) { | 
					
						
							|  |  |  | 	for i, ruleTest := range ruleTests { | 
					
						
							|  |  |  | 		testRules, err := LoadRulesFromFile(path.Join(fixturesPath, ruleTest.inputFile)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			if !ruleTest.shouldFail { | 
					
						
							|  |  |  | 				t.Fatalf("%d. Error parsing rules file %v: %v", i, ruleTest.inputFile, err) | 
					
						
							|  |  |  | 			} else { | 
					
						
							|  |  |  | 				if !strings.Contains(err.Error(), ruleTest.errContains) { | 
					
						
							|  |  |  | 					t.Fatalf("%d. Expected error containing '%v', got: %v", i, ruleTest.errContains, err) | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			numRecordingRules := 0 | 
					
						
							|  |  |  | 			numAlertingRules := 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			for j, rule := range testRules { | 
					
						
							|  |  |  | 				switch rule.(type) { | 
					
						
							|  |  |  | 				case *RecordingRule: | 
					
						
							|  |  |  | 					numRecordingRules++ | 
					
						
							|  |  |  | 				case *AlertingRule: | 
					
						
							|  |  |  | 					numAlertingRules++ | 
					
						
							|  |  |  | 				default: | 
					
						
							|  |  |  | 					t.Fatalf("%d.%d. Unknown rule type!", i, j) | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if numRecordingRules != ruleTest.numRecordingRules { | 
					
						
							|  |  |  | 				t.Fatalf("%d. Expected %d recording rules, got %d", i, ruleTest.numRecordingRules, numRecordingRules) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if numAlertingRules != ruleTest.numAlertingRules { | 
					
						
							|  |  |  | 				t.Fatalf("%d. Expected %d alerting rules, got %d", i, ruleTest.numAlertingRules, numAlertingRules) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// TODO(julius): add more complex checks on the parsed rules here.
 | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | func TestAlertingRule(t *testing.T) { | 
					
						
							|  |  |  | 	// Labels in expected output need to be alphabetically sorted.
 | 
					
						
							|  |  |  | 	var evalOutputs = [][]string{ | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			"ALERTS{alertname='HttpRequestRateLow',alertstate='pending',group='canary',instance='0',job='app-server'} => 1 @[%v]", | 
					
						
							|  |  |  | 			"ALERTS{alertname='HttpRequestRateLow',alertstate='pending',group='canary',instance='1',job='app-server'} => 1 @[%v]", | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			"ALERTS{alertname='HttpRequestRateLow',alertstate='pending',group='canary',instance='0',job='app-server'} => 0 @[%v]", | 
					
						
							|  |  |  | 			"ALERTS{alertname='HttpRequestRateLow',alertstate='firing',group='canary',instance='0',job='app-server'} => 1 @[%v]", | 
					
						
							|  |  |  | 			"ALERTS{alertname='HttpRequestRateLow',alertstate='pending',group='canary',instance='1',job='app-server'} => 0 @[%v]", | 
					
						
							|  |  |  | 			"ALERTS{alertname='HttpRequestRateLow',alertstate='firing',group='canary',instance='1',job='app-server'} => 1 @[%v]", | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			"ALERTS{alertname='HttpRequestRateLow',alertstate='firing',group='canary',instance='1',job='app-server'} => 0 @[%v]", | 
					
						
							|  |  |  | 			"ALERTS{alertname='HttpRequestRateLow',alertstate='firing',group='canary',instance='0',job='app-server'} => 0 @[%v]", | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 		/* empty */ | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 		/* empty */ | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	tieredStorage, closer := newTestStorage(t) | 
					
						
							|  |  |  | 	defer closer.Close() | 
					
						
							|  |  |  | 	tieredStorage.Flush() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	alertExpr, err := LoadExprFromString("http_requests{group='canary',job='app-server'} < 100") | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		t.Fatalf("Unable to parse alert expression: %s", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	alertName := "HttpRequestRateLow" | 
					
						
							|  |  |  | 	alertLabels := model.LabelSet{ | 
					
						
							|  |  |  | 		"summary": "HTTP request rate is low", | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	rule := NewAlertingRule(alertName, alertExpr.(ast.VectorNode), time.Minute, alertLabels) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for i, expected := range evalOutputs { | 
					
						
							|  |  |  | 		evalTime := testStartTime.Add(testSampleInterval * time.Duration(i)) | 
					
						
							| 
									
										
										
										
											2013-05-07 19:15:10 +08:00
										 |  |  | 		actual, err := rule.Eval(evalTime, tieredStorage) | 
					
						
							| 
									
										
										
										
											2013-04-24 17:51:40 +08:00
										 |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			t.Fatalf("Error during alerting rule evaluation: %s", err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		actualLines := strings.Split(actual.String(), "\n") | 
					
						
							|  |  |  | 		expectedLines := annotateWithTime(expected, evalTime) | 
					
						
							|  |  |  | 		if actualLines[0] == "" { | 
					
						
							|  |  |  | 			actualLines = []string{} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		failed := false | 
					
						
							|  |  |  | 		if len(actualLines) != len(expectedLines) { | 
					
						
							|  |  |  | 			t.Errorf("%d. Number of samples in expected and actual output don't match (%d vs. %d)", i, len(expectedLines), len(actualLines)) | 
					
						
							|  |  |  | 			failed = true | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		for j, expectedSample := range expectedLines { | 
					
						
							|  |  |  | 			found := false | 
					
						
							|  |  |  | 			for _, actualSample := range actualLines { | 
					
						
							|  |  |  | 				if actualSample == expectedSample { | 
					
						
							|  |  |  | 					found = true | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if !found { | 
					
						
							|  |  |  | 				t.Errorf("%d.%d. Couldn't find expected sample in output: '%v'", i, j, expectedSample) | 
					
						
							|  |  |  | 				failed = true | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if failed { | 
					
						
							|  |  |  | 			t.Fatalf("%d. Expected and actual outputs don't match:\n%v", i, vectorComparisonString(expectedLines, actualLines)) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } |