1// Copyright (c) The Thanos Authors. 2// Licensed under the Apache License 2.0. 3 4package v1 5 6import ( 7 "context" 8 "encoding/json" 9 "fmt" 10 "io/ioutil" 11 "net/http" 12 "net/url" 13 "os" 14 "reflect" 15 "testing" 16 "time" 17 18 "github.com/go-kit/kit/log" 19 "github.com/prometheus/client_golang/prometheus" 20 "github.com/prometheus/common/model" 21 "github.com/prometheus/common/route" 22 "github.com/prometheus/prometheus/pkg/labels" 23 "github.com/prometheus/prometheus/promql" 24 "github.com/prometheus/prometheus/rules" 25 "github.com/prometheus/prometheus/storage" 26 "github.com/prometheus/prometheus/storage/tsdb" 27 qapi "github.com/thanos-io/thanos/pkg/query/api" 28 thanosrule "github.com/thanos-io/thanos/pkg/rule" 29 "github.com/thanos-io/thanos/pkg/store/storepb" 30) 31 32// NewStorage returns a new storage for testing purposes 33// that removes all associated files on closing. 34func newStorage(t *testing.T) storage.Storage { 35 dir, err := ioutil.TempDir("", "test_storage") 36 if err != nil { 37 t.Fatalf("Opening test dir failed: %s", err) 38 } 39 40 // Tests just load data for a series sequentially. Thus we 41 // need a long appendable window. 42 db, err := tsdb.Open(dir, nil, nil, &tsdb.Options{ 43 MinBlockDuration: model.Duration(24 * time.Hour), 44 MaxBlockDuration: model.Duration(24 * time.Hour), 45 }) 46 if err != nil { 47 t.Fatalf("Opening test storage failed: %s", err) 48 } 49 return testStorage{Storage: tsdb.Adapter(db, int64(0)), dir: dir} 50} 51 52type testStorage struct { 53 storage.Storage 54 dir string 55} 56 57func (s testStorage) Close() error { 58 if err := s.Storage.Close(); err != nil { 59 return err 60 } 61 return os.RemoveAll(s.dir) 62} 63 64type rulesRetrieverMock struct { 65 testing *testing.T 66} 67 68func (m rulesRetrieverMock) RuleGroups() []thanosrule.Group { 69 storage := newStorage(m.testing) 70 71 engineOpts := promql.EngineOpts{ 72 Logger: nil, 73 Reg: nil, 74 MaxConcurrent: 10, 75 MaxSamples: 10, 76 Timeout: 100 * time.Second, 77 } 78 79 engine := promql.NewEngine(engineOpts) 80 opts := &rules.ManagerOptions{ 81 QueryFunc: rules.EngineQueryFunc(engine, storage), 82 Appendable: storage, 83 Context: context.Background(), 84 Logger: log.NewNopLogger(), 85 } 86 87 var r []rules.Rule 88 for _, ar := range alertingRules(m.testing) { 89 r = append(r, ar) 90 } 91 92 recordingExpr, err := promql.ParseExpr(`vector(1)`) 93 if err != nil { 94 m.testing.Fatalf("unable to parse alert expression: %s", err) 95 } 96 recordingRule := rules.NewRecordingRule("recording-rule-1", recordingExpr, labels.Labels{}) 97 r = append(r, recordingRule) 98 99 return []thanosrule.Group{ 100 thanosrule.Group{ 101 Group: rules.NewGroup("grp", "/path/to/file", time.Second, r, false, opts), 102 PartialResponseStrategy: storepb.PartialResponseStrategy_WARN, 103 }, 104 } 105} 106 107func (m rulesRetrieverMock) AlertingRules() []thanosrule.AlertingRule { 108 var ars []thanosrule.AlertingRule 109 for _, ar := range alertingRules(m.testing) { 110 ars = append(ars, thanosrule.AlertingRule{AlertingRule: ar}) 111 } 112 return ars 113} 114 115func alertingRules(t *testing.T) []*rules.AlertingRule { 116 expr1, err := promql.ParseExpr(`absent(test_metric3) != 1`) 117 if err != nil { 118 t.Fatalf("unable to parse alert expression: %s", err) 119 } 120 expr2, err := promql.ParseExpr(`up == 1`) 121 if err != nil { 122 t.Fatalf("unable to parse alert expression: %s", err) 123 } 124 125 return []*rules.AlertingRule{ 126 rules.NewAlertingRule( 127 "test_metric3", 128 expr1, 129 time.Second, 130 labels.Labels{}, 131 labels.Labels{}, 132 labels.Labels{}, 133 true, 134 log.NewNopLogger(), 135 ), 136 rules.NewAlertingRule( 137 "test_metric4", 138 expr2, 139 time.Second, 140 labels.Labels{}, 141 labels.Labels{}, 142 labels.Labels{}, 143 true, 144 log.NewNopLogger(), 145 ), 146 } 147} 148 149func TestEndpoints(t *testing.T) { 150 suite, err := promql.NewTest(t, ` 151 load 1m 152 test_metric1{foo="bar"} 0+100x100 153 test_metric1{foo="boo"} 1+0x100 154 test_metric2{foo="boo"} 1+0x100 155 `) 156 if err != nil { 157 t.Fatal(err) 158 } 159 defer suite.Close() 160 161 if err := suite.Run(); err != nil { 162 t.Fatal(err) 163 } 164 165 var algr rulesRetrieverMock 166 algr.testing = t 167 algr.AlertingRules() 168 algr.RuleGroups() 169 170 t.Run("local", func(t *testing.T) { 171 var algr rulesRetrieverMock 172 algr.testing = t 173 algr.AlertingRules() 174 algr.RuleGroups() 175 api := NewAPI( 176 nil, 177 prometheus.DefaultRegisterer, 178 algr, 179 ) 180 testEndpoints(t, api) 181 }) 182} 183 184func testEndpoints(t *testing.T, api *API) { 185 type test struct { 186 endpointFn qapi.ApiFunc 187 endpointName string 188 params map[string]string 189 query url.Values 190 response interface{} 191 } 192 var tests = []test{ 193 { 194 endpointFn: api.rules, 195 endpointName: "rules", 196 response: &RuleDiscovery{ 197 RuleGroups: []*RuleGroup{ 198 { 199 Name: "grp", 200 File: "", 201 Interval: 1, 202 PartialResponseStrategy: "WARN", 203 Rules: []rule{ 204 alertingRule{ 205 Name: "test_metric3", 206 Query: "absent(test_metric3) != 1", 207 Duration: 1, 208 Labels: labels.Labels{}, 209 Annotations: labels.Labels{}, 210 Alerts: []*Alert{}, 211 Health: "unknown", 212 Type: "alerting", 213 PartialResponseStrategy: "WARN", 214 }, 215 alertingRule{ 216 Name: "test_metric4", 217 Query: "up == 1", 218 Duration: 1, 219 Labels: labels.Labels{}, 220 Annotations: labels.Labels{}, 221 Alerts: []*Alert{}, 222 Health: "unknown", 223 Type: "alerting", 224 PartialResponseStrategy: "WARN", 225 }, 226 recordingRule{ 227 Name: "recording-rule-1", 228 Query: "vector(1)", 229 Labels: labels.Labels{}, 230 Health: "unknown", 231 Type: "recording", 232 }, 233 }, 234 }, 235 }, 236 }, 237 }, 238 } 239 240 methods := func(f qapi.ApiFunc) []string { 241 return []string{http.MethodGet} 242 } 243 244 request := func(m string, q url.Values) (*http.Request, error) { 245 return http.NewRequest(m, fmt.Sprintf("http://example.com?%s", q.Encode()), nil) 246 } 247 for _, test := range tests { 248 for _, method := range methods(test.endpointFn) { 249 t.Run(fmt.Sprintf("endpoint=%s/method=%s/query=%q", test.endpointName, method, test.query.Encode()), func(t *testing.T) { 250 // Build a context with the correct request params. 251 ctx := context.Background() 252 for p, v := range test.params { 253 ctx = route.WithParam(ctx, p, v) 254 } 255 256 req, err := request(method, test.query) 257 if err != nil { 258 t.Fatal(err) 259 } 260 endpoint, errors, apiError := test.endpointFn(req.WithContext(ctx)) 261 262 if errors != nil { 263 t.Fatalf("Unexpected errors: %s", errors) 264 return 265 } 266 assertAPIError(t, apiError) 267 assertAPIResponse(t, endpoint, test.response) 268 }) 269 } 270 } 271} 272 273func assertAPIError(t *testing.T, got *qapi.ApiError) { 274 t.Helper() 275 if got != nil { 276 t.Fatalf("Unexpected error: %s", got) 277 } 278} 279 280func assertAPIResponse(t *testing.T, got interface{}, exp interface{}) { 281 t.Helper() 282 if !reflect.DeepEqual(exp, got) { 283 respJSON, err := json.Marshal(got) 284 if err != nil { 285 t.Fatalf("failed to marshal response as JSON: %v", err.Error()) 286 } 287 288 expectedRespJSON, err := json.Marshal(exp) 289 if err != nil { 290 t.Fatalf("failed to marshal expected response as JSON: %v", err.Error()) 291 } 292 293 t.Fatalf( 294 "Response does not match, expected:\n%+v\ngot:\n%+v", 295 string(expectedRespJSON), 296 string(respJSON), 297 ) 298 } 299} 300