package handlers // Inspired by node's Connect library implementation of the logging middleware // https://github.com/senchalabs/connect import ( "fmt" "net/http" "regexp" "strings" "time" "github.com/PuerkitoBio/ghost" ) const ( // Predefined logging formats that can be passed as format string. Ldefault = "_default_" Lshort = "_short_" Ltiny = "_tiny_" ) var ( // Token parser for request and response headers rxHeaders = regexp.MustCompile(`^(req|res)\[([^\]]+)\]$`) // Lookup table for predefined formats predefFormats = map[string]struct { fmt string toks []string }{ Ldefault: { `%s - - [%s] "%s %s HTTP/%s" %d %s "%s" "%s"`, []string{"remote-addr", "date", "method", "url", "http-version", "status", "res[Content-Length]", "referrer", "user-agent"}, }, Lshort: { `%s - %s %s HTTP/%s %d %s - %.3f s`, []string{"remote-addr", "method", "url", "http-version", "status", "res[Content-Length]", "response-time"}, }, Ltiny: { `%s %s %d %s - %.3f s`, []string{"method", "url", "status", "res[Content-Length]", "response-time"}, }, } ) // Augmented ResponseWriter implementation that captures the status code for the logger. type statusResponseWriter struct { http.ResponseWriter code int oriURL string } // Intercept the WriteHeader call to save the status code. func (this *statusResponseWriter) WriteHeader(code int) { this.code = code this.ResponseWriter.WriteHeader(code) } // Intercept the Write call to save the default status code. func (this *statusResponseWriter) Write(data []byte) (int, error) { if this.code == 0 { this.code = http.StatusOK } return this.ResponseWriter.Write(data) } // Implement the WrapWriter interface. func (this *statusResponseWriter) WrappedWriter() http.ResponseWriter { return this.ResponseWriter } // LogHandler options type LogOptions struct { LogFn func(string, ...interface{}) // Defaults to ghost.LogFn if nil Format string Tokens []string CustomTokens map[string]func(http.ResponseWriter, *http.Request) string Immediate bool DateFormat string } // Create a new LogOptions struct. The DateFormat defaults to time.RFC3339. func NewLogOptions(l func(string, ...interface{}), ft string, tok ...string) *LogOptions { return &LogOptions{ LogFn: l, Format: ft, Tokens: tok, CustomTokens: make(map[string]func(http.ResponseWriter, *http.Request) string), DateFormat: time.RFC3339, } } // LogHandlerFunc is the same as LogHandler, it is just a convenience // signature that accepts a func(http.ResponseWriter, *http.Request) instead of // a http.Handler interface. It saves the boilerplate http.HandlerFunc() cast. func LogHandlerFunc(h http.HandlerFunc, opts *LogOptions) http.HandlerFunc { return LogHandler(h, opts) } // Create a log handler for every request it receives. func LogHandler(h http.Handler, opts *LogOptions) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if _, ok := getStatusWriter(w); ok { // Self-awareness, logging handler already set up h.ServeHTTP(w, r) return } // Save the response start time st := time.Now() // Call the wrapped handler, with the augmented ResponseWriter to handle the status code stw := &statusResponseWriter{w, 0, ""} // Log immediately if requested, otherwise on exit if opts.Immediate { logRequest(stw, r, st, opts) } else { // Store original URL, may get modified by handlers (i.e. StripPrefix) stw.oriURL = r.URL.String() defer logRequest(stw, r, st, opts) } h.ServeHTTP(stw, r) } } func getIpAddress(r *http.Request) string { hdr := r.Header hdrRealIp := hdr.Get("X-Real-Ip") hdrForwardedFor := hdr.Get("X-Forwarded-For") if hdrRealIp == "" && hdrForwardedFor == "" { return r.RemoteAddr } if hdrForwardedFor != "" { // X-Forwarded-For is potentially a list of addresses separated with "," part := strings.Split(hdrForwardedFor, ",")[0] return strings.TrimSpace(part) + ":0" } return hdrRealIp } // Check if the specified token is a predefined one, and if so return its current value. func getPredefinedTokenValue(t string, w *statusResponseWriter, r *http.Request, st time.Time, opts *LogOptions) (interface{}, bool) { switch t { case "http-version": return fmt.Sprintf("%d.%d", r.ProtoMajor, r.ProtoMinor), true case "response-time": return time.Now().Sub(st).Seconds(), true case "remote-addr": return getIpAddress(r), true case "date": return time.Now().Format(opts.DateFormat), true case "method": return r.Method, true case "url": if w.oriURL != "" { return w.oriURL, true } return r.URL.String(), true case "referrer", "referer": return r.Referer(), true case "user-agent": return r.UserAgent(), true case "status": return w.code, true } // Handle special cases for header mtch := rxHeaders.FindStringSubmatch(t) if len(mtch) > 2 { if mtch[1] == "req" { return r.Header.Get(mtch[2]), true } else { // This only works for headers explicitly set via the Header() map of // the writer, not those added by the http package under the covers. return w.Header().Get(mtch[2]), true } } return nil, false } // Do the actual logging. func logRequest(w *statusResponseWriter, r *http.Request, st time.Time, opts *LogOptions) { var ( fn func(string, ...interface{}) ok bool format string toks []string ) // If no specific log function, use the default one from the ghost package if opts.LogFn == nil { fn = ghost.LogFn } else { fn = opts.LogFn } // If this is a predefined format, use it instead if v, ok := predefFormats[opts.Format]; ok { format = v.fmt toks = v.toks } else { format = opts.Format toks = opts.Tokens } args := make([]interface{}, len(toks)) for i, t := range toks { if args[i], ok = getPredefinedTokenValue(t, w, r, st, opts); !ok { if f, ok := opts.CustomTokens[t]; ok && f != nil { args[i] = f(w, r) } else { args[i] = "?" } } } fn(format, args...) } // Helper function to retrieve the status writer. func getStatusWriter(w http.ResponseWriter) (*statusResponseWriter, bool) { st, ok := GetResponseWriter(w, func(tst http.ResponseWriter) bool { _, ok := tst.(*statusResponseWriter) return ok }) if ok { return st.(*statusResponseWriter), true } return nil, false }