Add structured logging with zerolog
- Implement zerolog logger package with configurable levels - Add contextual logging middleware (request_id, user_id, method, path, status, duration) - Support both JSON (production) and human-readable (development) formats - Add LOG_LEVEL and ENVIRONMENT configuration variables - Replace standard log calls with structured logger throughout application - Integrate logger in HTTP router and auth middleware
This commit is contained in:
@@ -23,6 +23,8 @@ type Config struct {
|
||||
SupportEmail string
|
||||
SendWelcomeEmail string
|
||||
RegistrationMode string
|
||||
LogLevel string
|
||||
Environment string
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
@@ -44,6 +46,8 @@ func Load() (*Config, error) {
|
||||
SupportEmail: getEnvOrDefault("SUPPORT_EMAIL", "contact@apocapoc.app"),
|
||||
SendWelcomeEmail: getEnvOrDefault("SEND_WELCOME_EMAIL", "false"),
|
||||
RegistrationMode: getEnvOrDefault("REGISTRATION_MODE", "open"),
|
||||
LogLevel: getEnvOrDefault("LOG_LEVEL", "info"),
|
||||
Environment: getEnvOrDefault("ENVIRONMENT", "production"),
|
||||
}
|
||||
|
||||
if cfg.DBPath == "" {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"apocapoc-api/internal/infrastructure/auth"
|
||||
"apocapoc-api/internal/infrastructure/logger"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
@@ -35,6 +36,7 @@ func AuthMiddleware(jwtService *auth.JWTService) func(http.Handler) http.Handler
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID)
|
||||
ctx = logger.AddUserID(ctx, claims.UserID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"apocapoc-api/internal/i18n"
|
||||
"apocapoc-api/internal/infrastructure/auth"
|
||||
"apocapoc-api/internal/infrastructure/logger"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
@@ -19,7 +20,7 @@ import (
|
||||
func NewRouter(appURL string, habitHandlers *HabitHandlers, authHandlers *AuthHandlers, statsHandlers *StatsHandlers, healthHandlers *HealthHandlers, userHandlers *UserHandlers, exportHandlers *ExportHandlers, jwtService *auth.JWTService, translator *i18n.Translator) *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(logger.Middleware)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(i18n.LanguageMiddleware(translator))
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
)
|
||||
|
||||
var Log zerolog.Logger
|
||||
|
||||
type Config struct {
|
||||
Level string
|
||||
Environment string
|
||||
}
|
||||
|
||||
func Init(config Config) {
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
zerolog.TimeFieldFormat = time.RFC3339
|
||||
|
||||
level := parseLogLevel(config.Level)
|
||||
zerolog.SetGlobalLevel(level)
|
||||
|
||||
var output io.Writer = os.Stdout
|
||||
|
||||
if config.Environment == "development" {
|
||||
output = zerolog.ConsoleWriter{
|
||||
Out: os.Stdout,
|
||||
TimeFormat: "15:04:05",
|
||||
NoColor: false,
|
||||
}
|
||||
}
|
||||
|
||||
Log = zerolog.New(output).
|
||||
With().
|
||||
Timestamp().
|
||||
Caller().
|
||||
Logger()
|
||||
|
||||
Log.Info().
|
||||
Str("level", level.String()).
|
||||
Str("environment", config.Environment).
|
||||
Msg("Logger initialized")
|
||||
}
|
||||
|
||||
func parseLogLevel(level string) zerolog.Level {
|
||||
switch strings.ToLower(level) {
|
||||
case "debug":
|
||||
return zerolog.DebugLevel
|
||||
case "info":
|
||||
return zerolog.InfoLevel
|
||||
case "warn", "warning":
|
||||
return zerolog.WarnLevel
|
||||
case "error":
|
||||
return zerolog.ErrorLevel
|
||||
case "fatal":
|
||||
return zerolog.FatalLevel
|
||||
case "panic":
|
||||
return zerolog.PanicLevel
|
||||
default:
|
||||
return zerolog.InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
func Debug() *zerolog.Event {
|
||||
return Log.Debug()
|
||||
}
|
||||
|
||||
func Info() *zerolog.Event {
|
||||
return Log.Info()
|
||||
}
|
||||
|
||||
func Warn() *zerolog.Event {
|
||||
return Log.Warn()
|
||||
}
|
||||
|
||||
func Error() *zerolog.Event {
|
||||
return Log.Error()
|
||||
}
|
||||
|
||||
func Fatal() *zerolog.Event {
|
||||
return Log.Fatal()
|
||||
}
|
||||
|
||||
func With() zerolog.Context {
|
||||
return Log.With()
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
RequestIDKey contextKey = "request_id"
|
||||
UserIDKey contextKey = "user_id"
|
||||
)
|
||||
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
size int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(status int) {
|
||||
rw.status = status
|
||||
rw.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Write(b []byte) (int, error) {
|
||||
size, err := rw.ResponseWriter.Write(b)
|
||||
rw.size += size
|
||||
return size, err
|
||||
}
|
||||
|
||||
func Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
requestID := uuid.New().String()
|
||||
ctx := context.WithValue(r.Context(), RequestIDKey, requestID)
|
||||
|
||||
logger := Log.With().
|
||||
Str("request_id", requestID).
|
||||
Str("method", r.Method).
|
||||
Str("path", r.URL.Path).
|
||||
Str("remote_addr", r.RemoteAddr).
|
||||
Str("user_agent", r.UserAgent()).
|
||||
Logger()
|
||||
|
||||
ctx = logger.WithContext(ctx)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
rw := &responseWriter{
|
||||
ResponseWriter: w,
|
||||
status: http.StatusOK,
|
||||
}
|
||||
|
||||
next.ServeHTTP(rw, r)
|
||||
|
||||
duration := time.Since(start)
|
||||
|
||||
event := logger.Info()
|
||||
if rw.status >= 400 && rw.status < 500 {
|
||||
event = logger.Warn()
|
||||
} else if rw.status >= 500 {
|
||||
event = logger.Error()
|
||||
}
|
||||
|
||||
event.
|
||||
Int("status", rw.status).
|
||||
Int("size", rw.size).
|
||||
Dur("duration", duration).
|
||||
Msg("HTTP request")
|
||||
})
|
||||
}
|
||||
|
||||
func FromContext(ctx context.Context) *zerolog.Logger {
|
||||
logger := zerolog.Ctx(ctx)
|
||||
if logger == nil || logger.GetLevel() == zerolog.Disabled {
|
||||
return &Log
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
func AddUserID(ctx context.Context, userID string) context.Context {
|
||||
logger := FromContext(ctx)
|
||||
updatedLogger := logger.With().Str("user_id", userID).Logger()
|
||||
return updatedLogger.WithContext(ctx)
|
||||
}
|
||||
Reference in New Issue
Block a user