141 lines
3.4 KiB
Go
141 lines
3.4 KiB
Go
package logging
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"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 (s *statusRecorder) WriteHeader(code int) {
|
|
if s.status == 0 {
|
|
s.status = code
|
|
}
|
|
s.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
// Unwrap returns the wrapped ResponseWriter, so that http.ResponseController
|
|
// can reach its Hijack and Flush: a WebSocket upgrade hijacks the connection.
|
|
func (s *statusRecorder) Unwrap() http.ResponseWriter {
|
|
return s.ResponseWriter
|
|
}
|
|
|
|
func (s *statusRecorder) Write(data []byte) (int, error) {
|
|
if s.status == 0 {
|
|
s.status = http.StatusOK
|
|
}
|
|
|
|
written, err := s.ResponseWriter.Write(data)
|
|
s.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(r *http.Request) string {
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
return r.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, r *http.Request) {
|
|
start := time.Now()
|
|
recorder := &statusRecorder{
|
|
ResponseWriter: w,
|
|
}
|
|
next.ServeHTTP(recorder, r)
|
|
if recorder.status == 0 {
|
|
recorder.status = http.StatusOK
|
|
}
|
|
|
|
user := "-"
|
|
if username, _, ok := r.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(r),
|
|
user,
|
|
start.Format("02/Jan/2006:15:04:05 -0700"),
|
|
logField(r.Method+" "+r.RequestURI+" "+r.Proto),
|
|
recorder.status,
|
|
size,
|
|
)
|
|
if combined {
|
|
line += fmt.Sprintf(
|
|
` "%s" "%s"`, logField(r.Referer()), logField(r.UserAgent()),
|
|
)
|
|
}
|
|
logger.Println(line)
|
|
})
|
|
}
|
|
|
|
// OpenAccessLog opens the access log at path for appending, creating it if
|
|
// needed, or stdout for "-", for AccessLog. It returns a logger writing to it
|
|
// and a function that closes it (doing nothing for stdout).
|
|
func OpenAccessLog(path string) (*log.Logger, func() error, error) {
|
|
if path == "-" {
|
|
return log.New(os.Stdout, "", 0), func() error {
|
|
return nil
|
|
}, nil
|
|
}
|
|
|
|
file, err := os.OpenFile(
|
|
filepath.Clean(path), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("opening access log: %w", err)
|
|
}
|
|
|
|
return log.New(file, "", 0), file.Close, nil
|
|
}
|