Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: better configuration logic #18

Merged
merged 2 commits into from
Feb 1, 2025
Merged
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
19 changes: 15 additions & 4 deletions cmd/httb/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
115 changes: 29 additions & 86 deletions internal/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion internal/pkg/service/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
Loading