Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type loggerKey struct{}
|
||||
|
||||
// Logger returns the logger the request carries, or the process default
|
||||
// logger outside a request. A handler uses it instead of the package-level
|
||||
// slog functions, so every line it writes carries the request id and the
|
||||
// route it happened on.
|
||||
func Logger(ctx context.Context) *slog.Logger {
|
||||
if logger, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok {
|
||||
return logger
|
||||
}
|
||||
return slog.Default()
|
||||
}
|
||||
|
||||
// WithLogger returns a context carrying logger, for a layer that has
|
||||
// learned something worth adding to every line of the request.
|
||||
func WithLogger(ctx context.Context, logger *slog.Logger) context.Context {
|
||||
return context.WithValue(ctx, loggerKey{}, logger)
|
||||
}
|
||||
|
||||
// RequestLogger gives every request an id, puts a logger carrying it into
|
||||
// the context, answers with the same id in X-Request-Id, and writes one
|
||||
// line when the request finishes.
|
||||
func RequestLogger(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
id := requestID()
|
||||
logger := slog.Default().With("request", id)
|
||||
w.Header().Set("X-Request-Id", id)
|
||||
|
||||
ctx := WithLogger(r.Context(), logger)
|
||||
recorder := &statusWriter{ResponseWriter: w, status: http.StatusOK}
|
||||
started := time.Now()
|
||||
next.ServeHTTP(recorder, r.WithContext(ctx))
|
||||
logger.LogAttrs(ctx, slog.LevelInfo, "request",
|
||||
slog.String("method", r.Method),
|
||||
slog.String("path", r.URL.Path),
|
||||
slog.Int("status", recorder.status),
|
||||
slog.Duration("took", time.Since(started)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// statusWriter records the status code so the access line can carry it.
|
||||
type statusWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
written bool
|
||||
}
|
||||
|
||||
func (s *statusWriter) WriteHeader(code int) {
|
||||
if !s.written {
|
||||
s.status = code
|
||||
s.written = true
|
||||
}
|
||||
s.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (s *statusWriter) Write(body []byte) (int, error) {
|
||||
s.written = true
|
||||
return s.ResponseWriter.Write(body)
|
||||
}
|
||||
|
||||
// Unwrap lets http.ResponseController reach the writer underneath, so a
|
||||
// handler that streams or flushes still can.
|
||||
func (s *statusWriter) Unwrap() http.ResponseWriter { return s.ResponseWriter }
|
||||
|
||||
func requestID() string {
|
||||
// Short enough to read in a log line, wide enough that two live
|
||||
// requests collide with negligible probability.
|
||||
return rand.Text()[:16]
|
||||
}
|
||||
Reference in New Issue
Block a user