github.com/lingyao2333/mo-zero@v1.4.1/rest/handler/loghandler.go (about)

     1  package handler
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net"
    11  	"net/http"
    12  	"net/http/httputil"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/lingyao2333/mo-zero/core/color"
    18  	"github.com/lingyao2333/mo-zero/core/iox"
    19  	"github.com/lingyao2333/mo-zero/core/logx"
    20  	"github.com/lingyao2333/mo-zero/core/syncx"
    21  	"github.com/lingyao2333/mo-zero/core/timex"
    22  	"github.com/lingyao2333/mo-zero/core/utils"
    23  	"github.com/lingyao2333/mo-zero/rest/httpx"
    24  	"github.com/lingyao2333/mo-zero/rest/internal"
    25  )
    26  
    27  const (
    28  	limitBodyBytes       = 1024
    29  	defaultSlowThreshold = time.Millisecond * 500
    30  )
    31  
    32  var slowThreshold = syncx.ForAtomicDuration(defaultSlowThreshold)
    33  
    34  type loggedResponseWriter struct {
    35  	w    http.ResponseWriter
    36  	r    *http.Request
    37  	code int
    38  }
    39  
    40  func (w *loggedResponseWriter) Flush() {
    41  	if flusher, ok := w.w.(http.Flusher); ok {
    42  		flusher.Flush()
    43  	}
    44  }
    45  
    46  func (w *loggedResponseWriter) Header() http.Header {
    47  	return w.w.Header()
    48  }
    49  
    50  // Hijack implements the http.Hijacker interface.
    51  // This expands the Response to fulfill http.Hijacker if the underlying http.ResponseWriter supports it.
    52  func (w *loggedResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
    53  	if hijacked, ok := w.w.(http.Hijacker); ok {
    54  		return hijacked.Hijack()
    55  	}
    56  
    57  	return nil, nil, errors.New("server doesn't support hijacking")
    58  }
    59  
    60  func (w *loggedResponseWriter) Write(bytes []byte) (int, error) {
    61  	return w.w.Write(bytes)
    62  }
    63  
    64  func (w *loggedResponseWriter) WriteHeader(code int) {
    65  	w.w.WriteHeader(code)
    66  	w.code = code
    67  }
    68  
    69  // LogHandler returns a middleware that logs http request and response.
    70  func LogHandler(next http.Handler) http.Handler {
    71  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    72  		timer := utils.NewElapsedTimer()
    73  		logs := new(internal.LogCollector)
    74  		lrw := loggedResponseWriter{
    75  			w:    w,
    76  			r:    r,
    77  			code: http.StatusOK,
    78  		}
    79  
    80  		var dup io.ReadCloser
    81  		r.Body, dup = iox.DupReadCloser(r.Body)
    82  		next.ServeHTTP(&lrw, r.WithContext(context.WithValue(r.Context(), internal.LogContext, logs)))
    83  		r.Body = dup
    84  		logBrief(r, lrw.code, timer, logs)
    85  	})
    86  }
    87  
    88  type detailLoggedResponseWriter struct {
    89  	writer *loggedResponseWriter
    90  	buf    *bytes.Buffer
    91  }
    92  
    93  func newDetailLoggedResponseWriter(writer *loggedResponseWriter, buf *bytes.Buffer) *detailLoggedResponseWriter {
    94  	return &detailLoggedResponseWriter{
    95  		writer: writer,
    96  		buf:    buf,
    97  	}
    98  }
    99  
   100  func (w *detailLoggedResponseWriter) Flush() {
   101  	w.writer.Flush()
   102  }
   103  
   104  func (w *detailLoggedResponseWriter) Header() http.Header {
   105  	return w.writer.Header()
   106  }
   107  
   108  // Hijack implements the http.Hijacker interface.
   109  // This expands the Response to fulfill http.Hijacker if the underlying http.ResponseWriter supports it.
   110  func (w *detailLoggedResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
   111  	if hijacked, ok := w.writer.w.(http.Hijacker); ok {
   112  		return hijacked.Hijack()
   113  	}
   114  
   115  	return nil, nil, errors.New("server doesn't support hijacking")
   116  }
   117  
   118  func (w *detailLoggedResponseWriter) Write(bs []byte) (int, error) {
   119  	w.buf.Write(bs)
   120  	return w.writer.Write(bs)
   121  }
   122  
   123  func (w *detailLoggedResponseWriter) WriteHeader(code int) {
   124  	w.writer.WriteHeader(code)
   125  }
   126  
   127  // DetailedLogHandler returns a middleware that logs http request and response in details.
   128  func DetailedLogHandler(next http.Handler) http.Handler {
   129  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   130  		timer := utils.NewElapsedTimer()
   131  		var buf bytes.Buffer
   132  		lrw := newDetailLoggedResponseWriter(&loggedResponseWriter{
   133  			w:    w,
   134  			r:    r,
   135  			code: http.StatusOK,
   136  		}, &buf)
   137  
   138  		var dup io.ReadCloser
   139  		r.Body, dup = iox.DupReadCloser(r.Body)
   140  		logs := new(internal.LogCollector)
   141  		next.ServeHTTP(lrw, r.WithContext(context.WithValue(r.Context(), internal.LogContext, logs)))
   142  		r.Body = dup
   143  		logDetails(r, lrw, timer, logs)
   144  	})
   145  }
   146  
   147  // SetSlowThreshold sets the slow threshold.
   148  func SetSlowThreshold(threshold time.Duration) {
   149  	slowThreshold.Set(threshold)
   150  }
   151  
   152  func dumpRequest(r *http.Request) string {
   153  	reqContent, err := httputil.DumpRequest(r, true)
   154  	if err != nil {
   155  		return err.Error()
   156  	}
   157  
   158  	return string(reqContent)
   159  }
   160  
   161  func isOkResponse(code int) bool {
   162  	// not server error
   163  	return code < http.StatusInternalServerError
   164  }
   165  
   166  func logBrief(r *http.Request, code int, timer *utils.ElapsedTimer, logs *internal.LogCollector) {
   167  	var buf bytes.Buffer
   168  	duration := timer.Duration()
   169  	logger := logx.WithContext(r.Context()).WithDuration(duration)
   170  	buf.WriteString(fmt.Sprintf("[HTTP] %s - %s %s - %s - %s",
   171  		wrapStatusCode(code), wrapMethod(r.Method), r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent()))
   172  	if duration > slowThreshold.Load() {
   173  		logger.Slowf("[HTTP] %s - %s %s - %s - %s - slowcall(%s)",
   174  			wrapStatusCode(code), wrapMethod(r.Method), r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent(),
   175  			timex.ReprOfDuration(duration))
   176  	}
   177  
   178  	ok := isOkResponse(code)
   179  	if !ok {
   180  		fullReq := dumpRequest(r)
   181  		limitReader := io.LimitReader(strings.NewReader(fullReq), limitBodyBytes)
   182  		body, err := io.ReadAll(limitReader)
   183  		if err != nil {
   184  			buf.WriteString(fmt.Sprintf("\n%s", fullReq))
   185  		} else {
   186  			buf.WriteString(fmt.Sprintf("\n%s", string(body)))
   187  		}
   188  	}
   189  
   190  	body := logs.Flush()
   191  	if len(body) > 0 {
   192  		buf.WriteString(fmt.Sprintf("\n%s", body))
   193  	}
   194  
   195  	if ok {
   196  		logger.Info(buf.String())
   197  	} else {
   198  		logger.Error(buf.String())
   199  	}
   200  }
   201  
   202  func logDetails(r *http.Request, response *detailLoggedResponseWriter, timer *utils.ElapsedTimer,
   203  	logs *internal.LogCollector) {
   204  	var buf bytes.Buffer
   205  	duration := timer.Duration()
   206  	code := response.writer.code
   207  	logger := logx.WithContext(r.Context())
   208  	buf.WriteString(fmt.Sprintf("[HTTP] %s - %d - %s - %s\n=> %s\n",
   209  		r.Method, code, r.RemoteAddr, timex.ReprOfDuration(duration), dumpRequest(r)))
   210  	if duration > defaultSlowThreshold {
   211  		logger.Slowf("[HTTP] %s - %d - %s - slowcall(%s)\n=> %s\n", r.Method, code, r.RemoteAddr,
   212  			fmt.Sprintf("slowcall(%s)", timex.ReprOfDuration(duration)), dumpRequest(r))
   213  	}
   214  
   215  	body := logs.Flush()
   216  	if len(body) > 0 {
   217  		buf.WriteString(fmt.Sprintf("%s\n", body))
   218  	}
   219  
   220  	respBuf := response.buf.Bytes()
   221  	if len(respBuf) > 0 {
   222  		buf.WriteString(fmt.Sprintf("<= %s", respBuf))
   223  	}
   224  
   225  	if isOkResponse(code) {
   226  		logger.Info(buf.String())
   227  	} else {
   228  		logger.Error(buf.String())
   229  	}
   230  }
   231  
   232  func wrapMethod(method string) string {
   233  	var colour color.Color
   234  	switch method {
   235  	case http.MethodGet:
   236  		colour = color.BgBlue
   237  	case http.MethodPost:
   238  		colour = color.BgCyan
   239  	case http.MethodPut:
   240  		colour = color.BgYellow
   241  	case http.MethodDelete:
   242  		colour = color.BgRed
   243  	case http.MethodPatch:
   244  		colour = color.BgGreen
   245  	case http.MethodHead:
   246  		colour = color.BgMagenta
   247  	case http.MethodOptions:
   248  		colour = color.BgWhite
   249  	}
   250  
   251  	if colour == color.NoColor {
   252  		return method
   253  	}
   254  
   255  	return logx.WithColorPadding(method, colour)
   256  }
   257  
   258  func wrapStatusCode(code int) string {
   259  	var colour color.Color
   260  	switch {
   261  	case code >= http.StatusOK && code < http.StatusMultipleChoices:
   262  		colour = color.BgGreen
   263  	case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
   264  		colour = color.BgBlue
   265  	case code >= http.StatusBadRequest && code < http.StatusInternalServerError:
   266  		colour = color.BgMagenta
   267  	default:
   268  		colour = color.BgYellow
   269  	}
   270  
   271  	return logx.WithColorPadding(strconv.Itoa(code), colour)
   272  }