github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/prometheus/native/middleware_test.go (about)

     1  // Copyright (c) 2021 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 native
    22  
    23  import (
    24  	"context"
    25  	"errors"
    26  	"net/http"
    27  	"net/http/httptest"
    28  	"net/url"
    29  	"strings"
    30  	"testing"
    31  	"time"
    32  
    33  	"github.com/gorilla/mux"
    34  	"github.com/jonboulle/clockwork"
    35  	"github.com/stretchr/testify/require"
    36  	"go.uber.org/zap"
    37  	"go.uber.org/zap/zapcore"
    38  	"go.uber.org/zap/zaptest/observer"
    39  
    40  	"github.com/m3db/m3/src/query/api/v1/middleware"
    41  	"github.com/m3db/m3/src/x/headers"
    42  	"github.com/m3db/m3/src/x/instrument"
    43  	xhttp "github.com/m3db/m3/src/x/net/http"
    44  )
    45  
    46  func TestQueryResponse(t *testing.T) {
    47  	now := time.Now().UTC().Round(0)
    48  	startTime := now
    49  	endTime := startTime.Add(time.Hour)
    50  	cases := []struct {
    51  		name            string
    52  		code            int
    53  		duration        time.Duration
    54  		threshold       time.Duration
    55  		disabled        bool
    56  		err             error
    57  		form            map[string]string
    58  		requestHeaders  map[string]string
    59  		responseHeaders map[string]string
    60  		fields          map[string]interface{}
    61  	}{
    62  		{
    63  			name:      "happy path",
    64  			code:      200,
    65  			duration:  time.Second,
    66  			threshold: time.Microsecond,
    67  			form: map[string]string{
    68  				"query": "fooquery",
    69  				"start": startTime.Format(time.RFC3339Nano),
    70  				"end":   endTime.Format(time.RFC3339Nano),
    71  				"extra": "foobar",
    72  			},
    73  			requestHeaders: map[string]string{
    74  				headers.LimitHeader:        "10",
    75  				headers.LimitMaxDocsHeader: "100",
    76  				"foo":                      "bar",
    77  			},
    78  			responseHeaders: map[string]string{
    79  				headers.TimeoutHeader:                 "10",
    80  				headers.ReturnedMetadataLimitedHeader: "100",
    81  				"foo":                                 "bar",
    82  			},
    83  			fields: map[string]interface{}{
    84  				"query":                               "fooquery",
    85  				"start":                               startTime,
    86  				"end":                                 endTime,
    87  				"status":                              int64(200),
    88  				"queryRange":                          time.Hour,
    89  				headers.LimitHeader:                   "10",
    90  				headers.LimitMaxDocsHeader:            "100",
    91  				headers.TimeoutHeader:                 "10",
    92  				headers.ReturnedMetadataLimitedHeader: "100",
    93  			},
    94  		},
    95  		{
    96  			name:      "no request data",
    97  			code:      504,
    98  			duration:  time.Second,
    99  			threshold: time.Microsecond,
   100  			fields: map[string]interface{}{
   101  				"query":      "",
   102  				"start":      time.Time{},
   103  				"end":        time.Time{},
   104  				"status":     int64(504),
   105  				"queryRange": time.Duration(0),
   106  			},
   107  		},
   108  		{
   109  			name: "instant",
   110  			form: map[string]string{
   111  				"query": "fooquery",
   112  				"start": "now",
   113  				"end":   "now",
   114  			},
   115  			code:      200,
   116  			duration:  time.Second,
   117  			threshold: time.Microsecond,
   118  			fields: map[string]interface{}{
   119  				"query":      "fooquery",
   120  				"start":      now,
   121  				"end":        now,
   122  				"status":     int64(200),
   123  				"queryRange": time.Duration(0),
   124  			},
   125  		},
   126  		{
   127  			name: "error",
   128  			form: map[string]string{
   129  				"query": "fooquery",
   130  				"start": "now",
   131  				"end":   "now",
   132  			},
   133  			code:      500,
   134  			err:       errors.New("boom"),
   135  			duration:  time.Second,
   136  			threshold: time.Microsecond,
   137  			fields: map[string]interface{}{
   138  				"query":      "fooquery",
   139  				"start":      now,
   140  				"end":        now,
   141  				"status":     int64(500),
   142  				"queryRange": time.Duration(0),
   143  				"error":      "boom",
   144  			},
   145  		},
   146  		{
   147  			name: "below threshold",
   148  			form: map[string]string{
   149  				"query": "fooquery",
   150  				"start": "now",
   151  				"end":   "now",
   152  			},
   153  			code:      200,
   154  			duration:  time.Millisecond,
   155  			threshold: time.Second,
   156  		},
   157  		{
   158  			name: "disabled",
   159  			form: map[string]string{
   160  				"query": "fooquery",
   161  				"start": "now",
   162  				"end":   "now",
   163  			},
   164  			code:      200,
   165  			disabled:  true,
   166  			duration:  time.Second,
   167  			threshold: time.Millisecond,
   168  		},
   169  	}
   170  
   171  	for _, tc := range cases {
   172  		tc := tc
   173  		t.Run(tc.name, func(t *testing.T) {
   174  			core, recorded := observer.New(zapcore.InfoLevel)
   175  			iOpts := instrument.NewOptions().SetLogger(zap.New(core))
   176  			clock := clockwork.NewFakeClockAt(now)
   177  			opts := WithQueryParams(middleware.Options{
   178  				InstrumentOpts: iOpts,
   179  				Clock:          clock,
   180  				Metrics:        middleware.MetricsOptions{},
   181  				Logging: middleware.LoggingOptions{
   182  					Threshold: tc.threshold,
   183  					Disabled:  tc.disabled,
   184  				},
   185  			})
   186  			h := middleware.ResponseLogging(opts).Middleware(
   187  				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   188  					clock.Advance(tc.duration)
   189  					if tc.err != nil {
   190  						xhttp.WriteError(w, tc.err)
   191  						return
   192  					}
   193  					for k, v := range tc.responseHeaders {
   194  						w.Header().Set(k, v)
   195  					}
   196  					w.WriteHeader(tc.code)
   197  				}))
   198  			r := mux.NewRouter()
   199  			r.Handle("/testRoute", h)
   200  			server := httptest.NewServer(r)
   201  			defer server.Close()
   202  
   203  			values := url.Values{}
   204  			for k, v := range tc.form {
   205  				values.Add(k, v)
   206  			}
   207  			req, err := http.NewRequestWithContext(context.Background(), "POST", server.URL+"/testRoute",
   208  				strings.NewReader(values.Encode()))
   209  			for k, v := range tc.requestHeaders {
   210  				req.Header.Set(k, v)
   211  			}
   212  			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   213  			require.NoError(t, err)
   214  			resp, err := server.Client().Do(req)
   215  			require.NoError(t, err)
   216  			require.NoError(t, resp.Body.Close())
   217  			require.Equal(t, tc.code, resp.StatusCode)
   218  			msgs := recorded.FilterMessage("finished handling request").All()
   219  			if len(tc.fields) == 0 {
   220  				require.Len(t, msgs, 0)
   221  			} else {
   222  				require.Len(t, msgs, 1)
   223  				fields := msgs[0].ContextMap()
   224  				require.Equal(t, "/testRoute", fields["url"])
   225  				require.True(t, fields["duration"].(time.Duration) >= tc.duration)
   226  
   227  				for k, v := range tc.fields {
   228  					require.Equal(t, v, fields[k], "log field %v", k)
   229  				}
   230  				require.Len(t, fields, len(tc.fields)+2)
   231  			}
   232  		})
   233  	}
   234  }