diff --git a/.env.example b/.env.example index 3f67837..6b9799c 100644 --- a/.env.example +++ b/.env.example @@ -20,3 +20,8 @@ SECURE_COOKIE=false # If you are using a reverse proxy like Nginx to handle HTTPS, please leave these empty. TLS_CERT="" TLS_KEY="" + +# LOG_LEVEL controls the minimum level of logs emitted by the server. +# Accepted values: DEBUG, INFO, WARN (or WARNING), ERROR +# Default: INFO (invalid values fall back to INFO) +LOG_LEVEL=INFO diff --git a/README.md b/README.md index b2a8d35..e6cf715 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ services: - "127.0.0.1:8080:8080" environment: - PASSWORD=fusion + # Set log level if desired (DEBUG, INFO, WARN, ERROR) + # - LOG_LEVEL=INFO restart: "unless-stopped" volumes: # Change `./data` to where you want the files stored diff --git a/cmd/server/server.go b/cmd/server/server.go index d7c51a9..8118357 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -19,6 +19,13 @@ func main() { })) slog.SetDefault(l) + config, err := conf.Load() + if err != nil { + slog.Error("failed to load configuration", "error", err) + return + } + + // Reconfigure logger based on configured LOG_LEVEL. if conf.Debug { l := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelDebug, @@ -31,12 +38,11 @@ func main() { return } }() - } - - config, err := conf.Load() - if err != nil { - slog.Error("failed to load configuration", "error", err) - return + } else { + l := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: config.LogLevel, + })) + slog.SetDefault(l) } repo.Init(config.DB) diff --git a/conf/conf.go b/conf/conf.go index efc98fb..6eff857 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "os" + "strings" "github.com/0x2e/fusion/auth" "github.com/caarlos0/env/v11" @@ -25,6 +26,7 @@ type Conf struct { SecureCookie bool TLSCert string TLSKey string + LogLevel slog.Level } func Load() (Conf, error) { @@ -44,6 +46,7 @@ func Load() (Conf, error) { SecureCookie bool `env:"SECURE_COOKIE" envDefault:"false"` TLSCert string `env:"TLS_CERT"` TLSKey string `env:"TLS_KEY"` + LogLevel string `env:"LOG_LEVEL" envDefault:"INFO"` } if err := env.Parse(&conf); err != nil { return Conf{}, err @@ -66,6 +69,13 @@ func Load() (Conf, error) { conf.SecureCookie = true } + // Parse log level; default to INFO on error + level, err := ParseLogLevel(conf.LogLevel) + if err != nil { + slog.Warn(fmt.Sprintf("invalid LOG_LEVEL '%s', defaulting to INFO", conf.LogLevel)) + level = slog.LevelInfo + } + return Conf{ Host: conf.Host, Port: conf.Port, @@ -74,5 +84,25 @@ func Load() (Conf, error) { SecureCookie: conf.SecureCookie, TLSCert: conf.TLSCert, TLSKey: conf.TLSKey, + LogLevel: level, }, nil } + +// ParseLogLevel validates and converts a string log level to slog.Level. +// Accepted values (case-insensitive): DEBUG, INFO, WARN, WARNING, ERROR. +// Returns an error for invalid values. +func ParseLogLevel(level string) (slog.Level, error) { + normalized := strings.ToUpper(strings.TrimSpace(level)) + switch normalized { + case "DEBUG": + return slog.LevelDebug, nil + case "", "INFO": + return slog.LevelInfo, nil + case "WARN", "WARNING": + return slog.LevelWarn, nil + case "ERROR": + return slog.LevelError, nil + default: + return slog.LevelInfo, fmt.Errorf("invalid log level: %s", level) + } +} diff --git a/conf/conf_test.go b/conf/conf_test.go new file mode 100644 index 0000000..deb502a --- /dev/null +++ b/conf/conf_test.go @@ -0,0 +1,86 @@ +package conf + +import ( + "log/slog" + "os" + "testing" +) + +func TestParseLogLevel(t *testing.T) { + tests := []struct { + input string + expected slog.Level + wantErr bool + }{ + {"DEBUG", slog.LevelDebug, false}, + {"debug", slog.LevelDebug, false}, + {"INFO", slog.LevelInfo, false}, + {"", slog.LevelInfo, false}, + {"warn", slog.LevelWarn, false}, + {"WARNING", slog.LevelWarn, false}, + {"ERROR", slog.LevelError, false}, + {"TRACE", slog.LevelInfo, true}, + {"invalid", slog.LevelInfo, true}, + } + + for _, tc := range tests { + lvl, err := ParseLogLevel(tc.input) + if tc.wantErr { + if err == nil { + t.Fatalf("ParseLogLevel(%q) expected error, got nil", tc.input) + } + } else { + if err != nil { + t.Fatalf("ParseLogLevel(%q) unexpected error: %v", tc.input, err) + } + if lvl != tc.expected { + t.Fatalf("ParseLogLevel(%q) expected %v, got %v", tc.input, tc.expected, lvl) + } + } + } +} + +func TestLoad_UsesLogLevelFromEnv(t *testing.T) { + // Save and restore environment + original := os.Getenv("LOG_LEVEL") + defer func() { + _ = os.Setenv("LOG_LEVEL", original) + }() + + // Ensure other required envs are not interfering + _ = os.Unsetenv("TLS_CERT") + _ = os.Unsetenv("TLS_KEY") + + if err := os.Setenv("LOG_LEVEL", "DEBUG"); err != nil { + t.Fatalf("failed to set env: %v", err) + } + cfg, err := Load() + if err != nil { + t.Fatalf("Load() error: %v", err) + } + if cfg.LogLevel != slog.LevelDebug { + t.Fatalf("expected slog.LevelDebug, got %v", cfg.LogLevel) + } + + if err := os.Setenv("LOG_LEVEL", "WARNING"); err != nil { + t.Fatalf("failed to set env: %v", err) + } + cfg, err = Load() + if err != nil { + t.Fatalf("Load() error: %v", err) + } + if cfg.LogLevel != slog.LevelWarn { + t.Fatalf("expected slog.LevelWarn, got %v", cfg.LogLevel) + } + + if err := os.Setenv("LOG_LEVEL", "invalid"); err != nil { + t.Fatalf("failed to set env: %v", err) + } + cfg, err = Load() + if err != nil { + t.Fatalf("Load() error: %v", err) + } + if cfg.LogLevel != slog.LevelInfo { + t.Fatalf("expected default slog.LevelInfo on invalid, got %v", cfg.LogLevel) + } +}