Skip to content
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
2 changes: 2 additions & 0 deletions .copier-answers.yml.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
{{ _copier_answers|to_nice_yaml -}}
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.2.0

- Add config validation, default values and automated registration of config keys.
- Add sloghttp middleware to exclude health checks from logging.

## 0.1.2

- Fix typo in readme.
Expand Down
2 changes: 1 addition & 1 deletion Makefile.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ download-deps:
go mod download

install-goa:
go install goa.design/goa/v3/cmd/goa@latest
go install goa.design/goa/v3/cmd/goa@v3.22.2

install-mockery:
go install github.com/vektra/mockery/v2@latest
Expand Down
66 changes: 60 additions & 6 deletions cmd/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import (
"reflect"
"strings"

"github.com/go-playground/validator/v10"
"github.com/go-viper/mapstructure/v2"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

type Config struct {
HTTP HTTPConfig `mapstructure:"http"`
LogLevel slog.Level `mapstructure:"log_level"`
LogLevel slog.Level `mapstructure:"log_level" default:"INFO"`
}

func ParseLogLevel(level string) (slog.Level, error) {
Expand Down Expand Up @@ -50,18 +51,17 @@ func Load(cfgFile string) (Config, error) {
home, err := os.UserHomeDir()
cobra.CheckErr(err)

// Search config in home directory with name ".goa-boilerplate" (without extension).
// Search config in home directory
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".goa-boilerplate")
}

// Set default values
viper.SetDefault("log_level", "INFO")
viper.SetDefault("http.port", "3000")
// Automatically register all config keys from struct tags (including defaults)
registerConfigKeys()

viper.AutomaticEnv() // read in environment variables that match
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv() // read in environment variables that match

// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
Expand All @@ -75,5 +75,59 @@ func Load(cfgFile string) (Config, error) {
log.Fatalf("unable to decode config into struct: %v", err)
}

// Validate using struct tags
if err := validateConfig(&cfg); err != nil {
log.Fatalf("config validation failed: %v", err)
}

return cfg, nil
}

// registerConfigKeys uses reflection to automatically register all config keys
// from the Config struct's mapstructure tags, so viper knows to read them from env
func registerConfigKeys() {
var cfg Config
registerStructKeys(reflect.TypeOf(cfg), "")
}

func registerStructKeys(t reflect.Type, prefix string) {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)

// Get the mapstructure tag
tag := field.Tag.Get("mapstructure")
if tag == "" || tag == "-" {
continue
}

// Build the full key path
key := tag
if prefix != "" {
key = prefix + "." + tag
}

// If it's a struct, recurse
if field.Type.Kind() == reflect.Struct {
registerStructKeys(field.Type, key)
} else {
// Get default value from tag, or use empty string
defaultVal := field.Tag.Get("default")
viper.SetDefault(key, defaultVal)
}
}
}

func validateConfig(cfg *Config) error {
validate := validator.New()

// Register custom tag name for better error messages
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := fld.Tag.Get("mapstructure")
if name == "" {
return fld.Name
}
return name
})

return validate.Struct(cfg)
}
2 changes: 1 addition & 1 deletion cmd/config/http.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package config

type HTTPConfig struct {
Port string `mapstructure:"port"`
Port string `mapstructure:"port" default:"3000"`
}
1 change: 1 addition & 0 deletions copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ _exclude:
- CHANGELOG.md
- README.md
- .git
- copier.yml

_tasks:
- make install-dev-tools
Expand Down
12 changes: 11 additions & 1 deletion internal/infra/http/server.go.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,17 @@ func ServeHTTP(mux goahttp.Muxer, ctx context.Context, cfg config.Config, wg *sy

// Middleware
handler := sloghttp.Recovery(mux)
handler = sloghttp.New(logger)(handler)

// Configure logging with filter to exclude health checks
config := sloghttp.Config{
DefaultLevel: cfg.LogLevel,

Filters: []sloghttp.Filter{
sloghttp.IgnorePath("/checks/health"),
},
}

handler = sloghttp.NewWithConfig(logger, config)(handler)

// Start HTTP server using default configuration, change the code to
// configure the server as required by your service.
Expand Down