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  }