github.com/thanos-io/thanos@v0.32.5/internal/cortex/querier/queryrange/split_by_interval_test.go (about)

     1  // Copyright (c) The Cortex Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package queryrange
     5  
     6  import (
     7  	"context"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"net/url"
    12  	"strconv"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/weaveworks/common/httpgrpc"
    17  
    18  	"github.com/prometheus/prometheus/promql/parser"
    19  	"github.com/stretchr/testify/require"
    20  	"github.com/weaveworks/common/middleware"
    21  	"github.com/weaveworks/common/user"
    22  	"go.uber.org/atomic"
    23  )
    24  
    25  const seconds = 1e3 // 1e3 milliseconds per second.
    26  
    27  func TestNextIntervalBoundary(t *testing.T) {
    28  	for i, tc := range []struct {
    29  		in, step, out int64
    30  		interval      time.Duration
    31  	}{
    32  		// Smallest possible period is 1 millisecond
    33  		{0, 1, toMs(day) - 1, day},
    34  		{0, 1, toMs(time.Hour) - 1, time.Hour},
    35  		// A more standard example
    36  		{0, 15 * seconds, toMs(day) - 15*seconds, day},
    37  		{0, 15 * seconds, toMs(time.Hour) - 15*seconds, time.Hour},
    38  		// Move start time forward 1 second; end time moves the same
    39  		{1 * seconds, 15 * seconds, toMs(day) - (15-1)*seconds, day},
    40  		{1 * seconds, 15 * seconds, toMs(time.Hour) - (15-1)*seconds, time.Hour},
    41  		// Move start time forward 14 seconds; end time moves the same
    42  		{14 * seconds, 15 * seconds, toMs(day) - (15-14)*seconds, day},
    43  		{14 * seconds, 15 * seconds, toMs(time.Hour) - (15-14)*seconds, time.Hour},
    44  		// Now some examples where the period does not divide evenly into a day:
    45  		// 1 day modulus 35 seconds = 20 seconds
    46  		{0, 35 * seconds, toMs(day) - 20*seconds, day},
    47  		// 1 hour modulus 35 sec = 30  (3600 mod 35 = 30)
    48  		{0, 35 * seconds, toMs(time.Hour) - 30*seconds, time.Hour},
    49  		// Move start time forward 1 second; end time moves the same
    50  		{1 * seconds, 35 * seconds, toMs(day) - (20-1)*seconds, day},
    51  		{1 * seconds, 35 * seconds, toMs(time.Hour) - (30-1)*seconds, time.Hour},
    52  		// If the end time lands exactly on midnight we stop one period before that
    53  		{20 * seconds, 35 * seconds, toMs(day) - 35*seconds, day},
    54  		{30 * seconds, 35 * seconds, toMs(time.Hour) - 35*seconds, time.Hour},
    55  		// This example starts 35 seconds after the 5th one ends
    56  		{toMs(day) + 15*seconds, 35 * seconds, 2*toMs(day) - 5*seconds, day},
    57  		{toMs(time.Hour) + 15*seconds, 35 * seconds, 2*toMs(time.Hour) - 15*seconds, time.Hour},
    58  	} {
    59  		t.Run(strconv.Itoa(i), func(t *testing.T) {
    60  			require.Equal(t, tc.out, nextIntervalBoundary(tc.in, tc.step, tc.interval))
    61  		})
    62  	}
    63  }
    64  
    65  func TestSplitQuery(t *testing.T) {
    66  	for i, tc := range []struct {
    67  		input    Request
    68  		expected []Request
    69  		interval time.Duration
    70  	}{
    71  		{
    72  			input: &PrometheusRequest{
    73  				Start: 0,
    74  				End:   60 * 60 * seconds,
    75  				Step:  15 * seconds,
    76  				Query: "foo",
    77  			},
    78  			expected: []Request{
    79  				&PrometheusRequest{
    80  					Start: 0,
    81  					End:   60 * 60 * seconds,
    82  					Step:  15 * seconds,
    83  					Query: "foo",
    84  				},
    85  			},
    86  			interval: day,
    87  		},
    88  		{
    89  			input: &PrometheusRequest{
    90  				Start: 60 * 60 * seconds,
    91  				End:   60 * 60 * seconds,
    92  				Step:  15 * seconds,
    93  				Query: "foo",
    94  			},
    95  			expected: []Request{
    96  				&PrometheusRequest{
    97  					Start: 60 * 60 * seconds,
    98  					End:   60 * 60 * seconds,
    99  					Step:  15 * seconds,
   100  					Query: "foo",
   101  				},
   102  			},
   103  			interval: day,
   104  		},
   105  		{
   106  			input: &PrometheusRequest{
   107  				Start: 0,
   108  				End:   60 * 60 * seconds,
   109  				Step:  15 * seconds,
   110  				Query: "foo",
   111  			},
   112  			expected: []Request{
   113  				&PrometheusRequest{
   114  					Start: 0,
   115  					End:   60 * 60 * seconds,
   116  					Step:  15 * seconds,
   117  					Query: "foo",
   118  				},
   119  			},
   120  			interval: 3 * time.Hour,
   121  		},
   122  		{
   123  			input: &PrometheusRequest{
   124  				Start: 0,
   125  				End:   24 * 3600 * seconds,
   126  				Step:  15 * seconds,
   127  				Query: "foo",
   128  			},
   129  			expected: []Request{
   130  				&PrometheusRequest{
   131  					Start: 0,
   132  					End:   24 * 3600 * seconds,
   133  					Step:  15 * seconds,
   134  					Query: "foo",
   135  				},
   136  			},
   137  			interval: day,
   138  		},
   139  		{
   140  			input: &PrometheusRequest{
   141  				Start: 0,
   142  				End:   3 * 3600 * seconds,
   143  				Step:  15 * seconds,
   144  				Query: "foo",
   145  			},
   146  			expected: []Request{
   147  				&PrometheusRequest{
   148  					Start: 0,
   149  					End:   3 * 3600 * seconds,
   150  					Step:  15 * seconds,
   151  					Query: "foo",
   152  				},
   153  			},
   154  			interval: 3 * time.Hour,
   155  		},
   156  		{
   157  			input: &PrometheusRequest{
   158  				Start: 0,
   159  				End:   2 * 24 * 3600 * seconds,
   160  				Step:  15 * seconds,
   161  				Query: "foo @ start()",
   162  			},
   163  			expected: []Request{
   164  				&PrometheusRequest{
   165  					Start: 0,
   166  					End:   (24 * 3600 * seconds) - (15 * seconds),
   167  					Step:  15 * seconds,
   168  					Query: "foo @ 0.000",
   169  				},
   170  				&PrometheusRequest{
   171  					Start: 24 * 3600 * seconds,
   172  					End:   2 * 24 * 3600 * seconds,
   173  					Step:  15 * seconds,
   174  					Query: "foo @ 0.000",
   175  				},
   176  			},
   177  			interval: day,
   178  		},
   179  		{
   180  			input: &PrometheusRequest{
   181  				Start: 0,
   182  				End:   2 * 3 * 3600 * seconds,
   183  				Step:  15 * seconds,
   184  				Query: "foo",
   185  			},
   186  			expected: []Request{
   187  				&PrometheusRequest{
   188  					Start: 0,
   189  					End:   (3 * 3600 * seconds) - (15 * seconds),
   190  					Step:  15 * seconds,
   191  					Query: "foo",
   192  				},
   193  				&PrometheusRequest{
   194  					Start: 3 * 3600 * seconds,
   195  					End:   2 * 3 * 3600 * seconds,
   196  					Step:  15 * seconds,
   197  					Query: "foo",
   198  				},
   199  			},
   200  			interval: 3 * time.Hour,
   201  		},
   202  		{
   203  			input: &PrometheusRequest{
   204  				Start: 3 * 3600 * seconds,
   205  				End:   3 * 24 * 3600 * seconds,
   206  				Step:  15 * seconds,
   207  				Query: "foo",
   208  			},
   209  			expected: []Request{
   210  				&PrometheusRequest{
   211  					Start: 3 * 3600 * seconds,
   212  					End:   (24 * 3600 * seconds) - (15 * seconds),
   213  					Step:  15 * seconds,
   214  					Query: "foo",
   215  				},
   216  				&PrometheusRequest{
   217  					Start: 24 * 3600 * seconds,
   218  					End:   (2 * 24 * 3600 * seconds) - (15 * seconds),
   219  					Step:  15 * seconds,
   220  					Query: "foo",
   221  				},
   222  				&PrometheusRequest{
   223  					Start: 2 * 24 * 3600 * seconds,
   224  					End:   3 * 24 * 3600 * seconds,
   225  					Step:  15 * seconds,
   226  					Query: "foo",
   227  				},
   228  			},
   229  			interval: day,
   230  		},
   231  		{
   232  			input: &PrometheusRequest{
   233  				Start: 2 * 3600 * seconds,
   234  				End:   3 * 3 * 3600 * seconds,
   235  				Step:  15 * seconds,
   236  				Query: "foo",
   237  			},
   238  			expected: []Request{
   239  				&PrometheusRequest{
   240  					Start: 2 * 3600 * seconds,
   241  					End:   (3 * 3600 * seconds) - (15 * seconds),
   242  					Step:  15 * seconds,
   243  					Query: "foo",
   244  				},
   245  				&PrometheusRequest{
   246  					Start: 3 * 3600 * seconds,
   247  					End:   (2 * 3 * 3600 * seconds) - (15 * seconds),
   248  					Step:  15 * seconds,
   249  					Query: "foo",
   250  				},
   251  				&PrometheusRequest{
   252  					Start: 2 * 3 * 3600 * seconds,
   253  					End:   3 * 3 * 3600 * seconds,
   254  					Step:  15 * seconds,
   255  					Query: "foo",
   256  				},
   257  			},
   258  			interval: 3 * time.Hour,
   259  		},
   260  	} {
   261  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   262  			days, err := splitQuery(tc.input, tc.interval)
   263  			require.NoError(t, err)
   264  			require.Equal(t, tc.expected, days)
   265  		})
   266  	}
   267  }
   268  
   269  func TestSplitByDay(t *testing.T) {
   270  	mergedResponse, err := PrometheusCodec.MergeResponse(nil, parsedResponse, parsedResponse)
   271  	require.NoError(t, err)
   272  
   273  	mergedHTTPResponse, err := PrometheusCodec.EncodeResponse(context.Background(), mergedResponse)
   274  	require.NoError(t, err)
   275  
   276  	mergedHTTPResponseBody, err := ioutil.ReadAll(mergedHTTPResponse.Body)
   277  	require.NoError(t, err)
   278  
   279  	for i, tc := range []struct {
   280  		path, expectedBody string
   281  		expectedQueryCount int32
   282  	}{
   283  		{query, string(mergedHTTPResponseBody), 2},
   284  	} {
   285  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   286  			var actualCount atomic.Int32
   287  			s := httptest.NewServer(
   288  				middleware.AuthenticateUser.Wrap(
   289  					http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   290  						actualCount.Inc()
   291  						_, _ = w.Write([]byte(responseBody))
   292  					}),
   293  				),
   294  			)
   295  			defer s.Close()
   296  
   297  			u, err := url.Parse(s.URL)
   298  			require.NoError(t, err)
   299  
   300  			interval := func(_ Request) time.Duration { return 24 * time.Hour }
   301  			roundtripper := NewRoundTripper(singleHostRoundTripper{
   302  				host: u.Host,
   303  				next: http.DefaultTransport,
   304  			}, PrometheusCodec, nil, NewLimitsMiddleware(mockLimits{}), SplitByIntervalMiddleware(interval, mockLimits{}, PrometheusCodec, nil))
   305  
   306  			req, err := http.NewRequest("GET", tc.path, http.NoBody)
   307  			require.NoError(t, err)
   308  
   309  			ctx := user.InjectOrgID(context.Background(), "1")
   310  			req = req.WithContext(ctx)
   311  
   312  			resp, err := roundtripper.RoundTrip(req)
   313  			require.NoError(t, err)
   314  			require.Equal(t, 200, resp.StatusCode)
   315  
   316  			bs, err := ioutil.ReadAll(resp.Body)
   317  			require.NoError(t, err)
   318  			require.Equal(t, tc.expectedBody, string(bs))
   319  			require.Equal(t, tc.expectedQueryCount, actualCount.Load())
   320  		})
   321  	}
   322  }
   323  
   324  func Test_evaluateAtModifier(t *testing.T) {
   325  	const (
   326  		start, end = int64(1546300800), int64(1646300800)
   327  	)
   328  	for _, tt := range []struct {
   329  		in, expected      string
   330  		expectedErrorCode int
   331  	}{
   332  		{
   333  			in:       "topk(5, rate(http_requests_total[1h] @ start()))",
   334  			expected: "topk(5, rate(http_requests_total[1h] @ 1546300.800))",
   335  		},
   336  		{
   337  			in:       "topk(5, rate(http_requests_total[1h] @ 0))",
   338  			expected: "topk(5, rate(http_requests_total[1h] @ 0.000))",
   339  		},
   340  		{
   341  			in:       "http_requests_total[1h] @ 10.001",
   342  			expected: "http_requests_total[1h] @ 10.001",
   343  		},
   344  		{
   345  			in: `min_over_time(
   346  				sum by(cluster) (
   347  					rate(http_requests_total[5m] @ end())
   348  				)[10m:]
   349  			)
   350  			or
   351  			max_over_time(
   352  				stddev_over_time(
   353  					deriv(
   354  						rate(http_requests_total[10m] @ start())
   355  					[5m:1m])
   356  				[2m:])
   357  			[10m:])`,
   358  			expected: `min_over_time(
   359  				sum by(cluster) (
   360  					rate(http_requests_total[5m] @ 1646300.800)
   361  				)[10m:]
   362  			)
   363  			or
   364  			max_over_time(
   365  				stddev_over_time(
   366  					deriv(
   367  						rate(http_requests_total[10m] @ 1546300.800)
   368  					[5m:1m])
   369  				[2m:])
   370  			[10m:])`,
   371  		},
   372  		{
   373  			// parse error: missing unit character in duration
   374  			in:                "http_requests_total[5] @ 10.001",
   375  			expectedErrorCode: http.StatusBadRequest,
   376  		},
   377  		{
   378  			// parse error: @ modifier must be preceded by an instant vector selector or range vector selector or a subquery
   379  			in:                "sum(http_requests_total[5m]) @ 10.001",
   380  			expectedErrorCode: http.StatusBadRequest,
   381  		},
   382  	} {
   383  		tt := tt
   384  		t.Run(tt.in, func(t *testing.T) {
   385  			t.Parallel()
   386  			out, err := EvaluateAtModifierFunction(tt.in, start, end)
   387  			if tt.expectedErrorCode != 0 {
   388  				require.Error(t, err)
   389  				httpResp, ok := httpgrpc.HTTPResponseFromError(err)
   390  				require.True(t, ok, "returned error is not an httpgrpc response")
   391  				require.Equal(t, tt.expectedErrorCode, int(httpResp.Code))
   392  			} else {
   393  				require.NoError(t, err)
   394  				expectedExpr, err := parser.ParseExpr(tt.expected)
   395  				require.NoError(t, err)
   396  				require.Equal(t, expectedExpr.String(), out)
   397  			}
   398  		})
   399  	}
   400  }