Files
ddc/internal/server/observe.go
T

116 lines
2.9 KiB
Go

package server
import (
"context"
"crypto/rand"
"encoding/hex"
"log/slog"
"net/http"
"runtime/debug"
"time"
)
type contextKey int
const requestIDKey contextKey = iota
type statusRecorder struct {
http.ResponseWriter
status int
}
func (recorder *statusRecorder) WriteHeader(status int) {
if recorder.status == 0 {
recorder.status = status
}
recorder.ResponseWriter.WriteHeader(status)
}
func (recorder *statusRecorder) Write(data []byte) (int, error) {
if recorder.status == 0 {
recorder.status = http.StatusOK
}
return recorder.ResponseWriter.Write(data)
}
func (recorder *statusRecorder) Unwrap() http.ResponseWriter {
return recorder.ResponseWriter
}
func (server *Server) observe(next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
started := time.Now()
requestID := newRequestID()
writer.Header().Set("X-Request-ID", requestID)
request = request.WithContext(context.WithValue(request.Context(), requestIDKey, requestID))
recorder := &statusRecorder{ResponseWriter: writer}
defer func() {
if recovered := recover(); recovered != nil {
if recovered == http.ErrAbortHandler {
panic(recovered)
}
slog.Error("request handler crashed",
"request_id", requestID,
"method", request.Method,
"path", request.URL.Path,
"panic", recovered,
"stack", string(debug.Stack()),
)
if recorder.status == 0 {
writeError(recorder, http.StatusInternalServerError, internalErrorMessage(requestID))
}
}
server.logRequest(request, recorder.status, time.Since(started))
}()
next.ServeHTTP(recorder, request)
})
}
func (server *Server) logRequest(request *http.Request, status int, duration time.Duration) {
if status == 0 {
status = http.StatusOK
}
slog.Log(request.Context(), requestLogLevel(request.Method, status), "request",
"request_id", requestIDFrom(request),
"method", request.Method,
"path", request.URL.Path,
"status", status,
"duration_ms", duration.Milliseconds(),
"client", server.clientAddress(request),
)
}
func requestLogLevel(method string, status int) slog.Level {
switch {
case status >= http.StatusInternalServerError:
return slog.LevelError
case status >= http.StatusBadRequest && status != http.StatusUnauthorized:
return slog.LevelWarn
case method != http.MethodGet && method != http.MethodHead:
return slog.LevelInfo
default:
return slog.LevelDebug
}
}
func requestIDFrom(request *http.Request) string {
if requestID, ok := request.Context().Value(requestIDKey).(string); ok {
return requestID
}
return ""
}
func newRequestID() string {
buffer := make([]byte, 6)
if _, err := rand.Read(buffer); err != nil {
return "unknown"
}
return hex.EncodeToString(buffer)
}
func internalErrorMessage(requestID string) string {
return "Something went wrong on the server (reference " + requestID + "). The server log has the details."
}