| // Copyright 2017 The Go Authors. All rights reserved. |
| // |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file or at |
| // https://developers.google.com/open-source/licenses/bsd. |
| |
| // Package requestlog provides an http.Handler that logs information |
| // about requests. |
| package requestlog |
| |
| import ( |
| "errors" |
| "io" |
| "io/ioutil" |
| "net" |
| "net/http" |
| "time" |
| ) |
| |
| // Logger wraps the Log method. Log must be safe to call from multiple |
| // goroutines. Log must not hold onto an Entry after it returns. |
| type Logger interface { |
| Log(*Entry) |
| } |
| |
| // A Handler emits request information to a Logger. |
| type Handler struct { |
| log Logger |
| h http.Handler |
| } |
| |
| // NewHandler returns a handler that emits information to log and calls |
| // h.ServeHTTP. |
| func NewHandler(log Logger, h http.Handler) *Handler { |
| return &Handler{ |
| log: log, |
| h: h, |
| } |
| } |
| |
| // ServeHTTP calls its underlying handler's ServeHTTP method, then calls |
| // Log after the handler returns. |
| // |
| // ServeHTTP will always consume the request body up to the first error, |
| // even if the underlying handler does not. |
| func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| start := time.Now() |
| ent := &Entry{ |
| ReceivedTime: start, |
| RequestMethod: r.Method, |
| RequestURL: r.URL.String(), |
| RequestHeaderSize: headerSize(r.Header), |
| UserAgent: r.UserAgent(), |
| Referer: r.Referer(), |
| Proto: r.Proto, |
| RemoteIP: ipFromHostPort(r.RemoteAddr), |
| } |
| if addr, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr); ok { |
| ent.ServerIP = ipFromHostPort(addr.String()) |
| } |
| r2 := new(http.Request) |
| *r2 = *r |
| rcc := &readCounterCloser{r: r.Body} |
| r2.Body = rcc |
| w2 := &responseStats{w: w} |
| |
| h.h.ServeHTTP(w2, r2) |
| |
| ent.Latency = time.Since(start) |
| if rcc.err == nil { |
| // If the handler hasn't encountered an error in the Body (like EOF), |
| // then consume the rest of the Body to provide an accurate rcc.n. |
| io.Copy(ioutil.Discard, rcc) |
| } |
| ent.RequestBodySize = rcc.n |
| ent.Status = w2.code |
| if ent.Status == 0 { |
| ent.Status = http.StatusOK |
| } |
| ent.ResponseHeaderSize, ent.ResponseBodySize = w2.size() |
| h.log.Log(ent) |
| } |
| |
| // Entry records information about a completed HTTP request. |
| type Entry struct { |
| ReceivedTime time.Time |
| RequestMethod string |
| RequestURL string |
| RequestHeaderSize int64 |
| RequestBodySize int64 |
| UserAgent string |
| Referer string |
| Proto string |
| |
| RemoteIP string |
| ServerIP string |
| |
| Status int |
| ResponseHeaderSize int64 |
| ResponseBodySize int64 |
| Latency time.Duration |
| } |
| |
| func ipFromHostPort(hp string) string { |
| h, _, err := net.SplitHostPort(hp) |
| if err != nil { |
| return "" |
| } |
| if len(h) > 0 && h[0] == '[' { |
| return h[1 : len(h)-1] |
| } |
| return h |
| } |
| |
| type readCounterCloser struct { |
| r io.ReadCloser |
| n int64 |
| err error |
| } |
| |
| func (rcc *readCounterCloser) Read(p []byte) (n int, err error) { |
| if rcc.err != nil { |
| return 0, rcc.err |
| } |
| n, rcc.err = rcc.r.Read(p) |
| rcc.n += int64(n) |
| return n, rcc.err |
| } |
| |
| func (rcc *readCounterCloser) Close() error { |
| rcc.err = errors.New("read from closed reader") |
| return rcc.r.Close() |
| } |
| |
| type writeCounter int64 |
| |
| func (wc *writeCounter) Write(p []byte) (n int, err error) { |
| *wc += writeCounter(len(p)) |
| return len(p), nil |
| } |
| |
| func headerSize(h http.Header) int64 { |
| var wc writeCounter |
| h.Write(&wc) |
| return int64(wc) + 2 // for CRLF |
| } |
| |
| type responseStats struct { |
| w http.ResponseWriter |
| hsize int64 |
| wc writeCounter |
| code int |
| } |
| |
| func (r *responseStats) Header() http.Header { |
| return r.w.Header() |
| } |
| |
| func (r *responseStats) WriteHeader(statusCode int) { |
| if r.code != 0 { |
| return |
| } |
| r.hsize = headerSize(r.w.Header()) |
| r.w.WriteHeader(statusCode) |
| r.code = statusCode |
| } |
| |
| func (r *responseStats) Write(p []byte) (n int, err error) { |
| if r.code == 0 { |
| r.WriteHeader(http.StatusOK) |
| } |
| n, err = r.w.Write(p) |
| r.wc.Write(p[:n]) |
| return |
| } |
| |
| func (r *responseStats) size() (hdr, body int64) { |
| if r.code == 0 { |
| return headerSize(r.w.Header()), 0 |
| } |
| // Use the header size from the time WriteHeader was called. |
| // The Header map can be mutated after the call to add HTTP Trailers, |
| // which we don't want to count. |
| return r.hsize, int64(r.wc) |
| } |