Add structured logging with request ID middleware
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
// RequestIDKey is the context key for the request ID value.
|
||||||
|
const RequestIDKey contextKey = "req_id"
|
||||||
|
|
||||||
|
// contextHandler wraps a slog.Handler, extracting req_id from the
|
||||||
|
// context.Context and appending it to every log record.
|
||||||
|
type contextHandler struct {
|
||||||
|
underlying slog.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContextHandler wraps an slog.Handler so that records get a req_id
|
||||||
|
// attribute extracted from context.Context (if present).
|
||||||
|
func NewContextHandler(h slog.Handler) slog.Handler {
|
||||||
|
return &contextHandler{underlying: h}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *contextHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||||
|
return h.underlying.Enabled(ctx, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *contextHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||||
|
if reqID, ok := ctx.Value(RequestIDKey).(string); ok && reqID != "" {
|
||||||
|
r.AddAttrs(slog.String("req_id", reqID))
|
||||||
|
}
|
||||||
|
return h.underlying.Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *contextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
return &contextHandler{underlying: h.underlying.WithAttrs(attrs)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *contextHandler) WithGroup(name string) slog.Handler {
|
||||||
|
return &contextHandler{underlying: h.underlying.WithGroup(name)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRequestID returns a child context carrying a request ID.
|
||||||
|
func WithRequestID(ctx context.Context, id string) context.Context {
|
||||||
|
return context.WithValue(ctx, RequestIDKey, id)
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/dhao2001/mygo/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewLogger creates a slog.Logger with dual output: terminal (stderr) and
|
||||||
|
// optional file. Terminal and file outputs use independent log levels.
|
||||||
|
// On file-open failure, a warning is emitted to stderr and the logger
|
||||||
|
// continues without file output.
|
||||||
|
func NewLogger(cfg config.LogConfig) *slog.Logger {
|
||||||
|
terminalLevel, _ := config.ParseLogLevel(cfg.Level)
|
||||||
|
fileLevel, _ := config.ParseLogLevel(cfg.FileLevel)
|
||||||
|
|
||||||
|
terminalHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
|
Level: terminalLevel,
|
||||||
|
})
|
||||||
|
|
||||||
|
handlers := []slog.Handler{terminalHandler}
|
||||||
|
|
||||||
|
if cfg.FilePath != "" {
|
||||||
|
f, err := os.OpenFile(cfg.FilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
slog.New(terminalHandler).Warn("failed to open log file, continuing without file output",
|
||||||
|
"path", cfg.FilePath, "error", err)
|
||||||
|
} else {
|
||||||
|
fileHandler := slog.NewTextHandler(f, &slog.HandlerOptions{
|
||||||
|
Level: fileLevel,
|
||||||
|
})
|
||||||
|
handlers = append(handlers, fileHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := NewContextHandler(&multiHandler{handlers: handlers})
|
||||||
|
return slog.New(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiHandler fans out to multiple slog.Handler implementations.
|
||||||
|
type multiHandler struct {
|
||||||
|
handlers []slog.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||||
|
for _, h := range m.handlers {
|
||||||
|
if h.Enabled(ctx, level) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multiHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||||
|
for _, h := range m.handlers {
|
||||||
|
if h.Enabled(ctx, r.Level) {
|
||||||
|
if err := h.Handle(ctx, r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
clones := make([]slog.Handler, len(m.handlers))
|
||||||
|
for i, h := range m.handlers {
|
||||||
|
clones[i] = h.WithAttrs(attrs)
|
||||||
|
}
|
||||||
|
return &multiHandler{handlers: clones}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multiHandler) WithGroup(name string) slog.Handler {
|
||||||
|
clones := make([]slog.Handler, len(m.handlers))
|
||||||
|
for i, h := range m.handlers {
|
||||||
|
clones[i] = h.WithGroup(name)
|
||||||
|
}
|
||||||
|
return &multiHandler{handlers: clones}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
mygolog "github.com/dhao2001/mygo/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequestID returns a Gin middleware that ensures every request has a
|
||||||
|
// request ID. If the client sends X-Request-ID, it is reused; otherwise a
|
||||||
|
// new UUID v4 is generated. The ID is injected into both the Go
|
||||||
|
// context.Context (for slog) and the Gin context (for handler access).
|
||||||
|
func RequestID() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
reqID := c.GetHeader("X-Request-ID")
|
||||||
|
if reqID == "" {
|
||||||
|
reqID = uuid.NewString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject into Go context for slog.*Context calls.
|
||||||
|
c.Request = c.Request.WithContext(mygolog.WithRequestID(c.Request.Context(), reqID))
|
||||||
|
|
||||||
|
// Also set in Gin context for direct access.
|
||||||
|
c.Set("req_id", reqID)
|
||||||
|
c.Header("X-Request-ID", reqID)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user