Short and single-letter names in application code become full words (tests keep theirs). Wrapped calls, signatures and composite literals put one item per line, and one-line function bodies are expanded. Exported names and groups of constants get doc comments, and comments refer to the renamed identifiers.
107 lines
2.5 KiB
Go
107 lines
2.5 KiB
Go
package logging
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ---------- Access logging ----------
|
|
|
|
// statusRecorder captures the status code and body size for the access log.
|
|
type statusRecorder struct {
|
|
http.ResponseWriter
|
|
status int
|
|
bytes int
|
|
}
|
|
|
|
func (r *statusRecorder) WriteHeader(code int) {
|
|
if r.status == 0 {
|
|
r.status = code
|
|
}
|
|
r.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
func (r *statusRecorder) Write(data []byte) (int, error) {
|
|
if r.status == 0 {
|
|
r.status = http.StatusOK
|
|
}
|
|
written, err := r.ResponseWriter.Write(data)
|
|
r.bytes += written
|
|
return written, err
|
|
}
|
|
|
|
// logField escapes a value for a quoted log field the way nginx does
|
|
// (quotes, backslashes and control characters become \xHH) and uses "-"
|
|
// for empty values.
|
|
func logField(value string) string {
|
|
if value == "" {
|
|
return "-"
|
|
}
|
|
var builder strings.Builder
|
|
for i := 0; i < len(value); i++ {
|
|
char := value[i]
|
|
if char == '"' || char == '\\' || char < 0x20 || char == 0x7f {
|
|
fmt.Fprintf(&builder, "\\x%02X", char)
|
|
} else {
|
|
builder.WriteByte(char)
|
|
}
|
|
}
|
|
return builder.String()
|
|
}
|
|
|
|
// clientKey attempts to produce a client host name as a key identifier to split
|
|
// requests in the log between sources. Could be extended to also check
|
|
// Forwarded-For or other proxy headers
|
|
func clientKey(req *http.Request) string {
|
|
host, _, err := net.SplitHostPort(req.RemoteAddr)
|
|
if err != nil {
|
|
return req.RemoteAddr
|
|
}
|
|
return host
|
|
}
|
|
|
|
// AccessLog wraps next, writing a line to logger for each request in the
|
|
// Apache/nginx Common Log Format, or the Combined Log Format (which adds the
|
|
// referer and user agent) if combined is true.
|
|
func AccessLog(logger *log.Logger, combined bool, next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
start := time.Now()
|
|
recorder := &statusRecorder{
|
|
ResponseWriter: w,
|
|
}
|
|
next.ServeHTTP(recorder, req)
|
|
if recorder.status == 0 {
|
|
recorder.status = http.StatusOK
|
|
}
|
|
|
|
user := "-"
|
|
if username, _, ok := req.BasicAuth(); ok && username != "" {
|
|
user = logField(username)
|
|
}
|
|
size := "-" // CLF uses "-" for an empty body
|
|
if recorder.bytes > 0 {
|
|
size = strconv.Itoa(recorder.bytes)
|
|
}
|
|
line := fmt.Sprintf(
|
|
`%s - %s [%s] "%s" %d %s`,
|
|
clientKey(req),
|
|
user,
|
|
start.Format("02/Jan/2006:15:04:05 -0700"),
|
|
logField(req.Method+" "+req.RequestURI+" "+req.Proto),
|
|
recorder.status,
|
|
size,
|
|
)
|
|
if combined {
|
|
line += fmt.Sprintf(
|
|
` "%s" "%s"`, logField(req.Referer()), logField(req.UserAgent()),
|
|
)
|
|
}
|
|
logger.Println(line)
|
|
})
|
|
}
|