Implement structured logging and enhance middleware with context-aware logging
This commit is contained in:
parent
bfdf7bf7d2
commit
2e4b2aba45
36
cmd/start.go
36
cmd/start.go
@ -1,11 +1,14 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.coopcloud.tech/wiki-cafe/member-console/internal/auth"
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/auth"
|
||||||
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/logging"
|
||||||
"git.coopcloud.tech/wiki-cafe/member-console/internal/middleware"
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/middleware"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@ -20,8 +23,18 @@ var startCmd = &cobra.Command{
|
|||||||
The server listens on port 8080 by default, unless a different port is specified using the --port flag.`,
|
The server listens on port 8080 by default, unless a different port is specified using the --port flag.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// Retrieve the port value from Viper
|
// Create base context for the application
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Retrieve the configuration values from Viper
|
||||||
port := viper.GetString("port")
|
port := viper.GetString("port")
|
||||||
|
env := viper.GetString("env")
|
||||||
|
|
||||||
|
// Set up structured logging
|
||||||
|
logger := logging.SetupLogger(env)
|
||||||
|
|
||||||
|
// Store logger in context
|
||||||
|
ctx = logging.WithContext(ctx, logger)
|
||||||
|
|
||||||
// Create a new HTTP request router
|
// Create a new HTTP request router
|
||||||
httpRequestRouter := http.NewServeMux()
|
httpRequestRouter := http.NewServeMux()
|
||||||
@ -29,7 +42,8 @@ var startCmd = &cobra.Command{
|
|||||||
// Set up authentication
|
// Set up authentication
|
||||||
authConfig, err := auth.Setup()
|
authConfig, err := auth.Setup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to set up authentication: %v", err)
|
logger.Error("failed to set up authentication", slog.Any("error", err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register auth handlers
|
// Register auth handlers
|
||||||
@ -42,7 +56,7 @@ var startCmd = &cobra.Command{
|
|||||||
middleware.RequestID(), // Generate a unique request ID
|
middleware.RequestID(), // Generate a unique request ID
|
||||||
middleware.MaxBodySize(1024*1024), // 1MB size limit
|
middleware.MaxBodySize(1024*1024), // 1MB size limit
|
||||||
middleware.SecureHeaders, // Set secure headers
|
middleware.SecureHeaders, // Set secure headers
|
||||||
middleware.Logging, // Log requests
|
middleware.Logging, // Log requests with structured logging
|
||||||
authConfig.Middleware(), // OIDC authentication middleware
|
authConfig.Middleware(), // OIDC authentication middleware
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -54,12 +68,22 @@ var startCmd = &cobra.Command{
|
|||||||
WriteTimeout: 4 * time.Second,
|
WriteTimeout: 4 * time.Second,
|
||||||
IdleTimeout: 8 * time.Second,
|
IdleTimeout: 8 * time.Second,
|
||||||
MaxHeaderBytes: 1024 * 1024, // 1MB
|
MaxHeaderBytes: 1024 * 1024, // 1MB
|
||||||
|
BaseContext: func(_ net.Listener) context.Context { return ctx }, // Pass base context to all requests
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve the components directory
|
// Serve the components directory
|
||||||
httpRequestRouter.Handle("/", http.FileServer(http.Dir("./components")))
|
httpRequestRouter.Handle("/", http.FileServer(http.Dir("./components")))
|
||||||
log.Println("Starting server on port", port)
|
|
||||||
log.Fatal(server.ListenAndServe())
|
// Log server startup with structured logging
|
||||||
|
logger.Info("starting server",
|
||||||
|
slog.String("port", port),
|
||||||
|
slog.String("environment", env),
|
||||||
|
slog.String("address", "http://localhost:"+port))
|
||||||
|
|
||||||
|
// Start server and log any errors
|
||||||
|
if err := server.ListenAndServe(); err != nil {
|
||||||
|
logger.Error("server error", slog.Any("error", err))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
85
internal/logging/logging.go
Normal file
85
internal/logging/logging.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loggerKey is the context key for logger values
|
||||||
|
type loggerKey struct{}
|
||||||
|
|
||||||
|
// AppLogger is the application-wide logger instance
|
||||||
|
var AppLogger *slog.Logger
|
||||||
|
|
||||||
|
// SetupLogger initializes the structured logger based on environment settings
|
||||||
|
func SetupLogger(env string) *slog.Logger {
|
||||||
|
// Determine log level
|
||||||
|
logLevel := slog.LevelInfo
|
||||||
|
if levelStr := os.Getenv("LOG_LEVEL"); levelStr != "" {
|
||||||
|
if err := logLevel.UnmarshalText([]byte(levelStr)); err != nil {
|
||||||
|
panic(fmt.Sprintf("invalid log level: %s", levelStr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler slog.Handler
|
||||||
|
|
||||||
|
// Configure handler based on environment
|
||||||
|
if env == "development" {
|
||||||
|
// In development, use text output with source information if in debug mode
|
||||||
|
opts := &slog.HandlerOptions{
|
||||||
|
Level: logLevel,
|
||||||
|
AddSource: logLevel == slog.LevelDebug,
|
||||||
|
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||||
|
// Format time for better readability in dev mode
|
||||||
|
if a.Key == slog.TimeKey {
|
||||||
|
if t, ok := a.Value.Any().(time.Time); ok {
|
||||||
|
return slog.String(slog.TimeKey, t.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
}
|
||||||
|
handler = slog.NewTextHandler(os.Stdout, opts)
|
||||||
|
} else {
|
||||||
|
// In production, use JSON output
|
||||||
|
handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
|
Level: logLevel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and set the default logger
|
||||||
|
logger := slog.New(handler)
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
AppLogger = logger
|
||||||
|
|
||||||
|
logger.Info("logger initialized",
|
||||||
|
slog.String("environment", env),
|
||||||
|
slog.String("level", logLevel.String()))
|
||||||
|
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromContext retrieves a logger from the given context
|
||||||
|
// If no logger exists in the context, returns the default logger
|
||||||
|
func FromContext(ctx context.Context) *slog.Logger {
|
||||||
|
if ctx == nil {
|
||||||
|
return AppLogger
|
||||||
|
}
|
||||||
|
if logger, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok {
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
return AppLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContext stores a logger in the given context
|
||||||
|
func WithContext(ctx context.Context, logger *slog.Logger) context.Context {
|
||||||
|
return context.WithValue(ctx, loggerKey{}, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithValues returns a new logger with additional context values
|
||||||
|
func WithValues(logger *slog.Logger, attrs ...any) *slog.Logger {
|
||||||
|
return logger.With(attrs...)
|
||||||
|
}
|
@ -1,9 +1,11 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
type wrappedWriter struct {
|
type wrappedWriter struct {
|
||||||
@ -16,21 +18,48 @@ func (w *wrappedWriter) WriteHeader(statusCode int) {
|
|||||||
w.statusCode = statusCode
|
w.statusCode = statusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logging is a middleware function that logs the request method, URL path, and status code
|
// Logging is a middleware function that logs requests with structured logging
|
||||||
func Logging(next http.Handler) http.Handler {
|
func Logging(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
// Prepare wrapped writer to capture status code
|
||||||
wrapped := &wrappedWriter{
|
wrapped := &wrappedWriter{
|
||||||
ResponseWriter: w,
|
ResponseWriter: w,
|
||||||
statusCode: http.StatusOK,
|
statusCode: http.StatusOK,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get request ID from header (set by RequestID middleware)
|
// Get request ID from context
|
||||||
requestID := r.Header.Get(RequestIDHeader)
|
requestID := GetRequestID(r.Context())
|
||||||
|
|
||||||
|
// Get logger from the application and add request information
|
||||||
|
logger := logging.FromContext(r.Context())
|
||||||
|
reqLogger := logging.WithValues(logger,
|
||||||
|
slog.String("request_id", requestID),
|
||||||
|
slog.String("method", r.Method),
|
||||||
|
slog.String("path", r.URL.Path),
|
||||||
|
slog.String("remote_ip", r.RemoteAddr),
|
||||||
|
slog.String("user_agent", r.UserAgent()),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store the request-specific logger in context
|
||||||
|
ctx := logging.WithContext(r.Context(), reqLogger)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
// Log request start if in debug mode
|
||||||
|
reqLogger.Debug("request started")
|
||||||
|
|
||||||
|
// Process the request with updated context
|
||||||
next.ServeHTTP(wrapped, r)
|
next.ServeHTTP(wrapped, r)
|
||||||
|
|
||||||
log.Printf("[%s] %d %s %s %v", requestID, wrapped.statusCode, r.Method, r.URL.Path, time.Since(start))
|
// Calculate duration
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
// Log request completion with status and duration
|
||||||
|
reqLogger.Info("request completed",
|
||||||
|
slog.Int("status", wrapped.statusCode),
|
||||||
|
slog.Duration("duration", duration),
|
||||||
|
slog.String("duration_human", duration.String()),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -1,23 +1,36 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Recovery middleware to catch panics, log them, and return a 500 error
|
// Recovery middleware catches panics and logs them with structured logging
|
||||||
func Recovery() Middleware {
|
func Recovery() Middleware {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
// Log the stack trace
|
// Get logger from context
|
||||||
log.Printf("PANIC: %v\n%s", err, debug.Stack())
|
logger := logging.FromContext(r.Context())
|
||||||
|
|
||||||
// Return a 500 Internal Server Error response
|
// Get request ID from context
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
requestID := GetRequestID(r.Context())
|
||||||
w.Write([]byte("Internal Server Error"))
|
|
||||||
|
// Log the panic with stack trace
|
||||||
|
logger.Error("panic recovered",
|
||||||
|
slog.String("request_id", requestID),
|
||||||
|
slog.String("method", r.Method),
|
||||||
|
slog.String("path", r.URL.Path),
|
||||||
|
slog.Any("error", err),
|
||||||
|
slog.String("stack", string(debug.Stack())),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return 500 Internal Server Error to the client
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -2,16 +2,15 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RequestIDKey is the context key for the request ID
|
// requestIDKey is the context key for request ID
|
||||||
type requestIDKey struct{}
|
type requestIDKey struct{}
|
||||||
|
|
||||||
// RequestIDHeader is the header key for the request ID
|
// RequestIDHeader is the HTTP header for request ID
|
||||||
const RequestIDHeader = "X-Request-ID"
|
const RequestIDHeader = "X-Request-ID"
|
||||||
|
|
||||||
// RequestID middleware generates a unique ID for each request
|
// RequestID middleware generates a unique ID for each request
|
||||||
@ -21,30 +20,26 @@ func RequestID() Middleware {
|
|||||||
// Check if request already has an ID
|
// Check if request already has an ID
|
||||||
requestID := r.Header.Get(RequestIDHeader)
|
requestID := r.Header.Get(RequestIDHeader)
|
||||||
if requestID == "" {
|
if requestID == "" {
|
||||||
// Generate a new UUID if none exists
|
// Generate a new UUID for the request if not present
|
||||||
requestID = uuid.New().String()
|
requestID = uuid.New().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the request ID to the response headers
|
// Add request ID to response headers
|
||||||
w.Header().Set(RequestIDHeader, requestID)
|
w.Header().Set(RequestIDHeader, requestID)
|
||||||
|
|
||||||
// Store the request ID in the request context
|
// Store request ID in context
|
||||||
ctx := context.WithValue(r.Context(), requestIDKey{}, requestID)
|
ctx := context.WithValue(r.Context(), requestIDKey{}, requestID)
|
||||||
|
|
||||||
// Log the request with its ID
|
// Call next handler with updated context
|
||||||
log.Printf("[%s] %s %s", requestID, r.Method, r.URL.Path)
|
|
||||||
|
|
||||||
// Call the next handler with the updated context
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRequestID retrieves the request ID from the context
|
// GetRequestID retrieves the request ID from context
|
||||||
func GetRequestID(ctx context.Context) string {
|
func GetRequestID(ctx context.Context) string {
|
||||||
id, ok := ctx.Value(requestIDKey{}).(string)
|
if id, ok := ctx.Value(requestIDKey{}).(string); ok {
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return id
|
return id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user