116 lines
2.9 KiB
Go
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."
|
|
}
|