transfer.sh/vendor/github.com/PuerkitoBio/ghost/handlers/log.go
Remco cb6e5cb0c7 Major rewrite
* use dep for vendoring
* lets encrypt
* moved web to transfer.sh-web repo
* single command install
* added first tests
2017-03-22 18:09:21 +01:00

231 lines
6.2 KiB
Go

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
}