github.com/gofunct/common@v0.0.0-20190131174352-fd058c7fbf22/pkg/transport/handlers/logging.go (about)

     1  package handlers
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net"
     7  	"net/http"
     8  	"net/url"
     9  	"strconv"
    10  	"time"
    11  	"unicode/utf8"
    12  )
    13  
    14  // Logging
    15  
    16  // FormatterParams is the structure any formatter will be handed when time to log comes
    17  type LogFormatterParams struct {
    18  	Request    *http.Request
    19  	URL        url.URL
    20  	TimeStamp  time.Time
    21  	StatusCode int
    22  	Size       int
    23  }
    24  
    25  // LogFormatter gives the signature of the formatter function passed to CustomLoggingHandler
    26  type LogFormatter func(writer io.Writer, params LogFormatterParams)
    27  
    28  // loggingHandler is the http.Handler implementation for LoggingHandlerTo and its
    29  // friends
    30  
    31  type loggingHandler struct {
    32  	writer    io.Writer
    33  	handler   http.Handler
    34  	formatter LogFormatter
    35  }
    36  
    37  func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    38  	t := time.Now()
    39  	logger := makeLogger(w)
    40  	url := *req.URL
    41  
    42  	h.handler.ServeHTTP(logger, req)
    43  
    44  	params := LogFormatterParams{
    45  		Request:    req,
    46  		URL:        url,
    47  		TimeStamp:  t,
    48  		StatusCode: logger.Status(),
    49  		Size:       logger.Size(),
    50  	}
    51  
    52  	h.formatter(h.writer, params)
    53  }
    54  
    55  func makeLogger(w http.ResponseWriter) loggingResponseWriter {
    56  	var logger loggingResponseWriter = &responseLogger{w: w, status: http.StatusOK}
    57  	if _, ok := w.(http.Hijacker); ok {
    58  		logger = &hijackLogger{responseLogger{w: w, status: http.StatusOK}}
    59  	}
    60  	h, ok1 := logger.(http.Hijacker)
    61  	c, ok2 := w.(http.CloseNotifier)
    62  	if ok1 && ok2 {
    63  		return hijackCloseNotifier{logger, h, c}
    64  	}
    65  	if ok2 {
    66  		return &closeNotifyWriter{logger, c}
    67  	}
    68  	return logger
    69  }
    70  
    71  type commonLoggingResponseWriter interface {
    72  	http.ResponseWriter
    73  	http.Flusher
    74  	Status() int
    75  	Size() int
    76  }
    77  
    78  const lowerhex = "0123456789abcdef"
    79  
    80  func appendQuoted(buf []byte, s string) []byte {
    81  	var runeTmp [utf8.UTFMax]byte
    82  	for width := 0; len(s) > 0; s = s[width:] {
    83  		r := rune(s[0])
    84  		width = 1
    85  		if r >= utf8.RuneSelf {
    86  			r, width = utf8.DecodeRuneInString(s)
    87  		}
    88  		if width == 1 && r == utf8.RuneError {
    89  			buf = append(buf, `\x`...)
    90  			buf = append(buf, lowerhex[s[0]>>4])
    91  			buf = append(buf, lowerhex[s[0]&0xF])
    92  			continue
    93  		}
    94  		if r == rune('"') || r == '\\' { // always backslashed
    95  			buf = append(buf, '\\')
    96  			buf = append(buf, byte(r))
    97  			continue
    98  		}
    99  		if strconv.IsPrint(r) {
   100  			n := utf8.EncodeRune(runeTmp[:], r)
   101  			buf = append(buf, runeTmp[:n]...)
   102  			continue
   103  		}
   104  		switch r {
   105  		case '\a':
   106  			buf = append(buf, `\a`...)
   107  		case '\b':
   108  			buf = append(buf, `\b`...)
   109  		case '\f':
   110  			buf = append(buf, `\f`...)
   111  		case '\n':
   112  			buf = append(buf, `\n`...)
   113  		case '\r':
   114  			buf = append(buf, `\r`...)
   115  		case '\t':
   116  			buf = append(buf, `\t`...)
   117  		case '\v':
   118  			buf = append(buf, `\v`...)
   119  		default:
   120  			switch {
   121  			case r < ' ':
   122  				buf = append(buf, `\x`...)
   123  				buf = append(buf, lowerhex[s[0]>>4])
   124  				buf = append(buf, lowerhex[s[0]&0xF])
   125  			case r > utf8.MaxRune:
   126  				r = 0xFFFD
   127  				fallthrough
   128  			case r < 0x10000:
   129  				buf = append(buf, `\u`...)
   130  				for s := 12; s >= 0; s -= 4 {
   131  					buf = append(buf, lowerhex[r>>uint(s)&0xF])
   132  				}
   133  			default:
   134  				buf = append(buf, `\U`...)
   135  				for s := 28; s >= 0; s -= 4 {
   136  					buf = append(buf, lowerhex[r>>uint(s)&0xF])
   137  				}
   138  			}
   139  		}
   140  	}
   141  	return buf
   142  
   143  }
   144  
   145  // buildCommonLogLine builds a log entry for req in Apache Common Log Format.
   146  // ts is the timestamp with which the entry should be logged.
   147  // status and size are used to provide the response HTTP status and size.
   148  func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int) []byte {
   149  	username := "-"
   150  	if url.User != nil {
   151  		if name := url.User.Username(); name != "" {
   152  			username = name
   153  		}
   154  	}
   155  
   156  	host, _, err := net.SplitHostPort(req.RemoteAddr)
   157  
   158  	if err != nil {
   159  		host = req.RemoteAddr
   160  	}
   161  
   162  	uri := req.RequestURI
   163  
   164  	// Requests using the CONNECT method over HTTP/2.0 must use
   165  	// the authority field (aka r.Host) to identify the target.
   166  	// Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT
   167  	if req.ProtoMajor == 2 && req.Method == "CONNECT" {
   168  		uri = req.Host
   169  	}
   170  	if uri == "" {
   171  		uri = url.RequestURI()
   172  	}
   173  
   174  	buf := make([]byte, 0, 3*(len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2)
   175  	buf = append(buf, host...)
   176  	buf = append(buf, " - "...)
   177  	buf = append(buf, username...)
   178  	buf = append(buf, " ["...)
   179  	buf = append(buf, ts.Format("02/Jan/2006:15:04:05 -0700")...)
   180  	buf = append(buf, `] "`...)
   181  	buf = append(buf, req.Method...)
   182  	buf = append(buf, " "...)
   183  	buf = appendQuoted(buf, uri)
   184  	buf = append(buf, " "...)
   185  	buf = append(buf, req.Proto...)
   186  	buf = append(buf, `" `...)
   187  	buf = append(buf, strconv.Itoa(status)...)
   188  	buf = append(buf, " "...)
   189  	buf = append(buf, strconv.Itoa(size)...)
   190  	return buf
   191  }
   192  
   193  // writeLog writes a log entry for req to w in Apache Common Log Format.
   194  // ts is the timestamp with which the entry should be logged.
   195  // status and size are used to provide the response HTTP status and size.
   196  func writeLog(writer io.Writer, params LogFormatterParams) {
   197  	buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
   198  	buf = append(buf, '\n')
   199  	writer.Write(buf)
   200  }
   201  
   202  // writeCombinedLog writes a log entry for req to w in Apache Combined Log Format.
   203  // ts is the timestamp with which the entry should be logged.
   204  // status and size are used to provide the response HTTP status and size.
   205  func writeCombinedLog(writer io.Writer, params LogFormatterParams) {
   206  	buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
   207  	buf = append(buf, ` "`...)
   208  	buf = appendQuoted(buf, params.Request.Referer())
   209  	buf = append(buf, `" "`...)
   210  	buf = appendQuoted(buf, params.Request.UserAgent())
   211  	buf = append(buf, '"', '\n')
   212  	writer.Write(buf)
   213  }
   214  
   215  // CombinedLoggingHandler return a http.Handler that wraps h and logs requests to out in
   216  // Apache Combined Log Format.
   217  //
   218  // See http://httpd.apache.org/docs/2.2/logs.html#combined for a description of this format.
   219  //
   220  // LoggingHandler always sets the ident field of the log to -
   221  func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler {
   222  	return loggingHandler{out, h, writeCombinedLog}
   223  }
   224  
   225  // LoggingHandler return a http.Handler that wraps h and logs requests to out in
   226  // Apache Common Log Format (CLF).
   227  //
   228  // See http://httpd.apache.org/docs/2.2/logs.html#common for a description of this format.
   229  //
   230  // LoggingHandler always sets the ident field of the log to -
   231  //
   232  // Example:
   233  //
   234  //  r := mux.NewRouter()
   235  //  r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   236  //  	w.Write([]byte("This is a catch-all route"))
   237  //  })
   238  //  loggedRouter := handlers.LoggingHandler(os.Stdout, r)
   239  //  http.ListenAndServe(":1123", loggedRouter)
   240  //
   241  func LoggingHandler(out io.Writer, h http.Handler) http.Handler {
   242  	return loggingHandler{out, h, writeLog}
   243  }
   244  
   245  // CustomLoggingHandler provides a way to supply a custom log formatter
   246  // while taking advantage of the mechanisms in this package
   247  func CustomLoggingHandler(out io.Writer, h http.Handler, f LogFormatter) http.Handler {
   248  	return loggingHandler{out, h, f}
   249  }
   250  
   251  type loggingResponseWriter interface {
   252  	commonLoggingResponseWriter
   253  	http.Pusher
   254  }
   255  
   256  func (l *responseLogger) Push(target string, opts *http.PushOptions) error {
   257  	p, ok := l.w.(http.Pusher)
   258  	if !ok {
   259  		return fmt.Errorf("responseLogger does not implement http.Pusher")
   260  	}
   261  	return p.Push(target, opts)
   262  }
   263  
   264  func (c *compressResponseWriter) Push(target string, opts *http.PushOptions) error {
   265  	p, ok := c.ResponseWriter.(http.Pusher)
   266  	if !ok {
   267  		return fmt.Errorf("compressResponseWriter does not implement http.Pusher")
   268  	}
   269  	return p.Push(target, opts)
   270  }