Skip to content
Snippets Groups Projects
logging.go 6.94 KiB
Newer Older
  • Learn to ignore specific revisions
  • ale's avatar
    ale committed
    // Copyright 2013 The Gorilla Authors. All rights reserved.
    // Use of this source code is governed by a BSD-style
    // license that can be found in the LICENSE file.
    
    package handlers
    
    import (
    	"io"
    	"net"
    	"net/http"
    	"net/url"
    	"strconv"
    	"time"
    	"unicode/utf8"
    )
    
    // Logging
    
    // FormatterParams is the structure any formatter will be handed when time to log comes
    type LogFormatterParams struct {
    	Request    *http.Request
    	URL        url.URL
    	TimeStamp  time.Time
    	StatusCode int
    	Size       int
    }
    
    // LogFormatter gives the signature of the formatter function passed to CustomLoggingHandler
    type LogFormatter func(writer io.Writer, params LogFormatterParams)
    
    // loggingHandler is the http.Handler implementation for LoggingHandlerTo and its
    // friends
    
    type loggingHandler struct {
    	writer    io.Writer
    	handler   http.Handler
    	formatter LogFormatter
    }
    
    func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    	t := time.Now()
    	logger := makeLogger(w)
    	url := *req.URL
    
    	h.handler.ServeHTTP(logger, req)
    
    	params := LogFormatterParams{
    		Request:    req,
    		URL:        url,
    		TimeStamp:  t,
    		StatusCode: logger.Status(),
    		Size:       logger.Size(),
    	}
    
    	h.formatter(h.writer, params)
    }
    
    func makeLogger(w http.ResponseWriter) loggingResponseWriter {
    	var logger loggingResponseWriter = &responseLogger{w: w, status: http.StatusOK}
    	if _, ok := w.(http.Hijacker); ok {
    		logger = &hijackLogger{responseLogger{w: w, status: http.StatusOK}}
    	}
    	h, ok1 := logger.(http.Hijacker)
    	c, ok2 := w.(http.CloseNotifier)
    	if ok1 && ok2 {
    		return hijackCloseNotifier{logger, h, c}
    	}
    	if ok2 {
    		return &closeNotifyWriter{logger, c}
    	}
    	return logger
    }
    
    type commonLoggingResponseWriter interface {
    	http.ResponseWriter
    	http.Flusher
    	Status() int
    	Size() int
    }
    
    const lowerhex = "0123456789abcdef"
    
    func appendQuoted(buf []byte, s string) []byte {
    	var runeTmp [utf8.UTFMax]byte
    	for width := 0; len(s) > 0; s = s[width:] {
    		r := rune(s[0])
    		width = 1
    		if r >= utf8.RuneSelf {
    			r, width = utf8.DecodeRuneInString(s)
    		}
    		if width == 1 && r == utf8.RuneError {
    			buf = append(buf, `\x`...)
    			buf = append(buf, lowerhex[s[0]>>4])
    			buf = append(buf, lowerhex[s[0]&0xF])
    			continue
    		}
    		if r == rune('"') || r == '\\' { // always backslashed
    			buf = append(buf, '\\')
    			buf = append(buf, byte(r))
    			continue
    		}
    		if strconv.IsPrint(r) {
    			n := utf8.EncodeRune(runeTmp[:], r)
    			buf = append(buf, runeTmp[:n]...)
    			continue
    		}
    		switch r {
    		case '\a':
    			buf = append(buf, `\a`...)
    		case '\b':
    			buf = append(buf, `\b`...)
    		case '\f':
    			buf = append(buf, `\f`...)
    		case '\n':
    			buf = append(buf, `\n`...)
    		case '\r':
    			buf = append(buf, `\r`...)
    		case '\t':
    			buf = append(buf, `\t`...)
    		case '\v':
    			buf = append(buf, `\v`...)
    		default:
    			switch {
    			case r < ' ':
    				buf = append(buf, `\x`...)
    				buf = append(buf, lowerhex[s[0]>>4])
    				buf = append(buf, lowerhex[s[0]&0xF])
    			case r > utf8.MaxRune:
    				r = 0xFFFD
    				fallthrough
    			case r < 0x10000:
    				buf = append(buf, `\u`...)
    				for s := 12; s >= 0; s -= 4 {
    					buf = append(buf, lowerhex[r>>uint(s)&0xF])
    				}
    			default:
    				buf = append(buf, `\U`...)
    				for s := 28; s >= 0; s -= 4 {
    					buf = append(buf, lowerhex[r>>uint(s)&0xF])
    				}
    			}
    		}
    	}
    	return buf
    
    }
    
    // buildCommonLogLine builds a log entry for req in Apache Common Log Format.
    // ts is the timestamp with which the entry should be logged.
    // status and size are used to provide the response HTTP status and size.
    func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int) []byte {
    	username := "-"
    	if url.User != nil {
    		if name := url.User.Username(); name != "" {
    			username = name
    		}
    	}
    
    	host, _, err := net.SplitHostPort(req.RemoteAddr)
    
    	if err != nil {
    		host = req.RemoteAddr
    	}
    
    	uri := req.RequestURI
    
    	// Requests using the CONNECT method over HTTP/2.0 must use
    	// the authority field (aka r.Host) to identify the target.
    	// Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT
    	if req.ProtoMajor == 2 && req.Method == "CONNECT" {
    		uri = req.Host
    	}
    	if uri == "" {
    		uri = url.RequestURI()
    	}
    
    	buf := make([]byte, 0, 3*(len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2)
    	buf = append(buf, host...)
    	buf = append(buf, " - "...)
    	buf = append(buf, username...)
    	buf = append(buf, " ["...)
    	buf = append(buf, ts.Format("02/Jan/2006:15:04:05 -0700")...)
    	buf = append(buf, `] "`...)
    	buf = append(buf, req.Method...)
    	buf = append(buf, " "...)
    	buf = appendQuoted(buf, uri)
    	buf = append(buf, " "...)
    	buf = append(buf, req.Proto...)
    	buf = append(buf, `" `...)
    	buf = append(buf, strconv.Itoa(status)...)
    	buf = append(buf, " "...)
    	buf = append(buf, strconv.Itoa(size)...)
    	return buf
    }
    
    // writeLog writes a log entry for req to w in Apache Common Log Format.
    // ts is the timestamp with which the entry should be logged.
    // status and size are used to provide the response HTTP status and size.
    func writeLog(writer io.Writer, params LogFormatterParams) {
    	buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
    	buf = append(buf, '\n')
    	writer.Write(buf)
    }
    
    // writeCombinedLog writes a log entry for req to w in Apache Combined Log Format.
    // ts is the timestamp with which the entry should be logged.
    // status and size are used to provide the response HTTP status and size.
    func writeCombinedLog(writer io.Writer, params LogFormatterParams) {
    	buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
    	buf = append(buf, ` "`...)
    	buf = appendQuoted(buf, params.Request.Referer())
    	buf = append(buf, `" "`...)
    	buf = appendQuoted(buf, params.Request.UserAgent())
    	buf = append(buf, '"', '\n')
    	writer.Write(buf)
    }
    
    // CombinedLoggingHandler return a http.Handler that wraps h and logs requests to out in
    // Apache Combined Log Format.
    //
    // See http://httpd.apache.org/docs/2.2/logs.html#combined for a description of this format.
    //
    // LoggingHandler always sets the ident field of the log to -
    func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler {
    	return loggingHandler{out, h, writeCombinedLog}
    }
    
    // LoggingHandler return a http.Handler that wraps h and logs requests to out in
    // Apache Common Log Format (CLF).
    //
    // See http://httpd.apache.org/docs/2.2/logs.html#common for a description of this format.
    //
    // LoggingHandler always sets the ident field of the log to -
    //
    // Example:
    //
    //  r := mux.NewRouter()
    //  r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    //  	w.Write([]byte("This is a catch-all route"))
    //  })
    //  loggedRouter := handlers.LoggingHandler(os.Stdout, r)
    //  http.ListenAndServe(":1123", loggedRouter)
    //
    func LoggingHandler(out io.Writer, h http.Handler) http.Handler {
    	return loggingHandler{out, h, writeLog}
    }
    
    // CustomLoggingHandler provides a way to supply a custom log formatter
    // while taking advantage of the mechanisms in this package
    func CustomLoggingHandler(out io.Writer, h http.Handler, f LogFormatter) http.Handler {
    	return loggingHandler{out, h, f}
    }