github.com/m3db/m3@v1.5.0/src/query/api/v1/middleware/logging.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  	"net/http"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/gorilla/mux"
    29  	"github.com/opentracing/opentracing-go"
    30  	"go.uber.org/zap"
    31  
    32  	"github.com/m3db/m3/src/cmd/services/m3query/config"
    33  	"github.com/m3db/m3/src/query/util/logging"
    34  	"github.com/m3db/m3/src/x/headers"
    35  	xhttp "github.com/m3db/m3/src/x/http"
    36  	"github.com/m3db/m3/src/x/instrument"
    37  )
    38  
    39  // RequestID populates the request scoped logger with a unique request ID.
    40  func RequestID(iOpts instrument.Options) mux.MiddlewareFunc {
    41  	return func(base http.Handler) http.Handler {
    42  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    43  			rqCtx := logging.NewContextWithGeneratedID(r.Context(), iOpts)
    44  
    45  			sp := opentracing.SpanFromContext(rqCtx)
    46  			if sp != nil {
    47  				rqID := logging.ReadContextID(rqCtx)
    48  				sp.SetTag("rqID", rqID)
    49  			}
    50  
    51  			// Propagate the context with the reqId
    52  			base.ServeHTTP(w, r.WithContext(rqCtx))
    53  		})
    54  	}
    55  }
    56  
    57  // LoggingOptions are the options for the logging middleware.
    58  type LoggingOptions struct {
    59  	Threshold time.Duration
    60  	Disabled  bool
    61  	Fields    Fields
    62  }
    63  
    64  // NewLoggingOptions returns new options from the config.
    65  func NewLoggingOptions(c config.LoggingMiddlewareConfiguration) LoggingOptions {
    66  	if c.Threshold == 0 {
    67  		c.Threshold = time.Second
    68  	}
    69  	return LoggingOptions{
    70  		Threshold: c.Threshold,
    71  		Disabled:  c.Disabled,
    72  	}
    73  }
    74  
    75  // Append the provided Fields to these Fields.
    76  func (f Fields) Append(other Fields) Fields {
    77  	return func(r *http.Request, start time.Time) []zap.Field {
    78  		var fields []zap.Field
    79  		if f != nil {
    80  			fields = f(r, start)
    81  		}
    82  		if other != nil {
    83  			fields = append(fields, other(r, start)...)
    84  		}
    85  		return fields
    86  	}
    87  }
    88  
    89  // WithNoResponseLogging disables response logging for a route.
    90  var WithNoResponseLogging = func(opts Options) Options {
    91  	opts.Logging.Disabled = true
    92  	return opts
    93  }
    94  
    95  // Fields returns additional logging fields to add to the response log message.
    96  type Fields func(r *http.Request, start time.Time) []zap.Field
    97  
    98  // ResponseLogging logs the response time if the request took longer than the configured threshold.
    99  func ResponseLogging(opts Options) mux.MiddlewareFunc {
   100  	return func(base http.Handler) http.Handler {
   101  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   102  			start := opts.Clock.Now()
   103  			statusCodeTracking := &xhttp.StatusCodeTracker{ResponseWriter: w, TrackError: true}
   104  			w = statusCodeTracking.WrappedResponseWriter()
   105  			base.ServeHTTP(w, r)
   106  			d := opts.Clock.Now().Sub(start)
   107  			if !opts.Logging.Disabled && d >= opts.Logging.Threshold {
   108  				logger := logging.WithContext(r.Context(), opts.InstrumentOpts)
   109  				fields := []zap.Field{
   110  					zap.Duration("duration", d),
   111  					zap.String("url", r.URL.RequestURI()),
   112  					zap.Int("status", statusCodeTracking.Status),
   113  				}
   114  				if statusCodeTracking.ErrMsg != "" {
   115  					fields = append(fields, zap.String("error", statusCodeTracking.ErrMsg))
   116  				}
   117  				fields = appendM3Headers(r.Header, fields)
   118  				fields = appendM3Headers(w.Header(), fields)
   119  				if opts.Logging.Fields != nil {
   120  					fields = append(fields, opts.Logging.Fields(r, start)...)
   121  				}
   122  				logger.Info("finished handling request", fields...)
   123  			}
   124  		})
   125  	}
   126  }
   127  
   128  func appendM3Headers(h http.Header, fields []zap.Field) []zap.Field {
   129  	for k, v := range h {
   130  		if strings.HasPrefix(k, headers.M3HeaderPrefix) {
   131  			if len(v) == 1 {
   132  				fields = append(fields, zap.String(k, v[0]))
   133  			} else {
   134  				fields = append(fields, zap.Strings(k, v))
   135  			}
   136  		}
   137  	}
   138  	return fields
   139  }