Initial commit
This commit is contained in:
169
internal/config/config.go
Normal file
169
internal/config/config.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// @tag config
|
||||
// @tag core
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/eleksir/pekoe/internal/logger"
|
||||
)
|
||||
|
||||
// Constants for default values - chosen to be production-ready defaults
|
||||
const (
|
||||
DefaultBind = "0.0.0.0"
|
||||
DefaultPort = 6379
|
||||
DefaultMaxClients = 10000
|
||||
DefaultReadTimeout = 0 // 0 means no timeout (blocking I/O)
|
||||
DefaultWriteTimeout = 0 // 0 means no timeout
|
||||
DefaultMaxMsgBytes = 512 * 1024 * 1024 // 512 MB - allows large messages
|
||||
DefaultLogLevel = "info"
|
||||
DefaultLogFile = "" // empty = stdout only
|
||||
DefaultPidFile = "" // empty = no PID file created
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Bind string `json:"bind"`
|
||||
Port int `json:"port"`
|
||||
MaxClients int `json:"max_clients"`
|
||||
ReadTimeout int `json:"read_timeout"` // in seconds, 0 = no timeout
|
||||
WriteTimeout int `json:"write_timeout"` // in seconds, 0 = no timeout
|
||||
MaxMsgBytes int `json:"max_message_bytes"`
|
||||
LogLevel string `json:"log_level"`
|
||||
LogFile string `json:"log_file"` // empty = stdout only
|
||||
PidFile string `json:"pid_file"` // empty = no PID file
|
||||
}
|
||||
|
||||
// SetDefaults applies sensible defaults when config values are missing or invalid.
|
||||
// This ensures the server can start even with incomplete configuration.
|
||||
// nolint: cyclop
|
||||
func (c *Config) SetDefaults() {
|
||||
if c.Bind == "" {
|
||||
c.Bind = DefaultBind
|
||||
}
|
||||
if c.Port <= 0 || c.Port > 65535 {
|
||||
c.Port = DefaultPort
|
||||
}
|
||||
if c.MaxClients <= 0 {
|
||||
c.MaxClients = DefaultMaxClients
|
||||
}
|
||||
if c.ReadTimeout < 0 {
|
||||
c.ReadTimeout = 0
|
||||
}
|
||||
if c.WriteTimeout < 0 {
|
||||
c.WriteTimeout = 0
|
||||
}
|
||||
if c.MaxMsgBytes <= 0 {
|
||||
c.MaxMsgBytes = DefaultMaxMsgBytes
|
||||
}
|
||||
if c.LogLevel == "" {
|
||||
c.LogLevel = DefaultLogLevel
|
||||
}
|
||||
if c.LogFile == "" {
|
||||
c.LogFile = DefaultLogFile
|
||||
}
|
||||
if c.PidFile == "" {
|
||||
c.PidFile = DefaultPidFile
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks that all configuration values are within acceptable bounds.
|
||||
// Returns an error if validation fails, which should prevent server startup.
|
||||
func (c *Config) Validate() error {
|
||||
if c.Bind == "" {
|
||||
return fmt.Errorf("bind address cannot be empty")
|
||||
}
|
||||
if net.ParseIP(c.Bind) == nil {
|
||||
return fmt.Errorf("bind must be a valid IP address (e.g. 0.0.0.0, ::, ::1, 192.168.1.1)")
|
||||
}
|
||||
if c.Port <= 0 || c.Port > 65535 {
|
||||
return fmt.Errorf("port must be between 1 and 65535")
|
||||
}
|
||||
if c.MaxClients <= 0 {
|
||||
return fmt.Errorf("max_clients must be positive")
|
||||
}
|
||||
if c.MaxMsgBytes <= 0 {
|
||||
return fmt.Errorf("max_message_bytes must be positive")
|
||||
}
|
||||
// Validate log level against allowed values
|
||||
validLevels := map[string]bool{
|
||||
"debug": true,
|
||||
"info": true,
|
||||
"warn": true,
|
||||
"error": true,
|
||||
}
|
||||
if !validLevels[c.LogLevel] {
|
||||
return fmt.Errorf("invalid log_level: %s (must be debug, info, warn, or error)", c.LogLevel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadTimeoutDuration converts the config's read timeout (in seconds) to a Duration.
|
||||
// Zero means no timeout, allowing the server to block on reads indefinitely.
|
||||
func (c *Config) ReadTimeoutDuration() time.Duration {
|
||||
if c.ReadTimeout == 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(c.ReadTimeout) * time.Second
|
||||
}
|
||||
|
||||
// WriteTimeoutDuration converts the config's write timeout (in seconds) to a Duration.
|
||||
func (c *Config) WriteTimeoutDuration() time.Duration {
|
||||
if c.WriteTimeout == 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(c.WriteTimeout) * time.Second
|
||||
}
|
||||
|
||||
// Load reads and parses the configuration from a JSON file.
|
||||
// It applies defaults for missing/invalid values and validates the final config.
|
||||
func Load(path string) (*Config, error) {
|
||||
//nolint: gosec
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
// Apply defaults before validation to ensure minimum viable config
|
||||
cfg.SetDefaults()
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("config validation failed: %w", err)
|
||||
}
|
||||
|
||||
logger.SetLevel(cfg.LogLevel)
|
||||
|
||||
if cfg.LogFile != "" {
|
||||
if err := logger.SetOutput(cfg.LogFile); err != nil {
|
||||
return nil, fmt.Errorf("failed to set log file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// Addr returns the bind address in host:port format for use in net.Listen.
|
||||
// IPv6 addresses are wrapped in brackets: [::]:6379.
|
||||
func (c *Config) Addr() string {
|
||||
ip := net.ParseIP(c.Bind)
|
||||
if ip != nil && ip.To4() == nil {
|
||||
// IPv6 address — wrap in brackets
|
||||
return "[" + c.Bind + "]:" + strconv.Itoa(c.Port)
|
||||
}
|
||||
return c.Bind + ":" + strconv.Itoa(c.Port)
|
||||
}
|
||||
|
||||
// String returns a JSON representation of the config for debugging.
|
||||
func (c *Config) String() string {
|
||||
b, _ := json.MarshalIndent(c, "", " ")
|
||||
return string(b)
|
||||
}
|
||||
Reference in New Issue
Block a user