Clone
2
EZConf
Haelnorr edited this page 2026-01-21 19:20:52 +11:00

EZConf - v0.1.1

A unified configuration management system for loading and managing environment-based configurations across multiple Go packages.

Installation

go get git.haelnorr.com/h/golib/ezconf

Key Concepts and Features

Unified Configuration Management

EZConf provides a centralized way to manage configurations from multiple packages that follow the ConfigFromEnv pattern. Instead of calling each package's configuration function individually, you can register them all with EZConf and load them together.

Source Code Parsing

EZConf parses Go source files to extract environment variable documentation directly from struct field comments. This eliminates the need to maintain separate documentation for environment variables.

.env File Management

EZConf can generate new .env files or update existing ones, preserving existing values while adding new variables. This makes it easy to maintain environment configuration across different deployment environments.

Environment Variable Documentation

EZConf supports a standard comment format for documenting environment variables:

// ENV VAR_NAME: Description (modifiers)

Supported modifiers:

  • (required) - marks variable as required
  • (required if condition) - marks variable as conditionally required
  • (default: value) - specifies default value

Quick Start

package main

import (
	"log"
	"os"

	"git.haelnorr.com/h/golib/ezconf"
	"git.haelnorr.com/h/golib/hlog"
	"git.haelnorr.com/h/golib/hws"
	"git.haelnorr.com/h/golib/hwsauth"
)

func main() {
	// Create loader
	loader := ezconf.New()

	// Register packages using built-in integrations
	loader.RegisterIntegrations(
		hlog.NewEZConfIntegration(),
		hws.NewEZConfIntegration(),
		hwsauth.NewEZConfIntegration(),
	)

  // Parse the envvars from the config structs
	if err := loader.ParseEnvVars(); err != nil {
		log.Fatal(err)
	}

	// Load the configs
	if err := loader.LoadConfigs(); err != nil {
		log.Fatal(err)
	}

	// Get configuration
	hlogCfg, _ := loader.GetConfig("hlog")
	cfg := hlogCfg.(*hlog.Config)

	// Use configuration
	logger, _ := hlog.NewLogger(cfg, os.Stdout)
	logger.Info().Msg("Application started")
}

Manual Registration

package main

import (
	"log"
	"os"

	"git.haelnorr.com/h/golib/ezconf"
	"git.haelnorr.com/h/golib/hlog"
)

func main() {
	// Create loader
	loader := ezconf.New()

	// Add package path
	loader.AddPackagePath("vendor/git.haelnorr.com/h/golib/hlog")

	// Add config function
	loader.AddConfigFunc("hlog", func() (interface{}, error) {
		return hlog.ConfigFromEnv()
	})

	// Load everything
	if err := loader.Load(); err != nil {
		log.Fatal(err)
	}

	// Get configuration
	hlogCfg, _ := loader.GetConfig("hlog")
	cfg := hlogCfg.(*hlog.Config)

	// Use configuration
	logger, _ := hlog.NewLogger(cfg, os.Stdout)
	logger.Info().Msg("Application started")
}

Configuration

EZConf uses ConfigFromEnv pattern - no environment variables needed for ezconf itself. It reads environment variables as documented in the packages you register.

Built-in Integrations

All golib packages that support configuration include built-in ezconf integration. These packages provide a NewEZConfIntegration() function that returns an Integration object:

  • hlog - hlog.NewEZConfIntegration()
  • hws - hws.NewEZConfIntegration()
  • hwsauth - hwsauth.NewEZConfIntegration()
  • tmdb - tmdb.NewEZConfIntegration()

Using built-in integrations is the recommended approach as it automatically configures the package path and configuration function.

Adding Packages

loader := ezconf.New()

// Add package paths to parse for ENV comments
loader.AddPackagePath("/path/to/golib/hlog")
loader.AddPackagePath("/path/to/golib/hws")
loader.AddPackagePath("/path/to/your/custom/package")

Adding Config Functions

// Register configuration loaders
loader.AddConfigFunc("hlog", func() (interface{}, error) {
	return hlog.ConfigFromEnv()
})

loader.AddConfigFunc("hws", func() (interface{}, error) {
	return hws.ConfigFromEnv()
})

loader.AddConfigFunc("myapp", func() (interface{}, error) {
	return myapp.ConfigFromEnv()
})

Adding Custom Environment Variables

// Add environment variables that aren't in a package config
loader.AddEnvVar(ezconf.EnvVar{
	Name:        "DATABASE_URL",
	Description: "PostgreSQL connection string",
	Required:    true,
	Default:     "postgres://localhost/mydb",
})

Detailed Usage

Using Built-in Integrations

The easiest way to use ezconf is with the built-in integrations:

loader := ezconf.New()

// Register a single integration
loader.RegisterIntegration(hlog.NewEZConfIntegration())

// Or register multiple at once
loader.RegisterIntegrations(
	hlog.NewEZConfIntegration(),
	hws.NewEZConfIntegration(),
	hwsauth.NewEZConfIntegration(),
	tmdb.NewEZConfIntegration(),
)

loader.Load()

The Integration interface requires three methods:

  • Name() string - returns the config name (e.g., "hlog")
  • PackagePath() string - returns the path to parse for ENV comments
  • ConfigFunc() func() (interface{}, error) - returns the ConfigFromEnv function

Creating Custom Integrations

You can create integrations for your own packages:

package myapp

import "runtime"

type EZConfIntegration struct{}

func (e EZConfIntegration) Name() string {
	return "myapp"
}

func (e EZConfIntegration) PackagePath() string {
	_, filename, _, _ := runtime.Caller(0)
	return filename[:len(filename)-len("/ezconf.go")]
}

func (e EZConfIntegration) ConfigFunc() func() (interface{}, error) {
	return func() (interface{}, error) {
		return ConfigFromEnv()
	}
}

func NewEZConfIntegration() EZConfIntegration {
	return EZConfIntegration{}
}

Then use it:

loader.RegisterIntegration(myapp.NewEZConfIntegration())

Loading Configurations

// Load all registered configurations and parse packages
if err := loader.Load(); err != nil {
	log.Fatal(err)
}

// Get a specific configuration
cfg, ok := loader.GetConfig("hlog")
if !ok {
	log.Fatal("hlog config not found")
}

// Type assert to the correct type
hlogCfg := cfg.(*hlog.Config)

Getting All Configurations

allConfigs := loader.GetAllConfigs()
for name, cfg := range allConfigs {
	fmt.Printf("Config %s: %+v\n", name, cfg)
}

Printing Environment Variables

Printing the environment variables requires loader.ParseEnvVars to have been called. If passing true to show the values, loader.LoadConfigs must also be called

// Print variable names and descriptions (no values)
if err := loader.PrintEnvVarsStdout(false); err != nil {
	log.Fatal(err)
}

// Output:
// LOG_LEVEL  # Log level for the logger (default: info)
// LOG_OUTPUT  # Output destination for logs (default: console)
// DATABASE_URL  # Database connection string (required)

// Print with current values
// Requires loader.LoadConfigs to be called
if err := loader.PrintEnvVarsStdout(true); err != nil {
	log.Fatal(err)
}

// Output:
// LOG_LEVEL=debug  # Log level for the logger (default: info)
// LOG_OUTPUT=console  # Output destination for logs (default: console)
// DATABASE_URL=postgres://localhost/db  # Database connection string (required)

Writing to Custom Writer

// Write to a file
file, _ := os.Create("env-vars.txt")
defer file.Close()
if err := loader.PrintEnvVars(file, true); err != nil {
	log.Fatal(err)
}

// Write to a buffer
buf := &bytes.Buffer{}
if err := loader.PrintEnvVars(buf, false); err != nil {
	log.Fatal(err)
}

Generating .env Files

// Generate new .env file with default values
err := loader.GenerateEnvFile(".env", false)

// Generate with current environment values
err := loader.GenerateEnvFile(".env.production", true)

// Generated file format:
// # Environment Variables Configuration
// # Generated by ezconf
//
// # Log level for the logger (default: info)
// LOG_LEVEL=info
//
// # Database connection string (required)
// DATABASE_URL=postgres://localhost/db

Updating Existing .env Files

// Update existing file, preserving existing values
err := loader.UpdateEnvFile(".env", false)

// Update or create if doesn't exist
err := loader.UpdateEnvFile(".env", true)

When updating:

  • Existing variables keep their values
  • New variables are added with default values
  • Comments are updated to match current documentation
  • Variables not in the config are preserved

Getting Environment Variable Metadata

envVars := loader.GetEnvVars()
for _, ev := range envVars {
	fmt.Printf("Name: %s\n", ev.Name)
	fmt.Printf("Description: %s\n", ev.Description)
	fmt.Printf("Required: %t\n", ev.Required)
	fmt.Printf("Default: %s\n", ev.Default)
	fmt.Printf("Current: %s\n", ev.CurrentValue)
}

ENV Comment Format

Basic Format

type Config struct {
	// ENV LOG_LEVEL: Log level for the application
	LogLevel string
}

With Default Value

type Config struct {
	// ENV LOG_LEVEL: Log level for the application (default: info)
	LogLevel string
}

Required Variable

type Config struct {
	// ENV DATABASE_URL: Database connection string (required)
	DatabaseURL string
}

Conditional Requirement

type Config struct {
	// ENV LOG_DIR: Directory for log files (required when LOG_OUTPUT is file) (default: /var/log)
	LogDir string
}

Inline Comments

type Config struct {
	LogLevel string // ENV LOG_LEVEL: Log level (default: info)
	Port     int    // ENV PORT: Server port (default: 8080)
}

Multiple Modifiers

type Config struct {
	// ENV LOG_FILE_NAME: Name of the log file (required when LOG_OUTPUT is file or both) (default: app.log)
	LogFileName string
}

Integration

With HLog

loader := ezconf.New()
loader.AddPackagePath("vendor/git.haelnorr.com/h/golib/hlog")
loader.AddConfigFunc("hlog", func() (interface{}, error) {
	return hlog.ConfigFromEnv()
})
loader.Load()

// Get hlog config
hlogCfg, _ := loader.GetConfig("hlog")
cfg := hlogCfg.(*hlog.Config)

// Create logger
logger, _ := hlog.NewLogger(cfg, os.Stdout)

With HWS and HWSAuth

loader := ezconf.New()

// Add all packages
loader.AddPackagePath("vendor/git.haelnorr.com/h/golib/hws")
loader.AddPackagePath("vendor/git.haelnorr.com/h/golib/hwsauth")
loader.AddPackagePath("vendor/git.haelnorr.com/h/golib/hlog")

// Add config functions
loader.AddConfigFunc("hws", func() (interface{}, error) {
	return hws.ConfigFromEnv()
})
loader.AddConfigFunc("hwsauth", func() (interface{}, error) {
	return hwsauth.ConfigFromEnv()
})
loader.AddConfigFunc("hlog", func() (interface{}, error) {
	return hlog.ConfigFromEnv()
})

loader.Load()

// Get all configs
hwsCfg, _ := loader.GetConfig("hws")
authCfg, _ := loader.GetConfig("hwsauth")
hlogCfg, _ := loader.GetConfig("hlog")

With Custom Config Structs

package myapp

type Config struct {
	// ENV MYAPP_NAME: Application name (default: myapp)
	Name string
	
	// ENV MYAPP_DEBUG: Enable debug mode (default: false)
	Debug bool
	
	// ENV MYAPP_API_KEY: API key for external service (required)
	APIKey string
}

func ConfigFromEnv() (*Config, error) {
	cfg := &Config{
		Name:   env.String("MYAPP_NAME", "myapp"),
		Debug:  env.Bool("MYAPP_DEBUG", false),
		APIKey: env.String("MYAPP_API_KEY", ""),
	}
	
	if cfg.APIKey == "" {
		return nil, errors.New("MYAPP_API_KEY is required")
	}
	
	return cfg, nil
}

// In your main application
loader := ezconf.New()
loader.AddPackagePath("./myapp")
loader.AddConfigFunc("myapp", func() (interface{}, error) {
	return myapp.ConfigFromEnv()
})

Best Practices

1. Use Consistent Comment Format

Always document your config struct fields with ENV comments:

type Config struct {
	// ENV VARNAME: Description (modifiers)
	FieldName string
}

Create a single Config struct per package rather than multiple structs:

// Good
type Config struct {
	Host string
	Port int
	TLS  bool
}

// Avoid
type HostConfig struct {
	Host string
}
type PortConfig struct {
	Port int
}

3. Use Descriptive Names

Environment variable names should be clear and prefixed with the package/app name:

// Good
// ENV MYAPP_DATABASE_URL: Database connection string

// Avoid
// ENV DB: Database

4. Document Requirements Clearly

Always indicate if a variable is required and under what conditions:

// ENV LOG_DIR: Directory for log files (required when LOG_OUTPUT is file or both)

5. Provide Sensible Defaults

Include default values for optional configuration:

// ENV LOG_LEVEL: Log level (default: info)

6. Generate .env Templates

Use ezconf to generate .env template files for your application:

// In a separate tool or init command
loader := ezconf.New()
// Add all packages...
loader.Load()
loader.GenerateEnvFile(".env.example", false)

Troubleshooting

ENV Comments Not Being Parsed

Problem: Environment variables are not showing up after parsing.

Solutions:

  • Ensure comments start with ENV (with space after ENV)
  • Check that the comment includes a colon after the variable name
  • Verify the package path is correct and points to the actual source files
  • Make sure files are not excluded (like test files)

Config Function Fails to Load

Problem: Load() returns an error from a config function.

Solutions:

  • Check that required environment variables are set
  • Verify the ConfigFromEnv function handles errors properly
  • Ensure the config function signature matches func() (interface{}, error)

.env File Update Doesn't Work

Problem: UpdateEnvFile doesn't preserve existing values.

Solutions:

  • Ensure the file exists or use createIfNotExist: true
  • Check file permissions (needs write access)
  • Verify the .env file format is correct (KEY=value lines)

Missing Environment Variables in Output

Problem: Some expected variables don't appear in the output.

Solutions:

  • Check if the struct field has an ENV comment
  • Verify the package was added with AddPackagePath
  • Ensure Load() was called before accessing env vars
  • Check that the file isn't a test file (*_test.go)

See Also

  • HLog - Structured logging with ConfigFromEnv
  • HWS - HTTP web server with ConfigFromEnv
  • HWSAuth - Authentication with ConfigFromEnv
  • Env - Environment variable helpers