Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 12 additions & 6 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)

Expand Down
30 changes: 30 additions & 0 deletions conf/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log/slog"
"os"
"strings"

"github.com/0x2e/fusion/auth"
"github.com/caarlos0/env/v11"
Expand All @@ -25,6 +26,7 @@ type Conf struct {
SecureCookie bool
TLSCert string
TLSKey string
LogLevel slog.Level
}

func Load() (Conf, error) {
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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)
}
}
86 changes: 86 additions & 0 deletions conf/conf_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}