diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go index e7fc445..cd7dc2c 100644 --- a/server/cmd/server/main.go +++ b/server/cmd/server/main.go @@ -5,6 +5,7 @@ import ( "log" "novamd/internal/app" + "novamd/internal/logging" ) // @title NovaMD API @@ -23,6 +24,10 @@ func main() { log.Fatal("Failed to load configuration:", err) } + // Setup logging + logging.Setup(cfg.LogLevel) + logging.Debug("Configuration loaded", "config", logging.Redact(cfg)) + // Initialize and start server options, err := app.DefaultOptions(cfg) if err != nil { @@ -32,7 +37,7 @@ func main() { server := app.NewServer(options) defer func() { if err := server.Close(); err != nil { - log.Println("Error closing server:", err) + logging.Error("Failed to close server:", err) } }() diff --git a/server/internal/app/config.go b/server/internal/app/config.go index 08a4815..0b70a82 100644 --- a/server/internal/app/config.go +++ b/server/internal/app/config.go @@ -19,10 +19,10 @@ type Config struct { RootURL string Domain string CORSOrigins []string - AdminEmail string - AdminPassword string - EncryptionKey string - JWTSigningKey string + AdminEmail string `log:"redact"` + AdminPassword string `log:"redact"` + EncryptionKey string `log:"redact"` + JWTSigningKey string `log:"redact"` RateLimitRequests int RateLimitWindow time.Duration IsDevelopment bool @@ -58,46 +58,37 @@ func (c *Config) validate() error { // LoadConfig creates a new Config instance with values from environment variables func LoadConfig() (*Config, error) { - logging.Info("loading configuration from environment variables") config := DefaultConfig() if env := os.Getenv("NOVAMD_ENV"); env != "" { - logging.Debug("loading config for environment", "env", env) config.IsDevelopment = env == "development" } if dbPath := os.Getenv("NOVAMD_DB_PATH"); dbPath != "" { - logging.Debug("loading config for database path", "path", dbPath) config.DBPath = dbPath } if workDir := os.Getenv("NOVAMD_WORKDIR"); workDir != "" { - logging.Debug("loading config for work directory", "dir", workDir) config.WorkDir = workDir } if staticPath := os.Getenv("NOVAMD_STATIC_PATH"); staticPath != "" { - logging.Debug("loading config for static path", "path", staticPath) config.StaticPath = staticPath } if port := os.Getenv("NOVAMD_PORT"); port != "" { - logging.Debug("loading config for port", "port", port) config.Port = port } if rootURL := os.Getenv("NOVAMD_ROOT_URL"); rootURL != "" { - logging.Debug("loading config for root URL", "url", rootURL) config.RootURL = rootURL } if domain := os.Getenv("NOVAMD_DOMAIN"); domain != "" { - logging.Debug("loading config for domain", "domain", domain) config.Domain = domain } if corsOrigins := os.Getenv("NOVAMD_CORS_ORIGINS"); corsOrigins != "" { - logging.Debug("loading config for CORS origins", "origins", corsOrigins) config.CORSOrigins = strings.Split(corsOrigins, ",") } @@ -106,35 +97,17 @@ func LoadConfig() (*Config, error) { config.EncryptionKey = os.Getenv("NOVAMD_ENCRYPTION_KEY") config.JWTSigningKey = os.Getenv("NOVAMD_JWT_SIGNING_KEY") - logging.Debug("sensitive configuration loaded", - "adminEmailSet", config.AdminEmail != "", - "adminPasswordSet", config.AdminPassword != "", - "encryptionKeySet", config.EncryptionKey != "", - "jwtSigningKeySet", config.JWTSigningKey != "") - // Configure rate limiting if reqStr := os.Getenv("NOVAMD_RATE_LIMIT_REQUESTS"); reqStr != "" { parsed, err := strconv.Atoi(reqStr) - if err != nil { - logging.Warn("invalid rate limit requests value, using default", - "value", reqStr, - "default", config.RateLimitRequests, - "error", err) - } else { - logging.Debug("loading config for rate limit requests", "requests", parsed) + if err == nil { config.RateLimitRequests = parsed } } if windowStr := os.Getenv("NOVAMD_RATE_LIMIT_WINDOW"); windowStr != "" { parsed, err := time.ParseDuration(windowStr) - if err != nil { - logging.Warn("invalid rate limit window value, using default", - "value", windowStr, - "default", config.RateLimitWindow, - "error", err) - } else { - logging.Debug("loading config for rate limit window", "window", parsed) + if err == nil { config.RateLimitWindow = parsed } } @@ -142,13 +115,10 @@ func LoadConfig() (*Config, error) { // Configure log level, if isDevelopment is set, default to debug if logLevel := os.Getenv("NOVAMD_LOG_LEVEL"); logLevel != "" { parsed := logging.ParseLogLevel(logLevel) - logging.Debug("loading config for log level", "level", parsed) config.LogLevel = parsed } else if config.IsDevelopment { - logging.Debug("setting log level to debug for development") config.LogLevel = logging.DEBUG } else { - logging.Debug("setting log level to info for production") config.LogLevel = logging.INFO } @@ -157,6 +127,5 @@ func LoadConfig() (*Config, error) { return nil, err } - logging.Info("configuration loaded successfully") return config, nil } diff --git a/server/internal/logging/logger.go b/server/internal/logging/logger.go index e05ea32..7e23261 100644 --- a/server/internal/logging/logger.go +++ b/server/internal/logging/logger.go @@ -4,6 +4,7 @@ package logging import ( "log/slog" "os" + "reflect" ) // Logger represents the interface for logging operations @@ -60,6 +61,27 @@ func ParseLogLevel(level string) LogLevel { } } +// Redact redacts sensitive fields from a struct based on the `log` struct tag +// if the tag is set to "redact" the field value is replaced with "[REDACTED]" +func Redact(v any) map[string]any { + result := make(map[string]any) + val := reflect.ValueOf(v) + typ := val.Type() + + for i := 0; i < val.NumField(); i++ { + field := typ.Field(i) + if tag := field.Tag.Get("log"); tag != "" { + switch tag { + case "redact": + result[field.Name] = "[REDACTED]" + default: + result[field.Name] = val.Field(i).Interface() + } + } + } + return result +} + // Implementation of Logger interface methods func (l *logger) Debug(msg string, args ...any) { l.logger.Debug(msg, args...) diff --git a/server/internal/secrets/secrets.go b/server/internal/secrets/secrets.go index 9a66824..a79ef7d 100644 --- a/server/internal/secrets/secrets.go +++ b/server/internal/secrets/secrets.go @@ -33,8 +33,6 @@ func getLogger() logging.Logger { // ValidateKey checks if the provided base64-encoded key is suitable for AES-256 func ValidateKey(key string) error { - log := getLogger() - log.Debug("validating encryption key") _, err := decodeAndValidateKey(key) return err }