Add structured logging and better error handling
This commit is contained in:
1 parent
db4e3c9611
commit
68c7dd76f2
31 files changed
+640
-109
No files matched your search
@@ -0,0 +1,115 @@
|
||||
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."
|
||||
}
|
||||
Reference in new issue
Block a user