From 6623ced227462a509ad37943e9cfde56643eebc4 Mon Sep 17 00:00:00 2001 From: Gemini CLI Date: Thu, 15 Jan 2026 18:28:17 +0000 Subject: [PATCH 1/6] feat(config): add yaml config with generic env override Adds internal/config package using gopkg.in/yaml.v3 and reflection for generic environment variable overrides. Updates main.go to load config and set log level. Adds tests and example config. --- cmd/app/main.go | 32 +++++++- config/config.yaml | 7 ++ go.mod | 2 + go.sum | 4 + internal/config/config.go | 136 +++++++++++++++++++++++++++++++++ internal/config/config_test.go | 43 +++++++++++ 6 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 config/config.yaml create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go diff --git a/cmd/app/main.go b/cmd/app/main.go index c87ad03..6329b0e 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -2,15 +2,30 @@ package main import ( "context" + "fmt" "log/slog" "os" "os/signal" + "strings" "syscall" "time" + + "github.com/placeholder/golang-template/internal/config" ) func main() { - logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + // Load Configuration + cfg, err := config.Load("config/config.yaml", "APP") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err) + os.Exit(1) + } + + // Setup Logger + opts := &slog.HandlerOptions{ + Level: parseLogLevel(cfg.LogLevel), + } + logger := slog.New(slog.NewJSONHandler(os.Stdout, opts)) slog.SetDefault(logger) ctx, cancel := context.WithCancel(context.Background()) @@ -31,6 +46,21 @@ func main() { } } +func parseLogLevel(level string) slog.Level { + switch strings.ToLower(level) { + case "debug": + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} + func run(ctx context.Context) error { slog.Info("Starting application...") diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..b5e7402 --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,7 @@ +# Application Configuration +# This file contains the default configuration for the application. +# Values can be overridden by environment variables with the prefix "APP_". +# Example: APP_LOG_LEVEL=debug + +# Log Level: debug, info, warn, error +log_level: "info" diff --git a/go.mod b/go.mod index 3c90b52..c492fcb 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/placeholder/golang-template go 1.23 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..e9b4038 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,136 @@ +package config + +import ( + "fmt" + "os" + "reflect" + "strconv" + "strings" + "time" + + "gopkg.in/yaml.v3" +) + +// Config holds the application configuration. +type Config struct { + LogLevel string `yaml:"log_level"` +} + +// Load reads configuration from a YAML file and overrides it with environment variables. +// The envPrefix is used to namespace environment variables (e.g. "APP"). +// Env vars should be in the format: PREFIX_FIELD_NAME (uppercase). +func Load(configPath string, envPrefix string) (*Config, error) { + cfg := &Config{} + + // 1. Read YAML file + file, err := os.Open(configPath) + if err != nil { + return nil, fmt.Errorf("failed to open config file: %w", err) + } + defer file.Close() + + decoder := yaml.NewDecoder(file) + if err := decoder.Decode(cfg); err != nil { + return nil, fmt.Errorf("failed to decode config file: %w", err) + } + + // 2. Override from Environment Variables + if err := overrideFromEnv(cfg, envPrefix); err != nil { + return nil, fmt.Errorf("failed to process environment variables: %w", err) + } + + return cfg, nil +} + +// overrideFromEnv uses reflection to traverse the struct and update fields from env vars. +func overrideFromEnv(cfg interface{}, prefix string) error { + v := reflect.ValueOf(cfg) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + return setFromEnv(v, prefix) +} + +func setFromEnv(v reflect.Value, prefix string) error { + t := v.Type() + + for i := 0; i < v.NumField(); i++ { + field := t.Field(i) + fieldVal := v.Field(i) + + // Determine the key for this field + // We use the yaml tag if available to guess the name, or just the field name + keyName := field.Name + if tag := field.Tag.Get("yaml"); tag != "" { + parts := strings.Split(tag, ",") + if parts[0] != "" { + keyName = parts[0] + } + } + + // Sanitize key name for Env (uppercase, replace non-alphanum with _) + envKey := strings.ToUpper(fmt.Sprintf("%s_%s", prefix, keyName)) + // Clean up common separators like hyphens in yaml keys + envKey = strings.ReplaceAll(envKey, "-", "_") + + if fieldVal.Kind() == reflect.Struct { + // Recursive call for nested structs + if err := setFromEnv(fieldVal, envKey); err != nil { + return err + } + continue + } + + if !fieldVal.CanSet() { + continue + } + + val, present := os.LookupEnv(envKey) + if present { + if err := setField(fieldVal, val); err != nil { + return fmt.Errorf("failed to set field %s from env %s: %w", field.Name, envKey, err) + } + } + } + return nil +} + +func setField(field reflect.Value, value string) error { + switch field.Kind() { + case reflect.String: + field.SetString(value) + case reflect.Bool: + b, err := strconv.ParseBool(value) + if err != nil { + return err + } + field.SetBool(b) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + // Handle Duration specially if it's an int64 underlying type? + // For now, let's assume standard ints. + // Real generic implementations often handle time.Duration + if field.Type() == reflect.TypeOf(time.Duration(0)) { + d, err := time.ParseDuration(value) + if err != nil { + return err + } + field.SetInt(int64(d)) + } else { + i, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + field.SetInt(i) + } + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + field.SetFloat(f) + default: + return fmt.Errorf("unsupported type: %s", field.Kind()) + } + return nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..43a13ab --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,43 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoad(t *testing.T) { + // Create a temporary config file + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "config.yaml") + + yamlContent := []byte(` +log_level: "info" +`) + if err := os.WriteFile(configFile, yamlContent, 0644); err != nil { + t.Fatalf("failed to write temp config file: %v", err) + } + + t.Run("Load from YAML", func(t *testing.T) { + cfg, err := Load(configFile, "TEST") + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if cfg.LogLevel != "info" { + t.Errorf("expected LogLevel 'info', got '%s'", cfg.LogLevel) + } + }) + + t.Run("Override from Env", func(t *testing.T) { + os.Setenv("TEST_LOG_LEVEL", "debug") + defer os.Unsetenv("TEST_LOG_LEVEL") + + cfg, err := Load(configFile, "TEST") + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if cfg.LogLevel != "debug" { + t.Errorf("expected LogLevel 'debug', got '%s'", cfg.LogLevel) + } + }) +} -- 2.49.1 From ec689c6152dcda3b56d779c4fbcecc6a2f92eea0 Mon Sep 17 00:00:00 2001 From: Gemini CLI Date: Thu, 15 Jan 2026 18:37:01 +0000 Subject: [PATCH 2/6] feat(config): support cli config path and nested config Adds -config CLI flag to main.go. Moves ParseLogLevel to internal/config. Adds ServerConfig nested struct to Config and corresponding tests for yaml/env overrides. --- cmd/app/main.go | 26 ++++++------------- internal/config/config.go | 26 ++++++++++++++++++- internal/config/config_test.go | 47 +++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 20 deletions(-) diff --git a/cmd/app/main.go b/cmd/app/main.go index 6329b0e..5041abe 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -2,11 +2,11 @@ package main import ( "context" + "flag" "fmt" "log/slog" "os" "os/signal" - "strings" "syscall" "time" @@ -14,8 +14,13 @@ import ( ) func main() { + // Parse CLI flags + var configPath string + flag.StringVar(&configPath, "config", "config/config.yaml", "path to config file") + flag.Parse() + // Load Configuration - cfg, err := config.Load("config/config.yaml", "APP") + cfg, err := config.Load(configPath, "APP") if err != nil { fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err) os.Exit(1) @@ -23,7 +28,7 @@ func main() { // Setup Logger opts := &slog.HandlerOptions{ - Level: parseLogLevel(cfg.LogLevel), + Level: config.ParseLogLevel(cfg.LogLevel), } logger := slog.New(slog.NewJSONHandler(os.Stdout, opts)) slog.SetDefault(logger) @@ -46,21 +51,6 @@ func main() { } } -func parseLogLevel(level string) slog.Level { - switch strings.ToLower(level) { - case "debug": - return slog.LevelDebug - case "info": - return slog.LevelInfo - case "warn": - return slog.LevelWarn - case "error": - return slog.LevelError - default: - return slog.LevelInfo - } -} - func run(ctx context.Context) error { slog.Info("Starting application...") diff --git a/internal/config/config.go b/internal/config/config.go index e9b4038..c5c9d1c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "log/slog" "os" "reflect" "strconv" @@ -13,7 +14,14 @@ import ( // Config holds the application configuration. type Config struct { - LogLevel string `yaml:"log_level"` + LogLevel string `yaml:"log_level"` + Server ServerConfig `yaml:"server"` +} + +// ServerConfig holds server-specific configuration. +type ServerConfig struct { + Host string `yaml:"host"` + Port int `yaml:"port"` } // Load reads configuration from a YAML file and overrides it with environment variables. @@ -42,6 +50,22 @@ func Load(configPath string, envPrefix string) (*Config, error) { return cfg, nil } +// ParseLogLevel converts a string log level to a slog.Level. +func ParseLogLevel(level string) slog.Level { + switch strings.ToLower(level) { + case "debug": + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} + // overrideFromEnv uses reflection to traverse the struct and update fields from env vars. func overrideFromEnv(cfg interface{}, prefix string) error { v := reflect.ValueOf(cfg) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 43a13ab..613a714 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "log/slog" "os" "path/filepath" "testing" @@ -13,6 +14,9 @@ func TestLoad(t *testing.T) { yamlContent := []byte(` log_level: "info" +server: + host: "localhost" + port: 8080 `) if err := os.WriteFile(configFile, yamlContent, 0644); err != nil { t.Fatalf("failed to write temp config file: %v", err) @@ -26,11 +30,23 @@ log_level: "info" if cfg.LogLevel != "info" { t.Errorf("expected LogLevel 'info', got '%s'", cfg.LogLevel) } + if cfg.Server.Host != "localhost" { + t.Errorf("expected Server.Host 'localhost', got '%s'", cfg.Server.Host) + } + if cfg.Server.Port != 8080 { + t.Errorf("expected Server.Port 8080, got %d", cfg.Server.Port) + } }) t.Run("Override from Env", func(t *testing.T) { os.Setenv("TEST_LOG_LEVEL", "debug") - defer os.Unsetenv("TEST_LOG_LEVEL") + os.Setenv("TEST_SERVER_PORT", "9090") + os.Setenv("TEST_SERVER_HOST", "0.0.0.0") + defer func() { + os.Unsetenv("TEST_LOG_LEVEL") + os.Unsetenv("TEST_SERVER_PORT") + os.Unsetenv("TEST_SERVER_HOST") + }() cfg, err := Load(configFile, "TEST") if err != nil { @@ -39,5 +55,34 @@ log_level: "info" if cfg.LogLevel != "debug" { t.Errorf("expected LogLevel 'debug', got '%s'", cfg.LogLevel) } + if cfg.Server.Port != 9090 { + t.Errorf("expected Server.Port 9090, got %d", cfg.Server.Port) + } + if cfg.Server.Host != "0.0.0.0" { + t.Errorf("expected Server.Host '0.0.0.0', got '%s'", cfg.Server.Host) + } }) } + +func TestParseLogLevel(t *testing.T) { + tests := []struct { + input string + expected slog.Level + }{ + {"debug", slog.LevelDebug}, + {"DEBUG", slog.LevelDebug}, + {"info", slog.LevelInfo}, + {"warn", slog.LevelWarn}, + {"error", slog.LevelError}, + {"unknown", slog.LevelInfo}, + {"", slog.LevelInfo}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + if got := ParseLogLevel(tt.input); got != tt.expected { + t.Errorf("ParseLogLevel(%q) = %v, want %v", tt.input, got, tt.expected) + } + }) + } +} -- 2.49.1 From 4807edd23ac23a0790c6da6ab6e9c568a0fbc092 Mon Sep 17 00:00:00 2001 From: Gemini CLI Date: Thu, 15 Jan 2026 18:57:01 +0000 Subject: [PATCH 3/6] refactor(logging): move logging setup to internal/logging Moves logging configuration and setup to a dedicated package. Updates Config struct to use nested LoggingConfig. Updates main.go to use the new logging package. --- cmd/app/main.go | 9 ++++---- config/config.yaml | 11 ++++++--- internal/config/config.go | 26 ++++++--------------- internal/config/config_test.go | 39 +++++++------------------------- internal/logging/logging.go | 37 ++++++++++++++++++++++++++++++ internal/logging/logging_test.go | 37 ++++++++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 58 deletions(-) create mode 100644 internal/logging/logging.go create mode 100644 internal/logging/logging_test.go diff --git a/cmd/app/main.go b/cmd/app/main.go index 5041abe..9f6d604 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -11,6 +11,7 @@ import ( "time" "github.com/placeholder/golang-template/internal/config" + "github.com/placeholder/golang-template/internal/logging" ) func main() { @@ -27,11 +28,9 @@ func main() { } // Setup Logger - opts := &slog.HandlerOptions{ - Level: config.ParseLogLevel(cfg.LogLevel), - } - logger := slog.New(slog.NewJSONHandler(os.Stdout, opts)) - slog.SetDefault(logger) + logging.Configure(logging.Config{ + Level: cfg.Logging.Level, + }) ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/config/config.yaml b/config/config.yaml index b5e7402..327f910 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,7 +1,12 @@ # Application Configuration # This file contains the default configuration for the application. # Values can be overridden by environment variables with the prefix "APP_". -# Example: APP_LOG_LEVEL=debug +# Example: APP_LOGGING_LEVEL=debug -# Log Level: debug, info, warn, error -log_level: "info" +logging: + # Log Level: debug, info, warn, error + level: "info" + +server: + host: "localhost" + port: 8080 \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index c5c9d1c..e547714 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,7 +2,6 @@ package config import ( "fmt" - "log/slog" "os" "reflect" "strconv" @@ -14,8 +13,13 @@ import ( // Config holds the application configuration. type Config struct { - LogLevel string `yaml:"log_level"` - Server ServerConfig `yaml:"server"` + Logging LoggingConfig `yaml:"logging"` + Server ServerConfig `yaml:"server"` +} + +// LoggingConfig holds logging-specific configuration. +type LoggingConfig struct { + Level string `yaml:"level"` } // ServerConfig holds server-specific configuration. @@ -50,22 +54,6 @@ func Load(configPath string, envPrefix string) (*Config, error) { return cfg, nil } -// ParseLogLevel converts a string log level to a slog.Level. -func ParseLogLevel(level string) slog.Level { - switch strings.ToLower(level) { - case "debug": - return slog.LevelDebug - case "info": - return slog.LevelInfo - case "warn": - return slog.LevelWarn - case "error": - return slog.LevelError - default: - return slog.LevelInfo - } -} - // overrideFromEnv uses reflection to traverse the struct and update fields from env vars. func overrideFromEnv(cfg interface{}, prefix string) error { v := reflect.ValueOf(cfg) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 613a714..6623ae8 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,7 +1,6 @@ package config import ( - "log/slog" "os" "path/filepath" "testing" @@ -13,7 +12,8 @@ func TestLoad(t *testing.T) { configFile := filepath.Join(tmpDir, "config.yaml") yamlContent := []byte(` -log_level: "info" +logging: + level: "info" server: host: "localhost" port: 8080 @@ -27,8 +27,8 @@ server: if err != nil { t.Fatalf("Load() error = %v", err) } - if cfg.LogLevel != "info" { - t.Errorf("expected LogLevel 'info', got '%s'", cfg.LogLevel) + if cfg.Logging.Level != "info" { + t.Errorf("expected Logging.Level 'info', got '%s'", cfg.Logging.Level) } if cfg.Server.Host != "localhost" { t.Errorf("expected Server.Host 'localhost', got '%s'", cfg.Server.Host) @@ -39,11 +39,11 @@ server: }) t.Run("Override from Env", func(t *testing.T) { - os.Setenv("TEST_LOG_LEVEL", "debug") + os.Setenv("TEST_LOGGING_LEVEL", "debug") os.Setenv("TEST_SERVER_PORT", "9090") os.Setenv("TEST_SERVER_HOST", "0.0.0.0") defer func() { - os.Unsetenv("TEST_LOG_LEVEL") + os.Unsetenv("TEST_LOGGING_LEVEL") os.Unsetenv("TEST_SERVER_PORT") os.Unsetenv("TEST_SERVER_HOST") }() @@ -52,8 +52,8 @@ server: if err != nil { t.Fatalf("Load() error = %v", err) } - if cfg.LogLevel != "debug" { - t.Errorf("expected LogLevel 'debug', got '%s'", cfg.LogLevel) + if cfg.Logging.Level != "debug" { + t.Errorf("expected Logging.Level 'debug', got '%s'", cfg.Logging.Level) } if cfg.Server.Port != 9090 { t.Errorf("expected Server.Port 9090, got %d", cfg.Server.Port) @@ -63,26 +63,3 @@ server: } }) } - -func TestParseLogLevel(t *testing.T) { - tests := []struct { - input string - expected slog.Level - }{ - {"debug", slog.LevelDebug}, - {"DEBUG", slog.LevelDebug}, - {"info", slog.LevelInfo}, - {"warn", slog.LevelWarn}, - {"error", slog.LevelError}, - {"unknown", slog.LevelInfo}, - {"", slog.LevelInfo}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - if got := ParseLogLevel(tt.input); got != tt.expected { - t.Errorf("ParseLogLevel(%q) = %v, want %v", tt.input, got, tt.expected) - } - }) - } -} diff --git a/internal/logging/logging.go b/internal/logging/logging.go new file mode 100644 index 0000000..bf8dba0 --- /dev/null +++ b/internal/logging/logging.go @@ -0,0 +1,37 @@ +package logging + +import ( + "log/slog" + "os" + "strings" +) + +// Config holds the logging configuration. +type Config struct { + Level string `yaml:"level"` +} + +// Configure sets up the global logger based on the provided configuration. +func Configure(cfg Config) *slog.Logger { + opts := &slog.HandlerOptions{ + Level: parseLogLevel(cfg.Level), + } + logger := slog.New(slog.NewJSONHandler(os.Stdout, opts)) + slog.SetDefault(logger) + return logger +} + +func parseLogLevel(level string) slog.Level { + switch strings.ToLower(level) { + case "debug": + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} diff --git a/internal/logging/logging_test.go b/internal/logging/logging_test.go new file mode 100644 index 0000000..eb30f0a --- /dev/null +++ b/internal/logging/logging_test.go @@ -0,0 +1,37 @@ +package logging + +import ( + "log/slog" + "testing" +) + +func TestParseLogLevel(t *testing.T) { + tests := []struct { + input string + expected slog.Level + }{ + {"debug", slog.LevelDebug}, + {"DEBUG", slog.LevelDebug}, + {"info", slog.LevelInfo}, + {"warn", slog.LevelWarn}, + {"error", slog.LevelError}, + {"unknown", slog.LevelInfo}, + {"", slog.LevelInfo}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + if got := parseLogLevel(tt.input); got != tt.expected { + t.Errorf("parseLogLevel(%q) = %v, want %v", tt.input, got, tt.expected) + } + }) + } +} + +func TestConfigure(t *testing.T) { + cfg := Config{Level: "debug"} + logger := Configure(cfg) + if logger == nil { + t.Error("Configure() returned nil") + } +} -- 2.49.1 From 1dde7418855af91b30355c6846bce2c0ae751729 Mon Sep 17 00:00:00 2001 From: Gemini CLI Date: Thu, 15 Jan 2026 18:57:58 +0000 Subject: [PATCH 4/6] docs: add configuration and logging documentation Adds docs/configuration.md detailing the YAML config structure, environment variable overrides, and logging features. --- docs/configuration.md | 70 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/configuration.md diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..0fc4fb6 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,70 @@ +# Configuration and Logging + +This application uses a flexible configuration system that supports YAML files and environment variable overrides. It also features a structured logging setup. + +## Configuration + +The configuration is loaded from a YAML file. By default, the application looks for `config/config.yaml`. You can specify a different configuration file using the `-config` CLI flag. + +```bash +./app -config /path/to/my-config.yaml +``` + +### Configuration Structure + +The configuration file supports the following structure: + +```yaml +# Logging configuration +logging: + # Log Level: debug, info, warn, error + level: "info" + +# Server configuration +server: + host: "localhost" + port: 8080 +``` + +### Environment Variables + +All configuration values can be overridden using environment variables. The application uses the `APP_` prefix for all environment variables. The variable name is constructed by capitalizing the path to the configuration value and replacing dots/nesting with underscores. + +| YAML Path | Environment Variable | Description | +| :--- | :--- | :--- | +| `logging.level` | `APP_LOGGING_LEVEL` | Sets the logging level (debug, info, warn, error) | +| `server.host` | `APP_SERVER_HOST` | Sets the server host address | +| `server.port` | `APP_SERVER_PORT` | Sets the server listening port | + +**Example:** + +To start the application with `debug` logging and listening on port `9090`: + +```bash +APP_LOGGING_LEVEL=debug APP_SERVER_PORT=9090 ./app +``` + +## Logging + +The application uses structured logging (JSON format) via the standard library's `log/slog` package. + +### Log Levels + +The supported log levels are: + +* `debug`: Detailed information for debugging purposes. +* `info`: General operational events (startup, shutdown, etc.). +* `warn`: Non-critical errors or potential issues. +* `error`: Critical errors that require attention. + +The log level is configured via the `logging.level` setting in the configuration file or the `APP_LOGGING_LEVEL` environment variable. + +### Log Output + +Logs are output to `stdout` in JSON format to facilitate parsing by log management systems. + +**Example Log Entry:** + +```json +{"time":"2023-10-27T10:00:00.123Z","level":"INFO","msg":"Starting application..."} +``` -- 2.49.1 From f3dedf47f374feea6e30827af75434c191abc422 Mon Sep 17 00:00:00 2001 From: Gemini CLI Date: Thu, 15 Jan 2026 19:37:13 +0000 Subject: [PATCH 5/6] refactor(config): decouple logging config and update readme Refactors internal/config to use logging.Config directly, removing duplication. Updates README.md to reference configuration documentation. --- README.md | 6 ++++++ cmd/app/main.go | 4 +--- internal/config/config.go | 10 +++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 646dff0..e067574 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,12 @@ make test make lint ``` +## Configuration + +The application is configured via a YAML file (default `config/config.yaml`) and environment variables. + +For detailed documentation on configuration options and logging, see [docs/configuration.md](docs/configuration.md). + ## Docker Build the Docker image: diff --git a/cmd/app/main.go b/cmd/app/main.go index 9f6d604..08f2fce 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -28,9 +28,7 @@ func main() { } // Setup Logger - logging.Configure(logging.Config{ - Level: cfg.Logging.Level, - }) + logging.Configure(cfg.Logging) ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/internal/config/config.go b/internal/config/config.go index e547714..fdc8119 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,18 +8,14 @@ import ( "strings" "time" + "github.com/placeholder/golang-template/internal/logging" "gopkg.in/yaml.v3" ) // Config holds the application configuration. type Config struct { - Logging LoggingConfig `yaml:"logging"` - Server ServerConfig `yaml:"server"` -} - -// LoggingConfig holds logging-specific configuration. -type LoggingConfig struct { - Level string `yaml:"level"` + Logging logging.Config `yaml:"logging"` + Server ServerConfig `yaml:"server"` } // ServerConfig holds server-specific configuration. -- 2.49.1 From 1e187140c45310048c384a76ad089d4373ebb902 Mon Sep 17 00:00:00 2001 From: Gemini CLI Date: Thu, 15 Jan 2026 19:41:07 +0000 Subject: [PATCH 6/6] docs: separate configuration and logging documentation Extracts logging documentation into its own file (docs/logging.md) and updates README.md to link to both configuration and logging guides. --- README.md | 7 +++---- docs/configuration.md | 37 ++++++------------------------------- docs/logging.md | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 35 deletions(-) create mode 100644 docs/logging.md diff --git a/README.md b/README.md index e067574..274c252 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,10 @@ make test make lint ``` -## Configuration +## Documentation -The application is configured via a YAML file (default `config/config.yaml`) and environment variables. - -For detailed documentation on configuration options and logging, see [docs/configuration.md](docs/configuration.md). +- [Configuration Guide](docs/configuration.md): Details on YAML config, environment variables, and CLI flags. +- [Logging Guide](docs/logging.md): Information about log levels and structured logging format. ## Docker diff --git a/docs/configuration.md b/docs/configuration.md index 0fc4fb6..d699f86 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,8 +1,8 @@ -# Configuration and Logging +# Configuration -This application uses a flexible configuration system that supports YAML files and environment variable overrides. It also features a structured logging setup. +This application uses a flexible configuration system that supports YAML files and environment variable overrides. -## Configuration +## Configuration File The configuration is loaded from a YAML file. By default, the application looks for `config/config.yaml`. You can specify a different configuration file using the `-config` CLI flag. @@ -10,7 +10,7 @@ The configuration is loaded from a YAML file. By default, the application looks ./app -config /path/to/my-config.yaml ``` -### Configuration Structure +### Structure The configuration file supports the following structure: @@ -26,7 +26,7 @@ server: port: 8080 ``` -### Environment Variables +## Environment Variables All configuration values can be overridden using environment variables. The application uses the `APP_` prefix for all environment variables. The variable name is constructed by capitalizing the path to the configuration value and replacing dots/nesting with underscores. @@ -42,29 +42,4 @@ To start the application with `debug` logging and listening on port `9090`: ```bash APP_LOGGING_LEVEL=debug APP_SERVER_PORT=9090 ./app -``` - -## Logging - -The application uses structured logging (JSON format) via the standard library's `log/slog` package. - -### Log Levels - -The supported log levels are: - -* `debug`: Detailed information for debugging purposes. -* `info`: General operational events (startup, shutdown, etc.). -* `warn`: Non-critical errors or potential issues. -* `error`: Critical errors that require attention. - -The log level is configured via the `logging.level` setting in the configuration file or the `APP_LOGGING_LEVEL` environment variable. - -### Log Output - -Logs are output to `stdout` in JSON format to facilitate parsing by log management systems. - -**Example Log Entry:** - -```json -{"time":"2023-10-27T10:00:00.123Z","level":"INFO","msg":"Starting application..."} -``` +``` \ No newline at end of file diff --git a/docs/logging.md b/docs/logging.md new file mode 100644 index 0000000..bd0c0ef --- /dev/null +++ b/docs/logging.md @@ -0,0 +1,24 @@ +# Logging + +The application uses structured logging (JSON format) via the standard library's `log/slog` package. + +## Log Levels + +The supported log levels are: + +* `debug`: Detailed information for debugging purposes. +* `info`: General operational events (startup, shutdown, etc.). +* `warn`: Non-critical errors or potential issues. +* `error`: Critical errors that require attention. + +The log level is configured via the `logging.level` setting in the configuration file or the `APP_LOGGING_LEVEL` environment variable. + +## Log Output + +Logs are output to `stdout` in JSON format to facilitate parsing by log management systems. + +**Example Log Entry:** + +```json +{"time":"2023-10-27T10:00:00.123Z","level":"INFO","msg":"Starting application..."} +``` -- 2.49.1