termd/internal/server/accesslog.go
2026-07-02 22:13:49 -03:00

78 lines
1.9 KiB
Go

package server
import (
"bufio"
"fmt"
"io"
"net"
"net/http"
"sync"
"time"
)
// AccessLog wraps h so every response writes one line to w:
//
// <timestamp> <method> <path> <status> <latency>
//
// For the attach websocket the line appears when the connection ends, with
// status 101 and the connection's lifetime as latency.
func AccessLog(h http.Handler, w io.Writer) http.Handler {
var mu sync.Mutex
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
sw := &statusWriter{ResponseWriter: rw}
start := time.Now()
h.ServeHTTP(sw, r)
status := sw.status
if status == 0 {
status = http.StatusOK
}
line := fmt.Sprintf("%s %s %s %d %s\n",
start.Format("2006-01-02T15:04:05.000Z07:00"),
r.Method, r.URL.RequestURI(), status,
time.Since(start).Round(time.Microsecond))
mu.Lock()
_, _ = io.WriteString(w, line)
mu.Unlock()
})
}
// statusWriter records the status code. It passes hijacking through so the
// attach endpoint's websocket upgrade keeps working.
type statusWriter struct {
http.ResponseWriter
status int
}
func (s *statusWriter) WriteHeader(code int) {
if s.status == 0 {
s.status = code
}
s.ResponseWriter.WriteHeader(code)
}
func (s *statusWriter) Write(b []byte) (int, error) {
if s.status == 0 {
s.status = http.StatusOK
}
return s.ResponseWriter.Write(b)
}
// Unwrap lets http.ResponseController reach the underlying writer.
func (s *statusWriter) Unwrap() http.ResponseWriter { return s.ResponseWriter }
func (s *statusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := s.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, fmt.Errorf("underlying ResponseWriter does not support hijacking")
}
if s.status == 0 {
s.status = http.StatusSwitchingProtocols
}
return h.Hijack()
}
func (s *statusWriter) Flush() {
if f, ok := s.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}