github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/api/v1/middleware/logging_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 middleware
    22  
    23  import (
    24  	"context"
    25  	"io/ioutil"
    26  	"net/http"
    27  	"os"
    28  	"strings"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/m3db/m3/src/query/util/logging"
    33  	"github.com/m3db/m3/src/x/instrument"
    34  
    35  	"github.com/jonboulle/clockwork"
    36  	"github.com/stretchr/testify/assert"
    37  	"github.com/stretchr/testify/require"
    38  	"go.uber.org/zap"
    39  	"go.uber.org/zap/zapcore"
    40  )
    41  
    42  type httpWriter struct {
    43  	written []string
    44  	status  int
    45  }
    46  
    47  func (w *httpWriter) Header() http.Header {
    48  	return make(http.Header)
    49  }
    50  
    51  func (w *httpWriter) WriteHeader(statusCode int) {
    52  	w.status = statusCode
    53  }
    54  
    55  func (w *httpWriter) Write(b []byte) (int, error) {
    56  	w.written = append(w.written, string(b))
    57  	return len(b), nil
    58  }
    59  
    60  type panicHandler struct{}
    61  
    62  func (h panicHandler) ServeHTTP(_ http.ResponseWriter, _ *http.Request) {
    63  	panic("beef")
    64  }
    65  
    66  type cleanupFn func()
    67  
    68  func setup(t *testing.T, ctxLogger bool) (
    69  	*os.File,
    70  	*os.File,
    71  	*http.Request,
    72  	instrument.Options,
    73  	cleanupFn,
    74  ) {
    75  	success := false
    76  
    77  	stdout, err := ioutil.TempFile("", "temp-log-file-out")
    78  	require.NoError(t, err, "Couldn't create a temporary out file for test.")
    79  	defer func() {
    80  		if success {
    81  			return
    82  		}
    83  		_ = os.Remove(stdout.Name())
    84  	}()
    85  
    86  	stderr, err := ioutil.TempFile("", "temp-log-file-err")
    87  	require.NoError(t, err, "Couldn't create a temporary err file for test.")
    88  	defer func() {
    89  		if success {
    90  			return
    91  		}
    92  		_ = os.Remove(stderr.Name())
    93  	}()
    94  
    95  	cfg := zap.NewDevelopmentConfig()
    96  	cfg.OutputPaths = []string{stdout.Name()}
    97  	cfg.ErrorOutputPaths = []string{stderr.Name()}
    98  
    99  	templogger, err := cfg.Build()
   100  	require.NoError(t, err)
   101  
   102  	instrumentOpts := instrument.NewOptions().
   103  		SetLogger(templogger)
   104  
   105  	req, err := http.NewRequest("GET", "cool", nil)
   106  	require.NoError(t, err)
   107  
   108  	if ctxLogger {
   109  		// Only stacktrace `error` priority and higher
   110  		highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
   111  			return lvl >= zapcore.ErrorLevel
   112  		})
   113  
   114  		templogger = templogger.WithOptions(zap.AddStacktrace(highPriority)).With(zap.String("foo", "bar"))
   115  		req = req.WithContext(logging.NewContextWithLogger(context.TODO(), templogger))
   116  	}
   117  
   118  	// Success to true to avoid removing log files
   119  	success = true
   120  	cleanup := func() {
   121  		_ = os.Remove(stdout.Name())
   122  		_ = os.Remove(stderr.Name())
   123  	}
   124  	return stdout, stderr, req, instrumentOpts, cleanup
   125  }
   126  
   127  func TestPanicErrorResponder(t *testing.T) {
   128  	stdout, stderr, req, instrumentOpts, cleanup := setup(t, true)
   129  	defer cleanup()
   130  
   131  	writer := &httpWriter{written: make([]string, 0, 10)}
   132  	deadbeef := Panic(instrumentOpts).Middleware(panicHandler{})
   133  	deadbeef.ServeHTTP(writer, req)
   134  
   135  	assert.Equal(t, 500, writer.status)
   136  	require.Equal(t, 1, len(writer.written))
   137  	assert.JSONEq(t, `{"status":"error","error":"caught panic: beef"}`, writer.written[0])
   138  
   139  	assertNoErrorLogs(t, stderr)
   140  	b, err := ioutil.ReadAll(stdout)
   141  	require.NoError(t, err)
   142  	outstrs := strings.Split(string(b), "\n")
   143  
   144  	assert.True(t, strings.Contains(outstrs[0], "ERROR"))
   145  	assert.True(t, strings.Contains(outstrs[0], "panic captured"))
   146  	assert.True(t, strings.Contains(outstrs[0], `"stack": "beef"`))
   147  	assert.True(t, strings.Contains(outstrs[0], `"foo": "bar"`))
   148  
   149  	// Assert that stack trace is written
   150  	count := 0
   151  	for _, s := range outstrs {
   152  		if strings.Contains(s, "runtime.gopanic") {
   153  			count++
   154  		}
   155  	}
   156  
   157  	assert.True(t, count > 0)
   158  }
   159  
   160  func assertNoErrorLogs(t *testing.T, stderr *os.File) {
   161  	b, err := ioutil.ReadAll(stderr)
   162  	require.NoError(t, err)
   163  	assert.Equal(t, 0, len(b))
   164  }
   165  
   166  func assertPanicLogsWritten(t *testing.T, stdout, stderr *os.File) {
   167  	assertNoErrorLogs(t, stderr)
   168  	b, err := ioutil.ReadAll(stdout)
   169  	require.NoError(t, err)
   170  	outstrs := strings.Split(string(b), "\n")
   171  
   172  	assert.True(t, strings.Contains(outstrs[0], "ERROR"))
   173  	assert.True(t, strings.Contains(outstrs[0], "panic captured"))
   174  	assert.True(t, strings.Contains(outstrs[0], `"stack": "err"`))
   175  	assert.True(t, strings.Contains(outstrs[0], `"foo": "bar"`))
   176  
   177  	// Assert that stack trace is written
   178  	count := 0
   179  	for _, s := range outstrs {
   180  		if strings.Contains(s, "runtime.gopanic") {
   181  			count++
   182  		}
   183  	}
   184  	assert.True(t, count > 0)
   185  
   186  	count = 0
   187  	for _, s := range outstrs {
   188  		if strings.Contains(s, "cannot write error for request; already written\t{\"foo\": \"bar\"}") {
   189  			count++
   190  		}
   191  	}
   192  	assert.True(t, count > 0)
   193  }
   194  
   195  func TestPanicErrorResponderOnlyIfNotWrittenRequest(t *testing.T) {
   196  	stdout, stderr, req, instrumentOpts, cleanup := setup(t, true)
   197  	defer cleanup()
   198  
   199  	writer := &httpWriter{written: make([]string, 0, 10)}
   200  	deadbeef := Panic(instrumentOpts).Middleware(
   201  		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   202  			_, _ = w.Write([]byte("foo"))
   203  			panic("err")
   204  		}))
   205  	deadbeef.ServeHTTP(writer, req)
   206  
   207  	assert.Equal(t, 0, writer.status)
   208  	require.Equal(t, 1, len(writer.written))
   209  	assert.Equal(t, "foo", writer.written[0])
   210  
   211  	// Verify that panic capture is still logged.
   212  	assertPanicLogsWritten(t, stdout, stderr)
   213  }
   214  
   215  func TestPanicErrorResponderOnlyIfNotWrittenHeader(t *testing.T) {
   216  	stdout, stderr, req, instrumentOpts, cleanup := setup(t, true)
   217  	defer cleanup()
   218  
   219  	writer := &httpWriter{written: make([]string, 0, 10)}
   220  	deadbeef := Panic(instrumentOpts).Middleware(
   221  		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   222  			w.WriteHeader(404)
   223  			panic("err")
   224  		}))
   225  	deadbeef.ServeHTTP(writer, req)
   226  
   227  	assert.Equal(t, 404, writer.status)
   228  	require.Equal(t, 0, len(writer.written))
   229  
   230  	// Verify that panic capture is still logged.
   231  	assertPanicLogsWritten(t, stdout, stderr)
   232  }
   233  
   234  type delayHandler struct {
   235  	delay time.Duration
   236  }
   237  
   238  func (h delayHandler) ServeHTTP(_ http.ResponseWriter, _ *http.Request) {
   239  	time.Sleep(h.delay)
   240  }
   241  
   242  func TestWithResponseTimeLogging(t *testing.T) {
   243  	stdout, stderr, req, instrumentOpts, cleanup := setup(t, false)
   244  	defer cleanup()
   245  
   246  	m := ResponseLogging(Options{
   247  		Clock:          clockwork.NewRealClock(),
   248  		InstrumentOpts: instrumentOpts,
   249  		Logging: LoggingOptions{
   250  			Threshold: time.Second,
   251  		},
   252  	})
   253  	slowHandler := m.Middleware(&delayHandler{delay: time.Second})
   254  	fastHandler := m.Middleware(&delayHandler{delay: time.Duration(0)})
   255  
   256  	writer := &httpWriter{written: make([]string, 0, 10)}
   257  
   258  	slowHandler.ServeHTTP(writer, req)
   259  	fastHandler.ServeHTTP(writer, req)
   260  
   261  	require.Equal(t, 0, len(writer.written))
   262  	assertNoErrorLogs(t, stderr)
   263  
   264  	b, err := ioutil.ReadAll(stdout)
   265  	require.NoError(t, err)
   266  	outstrs := strings.Split(string(b), "\n")
   267  
   268  	require.Equal(t, 2, len(outstrs))
   269  	out := outstrs[0]
   270  
   271  	assert.True(t, strings.Contains(out, "INFO"))
   272  	assert.True(t, strings.Contains(out, "finished handling request"))
   273  	assert.True(t, strings.Contains(out, `"url": "cool"`))
   274  	assert.True(t, strings.Contains(out, `duration": "1.`))
   275  }
   276  
   277  func TestWithResponseTimeAndPanicErrorLoggingFunc(t *testing.T) {
   278  	stdout, stderr, req, instrumentOpts, cleanup := setup(t, true)
   279  	defer cleanup()
   280  
   281  	writer := &httpWriter{written: make([]string, 0, 10)}
   282  	slowPanic := ResponseLogging(Options{
   283  		Clock:          clockwork.NewRealClock(),
   284  		InstrumentOpts: instrumentOpts,
   285  		Logging: LoggingOptions{
   286  			Threshold: time.Second,
   287  		},
   288  	}).Middleware(
   289  		Panic(instrumentOpts).Middleware(
   290  			http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   291  				time.Sleep(time.Second)
   292  				panic("err")
   293  			})))
   294  
   295  	slowPanic.ServeHTTP(writer, req)
   296  
   297  	assert.Equal(t, 500, writer.status)
   298  	require.Equal(t, 1, len(writer.written))
   299  	assert.JSONEq(t, `{"status":"error","error":"caught panic: err"}`, writer.written[0])
   300  
   301  	assertNoErrorLogs(t, stderr)
   302  
   303  	b, err := ioutil.ReadAll(stdout)
   304  	require.NoError(t, err)
   305  	outstrs := strings.Split(string(b), "\n")
   306  	assert.True(t, strings.Contains(outstrs[0], "ERROR"))
   307  	assert.True(t, strings.Contains(outstrs[0], "panic captured"))
   308  	assert.True(t, strings.Contains(outstrs[0], `"stack": "err"`))
   309  	assert.True(t, strings.Contains(outstrs[0], `"foo": "bar"`))
   310  
   311  	// Assert that stack trace is written
   312  	count := 0
   313  	for _, s := range outstrs {
   314  		if strings.Contains(s, "runtime.gopanic") {
   315  			count++
   316  		}
   317  	}
   318  
   319  	assert.True(t, count > 0)
   320  
   321  	// assert that the second last line of the log captures response time.
   322  	last := outstrs[len(outstrs)-2]
   323  
   324  	assert.True(t, strings.Contains(last, "INFO"))
   325  	assert.True(t, strings.Contains(last, "finished handling request"))
   326  	assert.True(t, strings.Contains(last, `"url": "cool"`))
   327  	assert.True(t, strings.Contains(last, `duration": "1.`))
   328  }
   329  
   330  func TestFields_Append(t *testing.T) {
   331  	cases := []struct {
   332  		name     string
   333  		fields   []zap.Field
   334  		other    []zap.Field
   335  		expected []zap.Field
   336  	}{
   337  		{
   338  			name: "both",
   339  			fields: []zap.Field{
   340  				zap.String("foo", "foos"),
   341  				zap.String("bar", "bars"),
   342  			},
   343  			other: []zap.Field{
   344  				zap.String("foo2", "foos2"),
   345  				zap.String("bar2", "bars2"),
   346  			},
   347  			expected: []zap.Field{
   348  				zap.String("foo", "foos"),
   349  				zap.String("bar", "bars"),
   350  				zap.String("foo2", "foos2"),
   351  				zap.String("bar2", "bars2"),
   352  			},
   353  		},
   354  		{
   355  			name:     "both nil",
   356  			expected: nil,
   357  		},
   358  		{
   359  			name: "fields nil",
   360  			other: []zap.Field{
   361  				zap.String("foo2", "foos2"),
   362  				zap.String("bar2", "bars2"),
   363  			},
   364  			expected: []zap.Field{
   365  				zap.String("foo2", "foos2"),
   366  				zap.String("bar2", "bars2"),
   367  			},
   368  		},
   369  		{
   370  			name: "other nil",
   371  			fields: []zap.Field{
   372  				zap.String("foo", "foos"),
   373  				zap.String("bar", "bars"),
   374  			},
   375  			expected: []zap.Field{
   376  				zap.String("foo", "foos"),
   377  				zap.String("bar", "bars"),
   378  			},
   379  		},
   380  	}
   381  
   382  	for _, tc := range cases {
   383  		tc := tc
   384  		t.Run(tc.name, func(t *testing.T) {
   385  			var (
   386  				f Fields
   387  				o Fields
   388  			)
   389  			req, err := http.NewRequest("GET", "/", nil)
   390  			startTime := time.Now()
   391  			require.NoError(t, err)
   392  			if tc.fields != nil {
   393  				f = func(r *http.Request, start time.Time) []zap.Field {
   394  					require.Equal(t, req, r)
   395  					require.Equal(t, startTime, start)
   396  					return tc.fields
   397  				}
   398  			}
   399  			if tc.other != nil {
   400  				o = func(r *http.Request, start time.Time) []zap.Field {
   401  					require.Equal(t, req, r)
   402  					require.Equal(t, startTime, start)
   403  					return tc.other
   404  				}
   405  			}
   406  			actual := f.Append(o)(req, startTime)
   407  			require.Equal(t, tc.expected, actual)
   408  		})
   409  	}
   410  }