github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/remote/codecs_test.go (about) 1 // Copyright (c) 2018 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package remote 22 23 import ( 24 "context" 25 "fmt" 26 "math" 27 "net/http" 28 "strings" 29 "testing" 30 "time" 31 32 "github.com/m3db/m3/src/metrics/generated/proto/policypb" 33 "github.com/m3db/m3/src/metrics/policy" 34 "github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions" 35 "github.com/m3db/m3/src/query/generated/proto/rpcpb" 36 rpc "github.com/m3db/m3/src/query/generated/proto/rpcpb" 37 "github.com/m3db/m3/src/query/models" 38 "github.com/m3db/m3/src/query/storage" 39 "github.com/m3db/m3/src/query/storage/m3/storagemetadata" 40 "github.com/m3db/m3/src/query/test" 41 "github.com/m3db/m3/src/query/util/logging" 42 "github.com/m3db/m3/src/x/instrument" 43 xtime "github.com/m3db/m3/src/x/time" 44 45 "github.com/stretchr/testify/assert" 46 "github.com/stretchr/testify/require" 47 "google.golang.org/grpc/metadata" 48 ) 49 50 var ( 51 now = time.Now() 52 name0 = []byte("regex") 53 val0 = []byte("[a-z]") 54 valList0 = []*rpc.Datapoint{{1, 1.0}, {2, 2.0}, {3, 3.0}} 55 time0 = "2000-02-06T11:54:48+07:00" 56 57 name1 = []byte("eq") 58 val1 = []byte("val") 59 valList1 = []*rpc.Datapoint{{1, 4.0}, {2, 5.0}, {3, 6.0}} 60 61 valList2 = []*rpc.Datapoint{ 62 {fromTime(now.Add(-3 * time.Minute)), 4.0}, 63 {fromTime(now.Add(-2 * time.Minute)), 5.0}, 64 {fromTime(now.Add(-1 * time.Minute)), 6.0}, 65 } 66 67 time1 = "2093-02-06T11:54:48+07:00" 68 69 tags0 = test.StringTagsToTags(test.StringTags{{N: "a", V: "b"}, {N: "c", V: "d"}}) 70 tags1 = test.StringTagsToTags(test.StringTags{{N: "e", V: "f"}, {N: "g", V: "h"}}) 71 ) 72 73 func parseTimes(t *testing.T) (time.Time, time.Time) { 74 t0, err := time.Parse(time.RFC3339, time0) 75 require.Nil(t, err) 76 t1, err := time.Parse(time.RFC3339, time1) 77 require.Nil(t, err) 78 return t0, t1 79 } 80 81 func TestTimeConversions(t *testing.T) { 82 time, _ := parseTimes(t) 83 tix := fromTime(time) 84 assert.True(t, time.Equal(toTime(tix))) 85 assert.Equal(t, tix, fromTime(toTime(tix))) 86 } 87 88 func createRPCSeries() []*rpc.DecompressedSeries { 89 return []*rpc.DecompressedSeries{ 90 { 91 Datapoints: valList0, 92 Tags: encodeTags(tags0), 93 }, 94 { 95 Datapoints: valList1, 96 Tags: encodeTags(tags1), 97 }, 98 { 99 Datapoints: valList2, 100 Tags: encodeTags(tags1), 101 }, 102 } 103 } 104 105 func readQueriesAreEqual(t *testing.T, this, other *storage.FetchQuery) { 106 assert.True(t, this.Start.Equal(other.Start)) 107 assert.True(t, this.End.Equal(other.End)) 108 assert.Equal(t, len(this.TagMatchers), len(other.TagMatchers)) 109 assert.Equal(t, 2, len(other.TagMatchers)) 110 for i, matcher := range this.TagMatchers { 111 assert.Equal(t, matcher.Type, other.TagMatchers[i].Type) 112 assert.Equal(t, matcher.Name, other.TagMatchers[i].Name) 113 assert.Equal(t, matcher.Value, other.TagMatchers[i].Value) 114 } 115 } 116 117 func createStorageFetchQuery(t *testing.T) (*storage.FetchQuery, time.Time, time.Time) { 118 m0, err := models.NewMatcher(models.MatchRegexp, name0, val0) 119 require.Nil(t, err) 120 m1, err := models.NewMatcher(models.MatchEqual, name1, val1) 121 require.Nil(t, err) 122 start, end := parseTimes(t) 123 124 matchers := []models.Matcher{m0, m1} 125 return &storage.FetchQuery{ 126 TagMatchers: matchers, 127 Start: start, 128 End: end, 129 }, start, end 130 } 131 132 func TestEncodeFetchMessage(t *testing.T) { 133 rQ, start, end := createStorageFetchQuery(t) 134 fetchOpts := storage.NewFetchOptions() 135 fetchOpts.SeriesLimit = 42 136 fetchOpts.RestrictQueryOptions = &storage.RestrictQueryOptions{ 137 RestrictByType: &storage.RestrictByType{ 138 MetricsType: storagemetadata.AggregatedMetricsType, 139 StoragePolicy: policy.MustParseStoragePolicy("1m:14d"), 140 }, 141 } 142 lookback := time.Minute 143 fetchOpts.LookbackDuration = &lookback 144 145 grpcQ, err := encodeFetchRequest(rQ, fetchOpts) 146 require.NoError(t, err) 147 require.NotNil(t, grpcQ) 148 assert.Equal(t, fromTime(start), grpcQ.GetStart()) 149 assert.Equal(t, fromTime(end), grpcQ.GetEnd()) 150 mRPC := grpcQ.GetTagMatchers().GetTagMatchers() 151 assert.Equal(t, 2, len(mRPC)) 152 assert.Equal(t, name0, mRPC[0].GetName()) 153 assert.Equal(t, val0, mRPC[0].GetValue()) 154 assert.Equal(t, models.MatchRegexp, models.MatchType(mRPC[0].GetType())) 155 assert.Equal(t, name1, mRPC[1].GetName()) 156 assert.Equal(t, val1, mRPC[1].GetValue()) 157 assert.Equal(t, models.MatchEqual, models.MatchType(mRPC[1].GetType())) 158 require.NotNil(t, grpcQ.Options) 159 assert.Equal(t, int64(42), grpcQ.Options.Limit) 160 require.NotNil(t, grpcQ.Options.Restrict) 161 require.NotNil(t, grpcQ.Options.Restrict.RestrictQueryType) 162 assert.Equal(t, rpc.MetricsType_AGGREGATED_METRICS_TYPE, 163 grpcQ.Options.Restrict.RestrictQueryType.MetricsType) 164 require.NotNil(t, grpcQ.Options.Restrict.RestrictQueryType.MetricsStoragePolicy) 165 expectedStoragePolicyProto, err := fetchOpts.RestrictQueryOptions. 166 RestrictByType.StoragePolicy.Proto() 167 require.NoError(t, err) 168 assert.Equal(t, expectedStoragePolicyProto, grpcQ.Options.Restrict. 169 RestrictQueryType.MetricsStoragePolicy) 170 assert.Equal(t, lookback, time.Duration(grpcQ.Options.LookbackDuration)) 171 } 172 173 func TestEncodeDecodeFetchQuery(t *testing.T) { 174 rQ, _, _ := createStorageFetchQuery(t) 175 fetchOpts := storage.NewFetchOptions() 176 fetchOpts.SeriesLimit = 42 177 fetchOpts.RestrictQueryOptions = &storage.RestrictQueryOptions{ 178 RestrictByType: &storage.RestrictByType{ 179 MetricsType: storagemetadata.AggregatedMetricsType, 180 StoragePolicy: policy.MustParseStoragePolicy("1m:14d"), 181 }, 182 } 183 lookback := time.Minute 184 fetchOpts.LookbackDuration = &lookback 185 186 gq, err := encodeFetchRequest(rQ, fetchOpts) 187 require.NoError(t, err) 188 reverted, err := decodeFetchRequest(gq) 189 require.NoError(t, err) 190 readQueriesAreEqual(t, rQ, reverted) 191 revertedOpts, err := decodeFetchOptions(gq.GetOptions()) 192 require.NoError(t, err) 193 require.NotNil(t, revertedOpts) 194 require.Equal(t, fetchOpts.SeriesLimit, revertedOpts.SeriesLimit) 195 require.Equal(t, fetchOpts.RestrictQueryOptions. 196 RestrictByType.MetricsType, 197 revertedOpts.RestrictQueryOptions.RestrictByType.MetricsType) 198 require.Equal(t, fetchOpts.RestrictQueryOptions. 199 RestrictByType.StoragePolicy.String(), 200 revertedOpts.RestrictQueryOptions.RestrictByType.StoragePolicy.String()) 201 require.NotNil(t, revertedOpts.LookbackDuration) 202 require.Equal(t, lookback, *revertedOpts.LookbackDuration) 203 204 // Encode again 205 gqr, err := encodeFetchRequest(reverted, revertedOpts) 206 require.NoError(t, err) 207 assert.Equal(t, gq, gqr) 208 } 209 210 func TestEncodeMetadata(t *testing.T) { 211 headers := make(http.Header) 212 headers.Add("Foo", "bar") 213 headers.Add("Foo", "baz") 214 headers.Add("Foo", "abc") 215 headers.Add("lorem", "ipsum") 216 ctx := context.WithValue(context.Background(), handleroptions.RequestHeaderKey, headers) 217 requestID := "requestID" 218 219 encodedCtx := encodeMetadata(ctx, requestID) 220 md, ok := metadata.FromOutgoingContext(encodedCtx) 221 require.True(t, ok) 222 assert.Equal(t, []string{"bar", "baz", "abc"}, md["foo"], "metadat keys must be lower case") 223 assert.Equal(t, []string{"ipsum"}, md["lorem"]) 224 assert.Equal(t, []string{requestID}, md[reqIDKey]) 225 } 226 227 func TestRetrieveMetadata(t *testing.T) { 228 headers := make(http.Header) 229 headers.Add("Foo", "bar") 230 headers.Add("Foo", "baz") 231 headers.Add("Foo", "abc") 232 headers.Add("Lorem", "ipsum") 233 requestID := "requestID" 234 headers[reqIDKey] = []string{requestID} 235 ctx := metadata.NewIncomingContext(context.TODO(), metadata.MD(headers)) 236 encodedCtx := retrieveMetadata(ctx, instrument.NewOptions()) 237 238 require.Equal(t, requestID, logging.ReadContextID(encodedCtx)) 239 } 240 241 func TestNewRestrictQueryOptionsFromProto(t *testing.T) { 242 tests := []struct { 243 value *rpcpb.RestrictQueryOptions 244 expected *storage.RestrictQueryOptions 245 errContains string 246 }{ 247 { 248 value: &rpcpb.RestrictQueryOptions{ 249 RestrictQueryType: &rpcpb.RestrictQueryType{ 250 MetricsType: rpcpb.MetricsType_UNAGGREGATED_METRICS_TYPE, 251 }, 252 }, 253 expected: &storage.RestrictQueryOptions{ 254 RestrictByType: &storage.RestrictByType{ 255 MetricsType: storagemetadata.UnaggregatedMetricsType, 256 }, 257 }, 258 }, 259 { 260 value: &rpcpb.RestrictQueryOptions{ 261 RestrictQueryType: &rpcpb.RestrictQueryType{ 262 MetricsType: rpcpb.MetricsType_AGGREGATED_METRICS_TYPE, 263 MetricsStoragePolicy: &policypb.StoragePolicy{ 264 Resolution: policypb.Resolution{ 265 WindowSize: int64(time.Minute), 266 Precision: int64(time.Second), 267 }, 268 Retention: policypb.Retention{ 269 Period: int64(24 * time.Hour), 270 }, 271 }, 272 }, 273 RestrictQueryTags: &rpc.RestrictQueryTags{ 274 Restrict: &rpc.TagMatchers{ 275 TagMatchers: []*rpc.TagMatcher{ 276 newRPCMatcher(rpc.MatcherType_NOTREGEXP, "foo", "bar"), 277 newRPCMatcher(rpc.MatcherType_EQUAL, "baz", "qux"), 278 }, 279 }, 280 Strip: [][]byte{ 281 []byte("foobar"), 282 }, 283 }, 284 }, 285 expected: &storage.RestrictQueryOptions{ 286 RestrictByType: &storage.RestrictByType{ 287 MetricsType: storagemetadata.AggregatedMetricsType, 288 StoragePolicy: policy.NewStoragePolicy(time.Minute, 289 xtime.Second, 24*time.Hour), 290 }, 291 RestrictByTag: &storage.RestrictByTag{ 292 Restrict: []models.Matcher{ 293 mustNewMatcher(models.MatchNotRegexp, "foo", "bar"), 294 mustNewMatcher(models.MatchEqual, "baz", "qux"), 295 }, 296 Strip: [][]byte{ 297 []byte("foobar"), 298 }, 299 }, 300 }, 301 }, 302 { 303 value: &rpcpb.RestrictQueryOptions{ 304 RestrictQueryType: &rpcpb.RestrictQueryType{ 305 MetricsType: rpcpb.MetricsType_UNKNOWN_METRICS_TYPE, 306 }, 307 }, 308 errContains: "unknown metrics type:", 309 }, 310 { 311 value: &rpcpb.RestrictQueryOptions{ 312 RestrictQueryType: &rpcpb.RestrictQueryType{ 313 MetricsType: rpcpb.MetricsType_UNAGGREGATED_METRICS_TYPE, 314 MetricsStoragePolicy: &policypb.StoragePolicy{ 315 Resolution: policypb.Resolution{ 316 WindowSize: int64(time.Minute), 317 Precision: int64(time.Second), 318 }, 319 Retention: policypb.Retention{ 320 Period: int64(24 * time.Hour), 321 }, 322 }, 323 }, 324 }, 325 errContains: "expected no storage policy for unaggregated metrics", 326 }, 327 { 328 value: &rpcpb.RestrictQueryOptions{ 329 RestrictQueryType: &rpcpb.RestrictQueryType{ 330 MetricsType: rpcpb.MetricsType_AGGREGATED_METRICS_TYPE, 331 MetricsStoragePolicy: &policypb.StoragePolicy{ 332 Resolution: policypb.Resolution{ 333 WindowSize: -1, 334 }, 335 }, 336 }, 337 }, 338 errContains: "unable to convert from duration to time unit", 339 }, 340 { 341 value: &rpcpb.RestrictQueryOptions{ 342 RestrictQueryType: &rpcpb.RestrictQueryType{ 343 MetricsType: rpcpb.MetricsType_AGGREGATED_METRICS_TYPE, 344 MetricsStoragePolicy: &policypb.StoragePolicy{ 345 Resolution: policypb.Resolution{ 346 WindowSize: int64(time.Minute), 347 Precision: int64(-1), 348 }, 349 }, 350 }, 351 }, 352 errContains: "unable to convert from duration to time unit", 353 }, 354 { 355 value: &rpcpb.RestrictQueryOptions{ 356 RestrictQueryType: &rpcpb.RestrictQueryType{ 357 MetricsType: rpcpb.MetricsType_AGGREGATED_METRICS_TYPE, 358 MetricsStoragePolicy: &policypb.StoragePolicy{ 359 Resolution: policypb.Resolution{ 360 WindowSize: int64(time.Minute), 361 Precision: int64(time.Second), 362 }, 363 Retention: policypb.Retention{ 364 Period: int64(-1), 365 }, 366 }, 367 }, 368 }, 369 errContains: "expected positive retention", 370 }, 371 } 372 for _, test := range tests { 373 t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) { 374 result, err := decodeRestrictQueryOptions(test.value) 375 if test.errContains == "" { 376 require.NoError(t, err) 377 assert.Equal(t, test.expected, result) 378 return 379 } 380 381 require.Error(t, err) 382 assert.True(t, 383 strings.Contains(err.Error(), test.errContains), 384 fmt.Sprintf("err=%v, want_contains=%v", err.Error(), test.errContains)) 385 }) 386 } 387 } 388 389 func mustNewMatcher(t models.MatchType, n, v string) models.Matcher { 390 matcher, err := models.NewMatcher(t, []byte(n), []byte(v)) 391 if err != nil { 392 panic(err) 393 } 394 395 return matcher 396 } 397 398 func newRPCMatcher(t rpc.MatcherType, n, v string) *rpc.TagMatcher { 399 return &rpc.TagMatcher{Name: []byte(n), Value: []byte(v), Type: t} 400 } 401 402 func TestRestrictQueryOptionsProto(t *testing.T) { 403 tests := []struct { 404 value storage.RestrictQueryOptions 405 expected *rpcpb.RestrictQueryOptions 406 errContains string 407 }{ 408 { 409 value: storage.RestrictQueryOptions{ 410 RestrictByType: &storage.RestrictByType{ 411 MetricsType: storagemetadata.UnaggregatedMetricsType, 412 }, 413 RestrictByTag: &storage.RestrictByTag{ 414 Restrict: []models.Matcher{ 415 mustNewMatcher(models.MatchNotRegexp, "foo", "bar"), 416 }, 417 Strip: [][]byte{[]byte("foobar")}, 418 }, 419 }, 420 expected: &rpcpb.RestrictQueryOptions{ 421 RestrictQueryType: &rpcpb.RestrictQueryType{ 422 MetricsType: rpcpb.MetricsType_UNAGGREGATED_METRICS_TYPE, 423 }, 424 RestrictQueryTags: &rpcpb.RestrictQueryTags{ 425 Restrict: &rpc.TagMatchers{ 426 TagMatchers: []*rpc.TagMatcher{ 427 newRPCMatcher(rpc.MatcherType_NOTREGEXP, "foo", "bar"), 428 }, 429 }, 430 Strip: [][]byte{[]byte("foobar")}, 431 }, 432 }, 433 }, 434 { 435 value: storage.RestrictQueryOptions{ 436 RestrictByType: &storage.RestrictByType{ 437 MetricsType: storagemetadata.AggregatedMetricsType, 438 StoragePolicy: policy.NewStoragePolicy(time.Minute, 439 xtime.Second, 24*time.Hour), 440 }, 441 RestrictByTag: &storage.RestrictByTag{ 442 Restrict: models.Matchers{ 443 mustNewMatcher(models.MatchNotRegexp, "foo", "bar"), 444 mustNewMatcher(models.MatchEqual, "baz", "qux"), 445 }, 446 Strip: [][]byte{[]byte("foobar")}, 447 }, 448 }, 449 expected: &rpcpb.RestrictQueryOptions{ 450 RestrictQueryType: &rpcpb.RestrictQueryType{ 451 MetricsType: rpcpb.MetricsType_AGGREGATED_METRICS_TYPE, 452 MetricsStoragePolicy: &policypb.StoragePolicy{ 453 Resolution: policypb.Resolution{ 454 WindowSize: int64(time.Minute), 455 Precision: int64(time.Second), 456 }, 457 Retention: policypb.Retention{ 458 Period: int64(24 * time.Hour), 459 }, 460 }, 461 }, 462 RestrictQueryTags: &rpcpb.RestrictQueryTags{ 463 Restrict: &rpc.TagMatchers{ 464 TagMatchers: []*rpc.TagMatcher{ 465 newRPCMatcher(rpc.MatcherType_NOTREGEXP, "foo", "bar"), 466 newRPCMatcher(rpc.MatcherType_EQUAL, "baz", "qux"), 467 }, 468 }, 469 Strip: [][]byte{[]byte("foobar")}, 470 }, 471 }, 472 }, 473 { 474 value: storage.RestrictQueryOptions{ 475 RestrictByType: &storage.RestrictByType{ 476 MetricsType: storagemetadata.MetricsType(uint(math.MaxUint16)), 477 }, 478 }, 479 errContains: "unknown metrics type:", 480 }, 481 { 482 value: storage.RestrictQueryOptions{ 483 RestrictByType: &storage.RestrictByType{ 484 MetricsType: storagemetadata.UnaggregatedMetricsType, 485 StoragePolicy: policy.NewStoragePolicy(time.Minute, 486 xtime.Second, 24*time.Hour), 487 }, 488 }, 489 errContains: "expected no storage policy for unaggregated metrics", 490 }, 491 } 492 for _, test := range tests { 493 t.Run(fmt.Sprintf("%+v", test.value), func(t *testing.T) { 494 result, err := encodeRestrictQueryOptions(&test.value) 495 if test.errContains == "" { 496 require.NoError(t, err) 497 require.Equal(t, test.expected, result) 498 return 499 } 500 501 require.Error(t, err) 502 assert.True(t, strings.Contains(err.Error(), test.errContains)) 503 }) 504 } 505 }