...

Source file src/github.com/palantir/go-githubapp/githubapp/middleware_logging.go

Documentation: github.com/palantir/go-githubapp/githubapp

     1  // Copyright 2022 Palantir Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package githubapp
    16  
    17  import (
    18  	"bytes"
    19  	"io"
    20  	"net/http"
    21  	"regexp"
    22  	"time"
    23  
    24  	"github.com/gregjones/httpcache"
    25  	"github.com/rs/zerolog"
    26  )
    27  
    28  // ClientLogging creates client middleware that logs request and response
    29  // information at the given level. If the request fails without creating a
    30  // response, it is logged with a status code of -1. The middleware uses a
    31  // logger from the request context.
    32  func ClientLogging(lvl zerolog.Level, opts ...ClientLoggingOption) ClientMiddleware {
    33  	var options clientLoggingOptions
    34  	for _, opt := range opts {
    35  		opt(&options)
    36  	}
    37  
    38  	return func(next http.RoundTripper) http.RoundTripper {
    39  		return roundTripperFunc(func(r *http.Request) (*http.Response, error) {
    40  			var err error
    41  			var reqBody, resBody []byte
    42  
    43  			if requestMatches(r, options.RequestBodyPatterns) {
    44  				if r, reqBody, err = mirrorRequestBody(r); err != nil {
    45  					return nil, err
    46  				}
    47  			}
    48  
    49  			start := time.Now()
    50  			res, err := next.RoundTrip(r)
    51  			elapsed := time.Now().Sub(start)
    52  
    53  			evt := zerolog.Ctx(r.Context()).
    54  				WithLevel(lvl).
    55  				Str("method", r.Method).
    56  				Str("path", r.URL.String()).
    57  				Dur("elapsed", elapsed)
    58  
    59  			if reqBody != nil {
    60  				evt.Bytes("request_body", reqBody)
    61  			}
    62  
    63  			if res != nil {
    64  				cached := res.Header.Get(httpcache.XFromCache) != ""
    65  				evt.Bool("cached", cached).
    66  					Int("status", res.StatusCode)
    67  
    68  				size := res.ContentLength
    69  				if requestMatches(r, options.ResponseBodyPatterns) {
    70  					if res, resBody, err = mirrorResponseBody(res); err != nil {
    71  						return res, err
    72  					}
    73  					if size < 0 {
    74  						size = int64(len(resBody))
    75  					}
    76  					evt.Int64("size", size).Bytes("response_body", resBody)
    77  				} else {
    78  					evt.Int64("size", size)
    79  				}
    80  			} else {
    81  				evt.Bool("cached", false).
    82  					Int("status", -1).
    83  					Int64("size", -1)
    84  			}
    85  
    86  			evt.Msg("github_request")
    87  			return res, err
    88  		})
    89  	}
    90  }
    91  
    92  // ClientLoggingOption controls behavior of client request logs.
    93  type ClientLoggingOption func(*clientLoggingOptions)
    94  
    95  type clientLoggingOptions struct {
    96  	RequestBodyPatterns  []*regexp.Regexp
    97  	ResponseBodyPatterns []*regexp.Regexp
    98  }
    99  
   100  // LogRequestBody enables request body logging for requests to paths matching
   101  // any of the regular expressions in patterns. It panics if any of the patterns
   102  // is not a valid regular expression.
   103  func LogRequestBody(patterns ...string) ClientLoggingOption {
   104  	regexps := compileRegexps(patterns)
   105  	return func(opts *clientLoggingOptions) {
   106  		opts.RequestBodyPatterns = regexps
   107  	}
   108  }
   109  
   110  // LogResponseBody enables response body logging for requests to paths matching
   111  // any of the regular expressions in patterns. It panics if any of the patterns
   112  // is not a valid regular expression.
   113  func LogResponseBody(patterns ...string) ClientLoggingOption {
   114  	regexps := compileRegexps(patterns)
   115  	return func(opts *clientLoggingOptions) {
   116  		opts.ResponseBodyPatterns = regexps
   117  	}
   118  }
   119  
   120  func mirrorRequestBody(r *http.Request) (*http.Request, []byte, error) {
   121  	switch {
   122  	case r.Body == nil || r.Body == http.NoBody:
   123  		return r, []byte{}, nil
   124  
   125  	case r.GetBody != nil:
   126  		br, err := r.GetBody()
   127  		if err != nil {
   128  			return r, nil, err
   129  		}
   130  		body, err := io.ReadAll(br)
   131  		closeBody(br)
   132  		return r, body, err
   133  
   134  	default:
   135  		body, err := io.ReadAll(r.Body)
   136  		closeBody(r.Body)
   137  		if err != nil {
   138  			return r, nil, err
   139  		}
   140  		rCopy := r.Clone(r.Context())
   141  		rCopy.Body = io.NopCloser(bytes.NewReader(body))
   142  		return rCopy, body, nil
   143  	}
   144  }
   145  
   146  func mirrorResponseBody(res *http.Response) (*http.Response, []byte, error) {
   147  	body, err := io.ReadAll(res.Body)
   148  	closeBody(res.Body)
   149  	if err != nil {
   150  		return res, nil, err
   151  	}
   152  
   153  	res.Body = io.NopCloser(bytes.NewReader(body))
   154  	return res, body, nil
   155  }
   156  
   157  func compileRegexps(pats []string) []*regexp.Regexp {
   158  	regexps := make([]*regexp.Regexp, len(pats))
   159  	for i, p := range pats {
   160  		regexps[i] = regexp.MustCompile(p)
   161  	}
   162  	return regexps
   163  }
   164  
   165  func requestMatches(r *http.Request, pats []*regexp.Regexp) bool {
   166  	for _, pat := range pats {
   167  		if pat.MatchString(r.URL.Path) {
   168  			return true
   169  		}
   170  	}
   171  	return false
   172  }
   173  
   174  func closeBody(b io.ReadCloser) {
   175  	_ = b.Close() // per http.Transport impl, ignoring close errors is fine
   176  }
   177  

View as plain text