From cb0169839a7b6a99ddc939a77cb4bb4b2031de9e Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Sat, 1 Feb 2025 11:38:45 +0100 Subject: [PATCH 1/2] refactor: better configuration logic --- cmd/httb/main.go | 19 ++++-- go.mod | 1 + go.sum | 2 + internal/pkg/config/config.go | 115 +++++++++------------------------- 4 files changed, 47 insertions(+), 90 deletions(-) diff --git a/cmd/httb/main.go b/cmd/httb/main.go index 397afe8..5a6373e 100644 --- a/cmd/httb/main.go +++ b/cmd/httb/main.go @@ -2,8 +2,10 @@ package main import ( _ "embed" + "fmt" "github.com/marvinjwendt/httb/internal/pkg/service" "log/slog" + "os" "github.com/marvinjwendt/httb/internal/pkg/config" ) @@ -12,14 +14,23 @@ var cfg *config.Config func init() { // Init config - env := config.ReadEnv() - cfg = config.New(env) + var err error + cfg, err = config.LoadConfig() + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err) + os.Exit(1) + } // Init logger - slog.SetDefault(cfg.Logger) + logger, err := cfg.SetupLogger() + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to setup logger: %v\n", err) + os.Exit(1) + } + slog.SetDefault(logger) // Print config in debug mode - slog.Debug("configuration", "environment", env) + slog.Debug("configuration", "environment", cfg) } func main() { diff --git a/go.mod b/go.mod index b35bce1..9e148ae 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( github.com/breml/errchkjson v0.4.0 // indirect github.com/butuzov/ireturn v0.3.0 // indirect github.com/butuzov/mirror v1.2.0 // indirect + github.com/caarlos0/env/v11 v11.3.1 // indirect github.com/catenacyber/perfsprint v0.7.1 // indirect github.com/ccojocar/zxcvbn-go v1.0.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect diff --git a/go.sum b/go.sum index 1a7a80a..4471832 100644 --- a/go.sum +++ b/go.sum @@ -108,6 +108,8 @@ github.com/butuzov/ireturn v0.3.0 h1:hTjMqWw3y5JC3kpnC5vXmFJAWI/m31jaCYQqzkS6PL0 github.com/butuzov/ireturn v0.3.0/go.mod h1:A09nIiwiqzN/IoVo9ogpa0Hzi9fex1kd9PSD6edP5ZA= github.com/butuzov/mirror v1.2.0 h1:9YVK1qIjNspaqWutSv8gsge2e/Xpq1eqEkslEUHy5cs= github.com/butuzov/mirror v1.2.0/go.mod h1:DqZZDtzm42wIAIyHXeN8W/qb1EPlb9Qn/if9icBOpdQ= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/catenacyber/perfsprint v0.7.1 h1:PGW5G/Kxn+YrN04cRAZKC+ZuvlVwolYMrIyyTJ/rMmc= github.com/catenacyber/perfsprint v0.7.1/go.mod h1:/wclWYompEyjUD2FuIIDVKNkqz7IgBIWXIH3V0Zol50= github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 55bd376..da53e16 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -2,121 +2,64 @@ package config import ( "fmt" + "github.com/caarlos0/env/v11" "log/slog" "os" "strings" "time" ) -var hasError bool - -type Env struct { - LogLevel string - LogFormat string - Addr string - Timeout string -} - -func (e *Env) String() string { - return fmt.Sprintf("%+v", *e) -} - +// Config holds application configuration. type Config struct { - // Logger is the slog logger to use - Logger *slog.Logger - - // Addr is the address to listen on - Addr string `json:"addr"` - - // Timeout is the timeout for the http server - Timeout time.Duration `json:"timeout"` -} - -// ReadEnv reads the environment variables and returns an Env struct. -func ReadEnv() *Env { - return &Env{ - LogLevel: getEnv("LOG_LEVEL", "info"), - LogFormat: getEnv("LOG_FORMAT", "logfmt"), - Addr: getEnv("ADDR", ":8080"), - Timeout: getEnv("TIMEOUT", "2m"), - } + LogLevel string `env:"LOG_LEVEL" envDefault:"info"` + LogFormat string `env:"LOG_FORMAT" envDefault:"logfmt"` + Addr string `env:"ADDR" envDefault:":8080"` + Timeout time.Duration `env:"TIMEOUT" envDefault:"2m"` } -// New reads the configuration from the environment variables and returns a Config and Env struct. -func New(env *Env) *Config { - cfg := &Config{ - Logger: parseLogger(env.LogLevel, env.LogFormat), - Addr: env.Addr, - Timeout: parseTimeout(env.Timeout), - } - - if hasError { - os.Exit(1) +// LoadConfig reads the environment variables and returns a Config struct. +func LoadConfig() (*Config, error) { + cfg := &Config{} + if err := env.Parse(cfg); err != nil { + return nil, fmt.Errorf("failed to parse environment variables: %w", err) } - - return cfg + return cfg, nil } -func parseTimeout(timeout string) time.Duration { - d, err := time.ParseDuration(timeout) +// SetupLogger initializes the slog.Logger based on the config. +func (cfg *Config) SetupLogger() (*slog.Logger, error) { + format := strings.ToLower(cfg.LogFormat) + level, err := parseLogLevel(cfg.LogLevel) if err != nil { - configError("invalid timeout", "timeout", timeout) + return nil, err } - return d -} - -func parseLogger(level, format string) *slog.Logger { - format = strings.ToLower(format) - var handler slog.Handler switch format { case "json": - handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: parseLogLevel(level)}) + handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}) case "logfmt", "text": - handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: parseLogLevel(level)}) + handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level}) default: - configError("invalid log format", "log_format", format) + return nil, fmt.Errorf("invalid log format: %s", format) } - return slog.New(handler) + return slog.New(handler), nil } -// parseLogLevel parses the log level from the environment variable and returns the corresponding slog.Level. -// If the level is invalid, it logs an error and returns slog.LevelInfo. -// Possible values are: debug, info, warn, error - default is info. -func parseLogLevel(level string) slog.Level { - level = strings.ToLower(level) - - switch level { +// parseLogLevel converts a string to a slog.Level, returning an error if invalid. +func parseLogLevel(level string) (slog.Level, error) { + switch strings.ToLower(level) { case "debug": - return slog.LevelDebug + return slog.LevelDebug, nil case "info": - return slog.LevelInfo + return slog.LevelInfo, nil case "warn": - return slog.LevelWarn + return slog.LevelWarn, nil case "error": - return slog.LevelError + return slog.LevelError, nil default: - configError("invalid log level", "log_level", level) - return slog.LevelInfo - } -} - -// error logs an error message and sets the hasError flag to true, which will cause the program to exit with a non-zero exit code. -func configError(msg string, args ...any) { - msg = "in config: " + msg - slog.Error(msg, args...) - - hasError = true -} - -// getEnv returns the value of a given environment variable, or the defined default value. -func getEnv(key, def string) string { - if val := os.Getenv(key); val != "" { - return val + return slog.LevelInfo, fmt.Errorf("invalid log level: %s", level) } - - return def } From 8f9c380b763ed238ead8b1380d0e6a68ad37e877 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Sat, 1 Feb 2025 11:41:48 +0100 Subject: [PATCH 2/2] fixed build error --- internal/pkg/service/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkg/service/server.go b/internal/pkg/service/server.go index 7c4d3d0..a771e33 100644 --- a/internal/pkg/service/server.go +++ b/internal/pkg/service/server.go @@ -17,7 +17,7 @@ func (s Service) Start() error { e.HidePort = true // Echo middlewares - e.Use(slogecho.New(s.config.Logger)) + e.Use(slogecho.New(slog.Default())) e.Use(middleware.Recover()) e.Use(middleware.CORS())