diff --git a/backend/go.mod b/backend/go.mod index 8927406e..183a15e6 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -10,6 +10,7 @@ require ( github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-smtp v0.21.3 github.com/fxamacker/cbor/v2 v2.9.0 + github.com/gin-contrib/slog v1.1.0 github.com/gin-gonic/gin v1.10.1 github.com/glebarez/go-sqlite v1.22.0 github.com/glebarez/sqlite v1.11.0 @@ -29,7 +30,6 @@ require ( github.com/mileusna/useragent v1.3.5 github.com/orandin/slog-gorm v1.4.0 github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8 - github.com/samber/slog-gin v1.15.1 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/contrib/bridges/otelslog v0.12.0 @@ -45,6 +45,7 @@ require ( go.opentelemetry.io/otel/trace v1.37.0 golang.org/x/crypto v0.41.0 golang.org/x/image v0.30.0 + golang.org/x/sync v0.16.0 golang.org/x/text v0.28.0 golang.org/x/time v0.12.0 gorm.io/driver/postgres v1.6.0 @@ -135,7 +136,6 @@ require ( golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.35.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect diff --git a/backend/go.sum b/backend/go.sum index 45d24aba..a0b3a5d4 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -56,6 +56,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/slog v1.1.0 h1:K9MVNrETT6r/C3u2Aheer/gxwVeVqrGL0hXlsmv3fm4= +github.com/gin-contrib/slog v1.1.0/go.mod h1:PvNXQVXcVOAaaiJR84LV1/xlQHIaXi9ygEXyBkmjdkY= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= @@ -241,8 +243,6 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/samber/slog-gin v1.15.1 h1:jsnfr+S5HQPlz9pFPA3tOmKW7wN/znyZiE6hncucrTM= -github.com/samber/slog-gin v1.15.1/go.mod h1:mPAEinK/g2jPLauuWO11m3Q0Ca7aG4k9XjXjXY8IhMQ= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= diff --git a/backend/internal/bootstrap/db_bootstrap.go b/backend/internal/bootstrap/db_bootstrap.go index b1b5f04a..c0145ab5 100644 --- a/backend/internal/bootstrap/db_bootstrap.go +++ b/backend/internal/bootstrap/db_bootstrap.go @@ -422,17 +422,18 @@ func getGormLogger() gormLogger.Interface { slogGorm.WithErrorField("error"), ) - if common.EnvConfig.AppEnv == "production" { - loggerOpts = append(loggerOpts, - slogGorm.SetLogLevel(slogGorm.DefaultLogType, slog.LevelWarn), - slogGorm.WithIgnoreTrace(), - ) - } else { + if common.EnvConfig.LogLevel == "debug" { loggerOpts = append(loggerOpts, slogGorm.SetLogLevel(slogGorm.DefaultLogType, slog.LevelDebug), slogGorm.WithRecordNotFoundError(), slogGorm.WithTraceAll(), ) + + } else { + loggerOpts = append(loggerOpts, + slogGorm.SetLogLevel(slogGorm.DefaultLogType, slog.LevelWarn), + slogGorm.WithIgnoreTrace(), + ) } return slogGorm.New(loggerOpts...) diff --git a/backend/internal/bootstrap/observability_boostrap.go b/backend/internal/bootstrap/observability_boostrap.go index d335567d..7228b36b 100644 --- a/backend/internal/bootstrap/observability_boostrap.go +++ b/backend/internal/bootstrap/observability_boostrap.go @@ -8,6 +8,8 @@ import ( "os" "time" + sloggin "github.com/gin-contrib/slog" + "github.com/lmittmann/tint" "github.com/mattn/go-isatty" "go.opentelemetry.io/contrib/bridges/otelslog" @@ -89,28 +91,19 @@ func initOtelLogging(ctx context.Context, resource *resource.Resource) error { return fmt.Errorf("failed to initialize OpenTelemetry log exporter: %w", err) } - level := slog.LevelDebug - if common.EnvConfig.AppEnv == "production" { - level = slog.LevelInfo - } + level, _ := sloggin.ParseLevel(common.EnvConfig.LogLevel) // Create the handler var handler slog.Handler - switch { - case common.EnvConfig.LogJSON: - // Log as JSON if configured + if common.EnvConfig.LogJSON { handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: level, }) - case isatty.IsTerminal(os.Stdout.Fd()): - // Enable colors if we have a TTY + } else { handler = tint.NewHandler(os.Stdout, &tint.Options{ - TimeFormat: time.StampMilli, + TimeFormat: time.Stamp, Level: level, - }) - default: - handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ - Level: level, + NoColor: !isatty.IsTerminal(os.Stdout.Fd()), }) } diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go index 91cd0a1a..78ed56a9 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -12,8 +12,8 @@ import ( "strings" "time" + sloggin "github.com/gin-contrib/slog" "github.com/gin-gonic/gin" - sloggin "github.com/samber/slog-gin" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" "golang.org/x/time/rate" "gorm.io/gorm" @@ -49,30 +49,8 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) { gin.SetMode(gin.TestMode) } - // do not log these URLs - loggerSkipPathsPrefix := []string{ - "GET /application-configuration/logo", - "GET /application-configuration/background-image", - "GET /application-configuration/favicon", - "GET /_app", - "GET /fonts", - "GET /healthz", - "HEAD /healthz", - } - r := gin.New() - r.Use(sloggin.NewWithConfig(slog.Default(), sloggin.Config{ - Filters: []sloggin.Filter{ - func(c *gin.Context) bool { - for _, prefix := range loggerSkipPathsPrefix { - if strings.HasPrefix(c.Request.Method+" "+c.Request.URL.String(), prefix) { - return false - } - } - return true - }, - }, - })) + initLogger(r) if !common.EnvConfig.TrustProxy { _ = r.SetTrustedProxies(nil) @@ -200,3 +178,29 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) { return runFn, nil } + +func initLogger(r *gin.Engine) { + loggerSkipPathsPrefix := []string{ + "GET /api/application-configuration/logo", + "GET /api/application-configuration/background-image", + "GET /api/application-configuration/favicon", + "GET /_app", + "GET /fonts", + "GET /healthz", + "HEAD /healthz", + } + + r.Use(sloggin.SetLogger( + sloggin.WithLogger(func(_ *gin.Context, _ *slog.Logger) *slog.Logger { + return slog.Default() + }), + sloggin.WithSkipper(func(c *gin.Context) bool { + for _, prefix := range loggerSkipPathsPrefix { + if strings.HasPrefix(c.Request.Method+" "+c.Request.URL.String(), prefix) { + return true + } + } + return false + }), + )) +} diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index 6d844c68..041f1b09 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/caarlos0/env/v11" + sloggin "github.com/gin-contrib/slog" _ "github.com/joho/godotenv/autoload" ) @@ -32,6 +33,7 @@ const ( type EnvConfigSchema struct { AppEnv string `env:"APP_ENV"` + LogLevel string `env:"LOG_LEVEL"` AppURL string `env:"APP_URL"` DbProvider DbProvider `env:"DB_PROVIDER"` DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"` @@ -70,6 +72,7 @@ func init() { func defaultConfig() EnvConfigSchema { return EnvConfigSchema{ AppEnv: "production", + LogLevel: "info", DbProvider: "sqlite", DbConnectionString: "", UploadPath: "data/uploads", @@ -115,6 +118,11 @@ func parseEnvConfig() error { } // Validate the environment variables + EnvConfig.LogLevel = strings.ToLower(EnvConfig.LogLevel) + if _, err := sloggin.ParseLevel(EnvConfig.LogLevel); err != nil { + return errors.New("invalid LOG_LEVEL value. Must be 'debug', 'info', 'warn' or 'error'") + } + switch EnvConfig.DbProvider { case DbProviderSqlite: if EnvConfig.DbConnectionString == "" {