// 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} }