Files
go-skeleton/config/config.go
T

318 lines
8.4 KiB
Go

package config
import (
"fmt"
"os"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
type AppConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Version string `yaml:"version"`
Debug bool `yaml:"debug"`
Timezone string `yaml:"timezone"`
Environment string `yaml:"environment"`
}
type LoggerConfig struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
Output string `yaml:"output"`
Filename string `yaml:"filename"`
MaxSize int `yaml:"max_size"`
MaxAge int `yaml:"max_age"`
MaxBackups int `yaml:"max_backups"`
Compress bool `yaml:"compress"`
}
type DatabaseConfig struct {
Driver string `yaml:"driver"`
DSN string `yaml:"dsn"`
Host string `yaml:"host"`
Port int `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
DBName string `yaml:"dbname"`
SSLMode string `yaml:"sslmode"`
SQLitePath string `yaml:"sqlite_path"`
MaxIdleConns int `yaml:"max_idle_conns"`
MaxOpenConns int `yaml:"max_open_conns"`
ConnMaxLifetime int `yaml:"conn_max_lifetime"`
}
type RedisConfig struct {
Enabled bool `yaml:"enabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
Password string `yaml:"password"`
Database int `yaml:"database"`
PoolSize int `yaml:"pool_size"`
MinIdleConns int `yaml:"min_idle_conns"`
MaxRetries int `yaml:"max_retries"`
}
type JWTConfig struct {
Secret string `yaml:"secret"`
ExpireHours int `yaml:"expire_hours"` // 兼容旧配置,作为 access token 默认值
AccessExpireMinutes int `yaml:"access_expire_minutes"`
RefreshExpireHours int `yaml:"refresh_expire_hours"`
Issuer string `yaml:"issuer"`
}
type ObservabilityConfig struct {
MetricsEnabled bool `yaml:"metrics_enabled"`
MetricsPath string `yaml:"metrics_path"`
TracingEnabled bool `yaml:"tracing_enabled"`
TracingService string `yaml:"tracing_service"`
TracingEndpoint string `yaml:"tracing_endpoint"`
TracingInsecure bool `yaml:"tracing_insecure"`
}
type Config struct {
App AppConfig `yaml:"app"`
Logger LoggerConfig `yaml:"logger"`
Database DatabaseConfig `yaml:"database"`
Redis RedisConfig `yaml:"redis"`
JWT JWTConfig `yaml:"jwt"`
Observability ObservabilityConfig `yaml:"observability"`
}
func Load() (*Config, error) {
path := os.Getenv("SKELETON_CONFIG")
if path == "" {
path = "config/app.yaml"
}
return LoadFrom(path)
}
func LoadFrom(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
cfg.applyEnvironment()
cfg.setDefaults()
if err := cfg.validate(); err != nil {
return nil, err
}
return &cfg, nil
}
func (c *Config) validate() error {
switch c.Database.Driver {
case "postgres", "mysql", "sqlite":
default:
return fmt.Errorf("不支持的数据库驱动 %q", c.Database.Driver)
}
if strings.EqualFold(c.App.Environment, "production") &&
(c.JWT.Secret == "change-this-secret-key-in-production" || len(c.JWT.Secret) < 32) {
return fmt.Errorf("生产环境 JWT_SECRET 必须修改且至少 32 个字符")
}
return nil
}
func (c *Config) applyEnvironment() {
stringEnv("APP_HOST", &c.App.Host)
intEnv("APP_PORT", &c.App.Port)
stringEnv("APP_VERSION", &c.App.Version)
boolEnv("APP_DEBUG", &c.App.Debug)
stringEnv("APP_TIMEZONE", &c.App.Timezone)
stringEnv("APP_ENVIRONMENT", &c.App.Environment)
stringEnv("LOG_LEVEL", &c.Logger.Level)
stringEnv("LOG_FORMAT", &c.Logger.Format)
stringEnv("LOG_OUTPUT", &c.Logger.Output)
stringEnv("LOG_FILENAME", &c.Logger.Filename)
stringEnv("DATABASE_DRIVER", &c.Database.Driver)
stringEnv("DATABASE_DSN", &c.Database.DSN)
stringEnv("DATABASE_HOST", &c.Database.Host)
intEnv("DATABASE_PORT", &c.Database.Port)
stringEnv("DATABASE_USERNAME", &c.Database.Username)
stringEnv("DATABASE_PASSWORD", &c.Database.Password)
stringEnv("DATABASE_NAME", &c.Database.DBName)
stringEnv("DATABASE_SSLMODE", &c.Database.SSLMode)
stringEnv("DATABASE_SQLITE_PATH", &c.Database.SQLitePath)
intEnv("DATABASE_MAX_IDLE_CONNS", &c.Database.MaxIdleConns)
intEnv("DATABASE_MAX_OPEN_CONNS", &c.Database.MaxOpenConns)
intEnv("DATABASE_CONN_MAX_LIFETIME", &c.Database.ConnMaxLifetime)
boolEnv("REDIS_ENABLED", &c.Redis.Enabled)
stringEnv("REDIS_HOST", &c.Redis.Host)
intEnv("REDIS_PORT", &c.Redis.Port)
stringEnv("REDIS_PASSWORD", &c.Redis.Password)
intEnv("REDIS_DATABASE", &c.Redis.Database)
stringEnv("JWT_SECRET", &c.JWT.Secret)
intEnv("JWT_ACCESS_EXPIRE_MINUTES", &c.JWT.AccessExpireMinutes)
intEnv("JWT_REFRESH_EXPIRE_HOURS", &c.JWT.RefreshExpireHours)
stringEnv("JWT_ISSUER", &c.JWT.Issuer)
boolEnv("METRICS_ENABLED", &c.Observability.MetricsEnabled)
stringEnv("METRICS_PATH", &c.Observability.MetricsPath)
boolEnv("TRACING_ENABLED", &c.Observability.TracingEnabled)
stringEnv("OTEL_SERVICE_NAME", &c.Observability.TracingService)
stringEnv("OTEL_EXPORTER_OTLP_ENDPOINT", &c.Observability.TracingEndpoint)
boolEnv("OTEL_EXPORTER_OTLP_INSECURE", &c.Observability.TracingInsecure)
}
func stringEnv(key string, target *string) {
if value, ok := os.LookupEnv(key); ok {
*target = value
}
}
func intEnv(key string, target *int) {
if value, ok := os.LookupEnv(key); ok {
if parsed, err := strconv.Atoi(value); err == nil {
*target = parsed
}
}
}
func boolEnv(key string, target *bool) {
if value, ok := os.LookupEnv(key); ok {
if parsed, err := strconv.ParseBool(value); err == nil {
*target = parsed
}
}
}
func (c *Config) setDefaults() {
if c.App.Host == "" {
c.App.Host = "0.0.0.0"
}
if c.App.Port == 0 {
c.App.Port = 8080
}
if c.App.Version == "" {
c.App.Version = "0.1.0"
}
if c.App.Timezone == "" {
c.App.Timezone = "UTC"
}
if c.App.Environment == "" {
c.App.Environment = "development"
}
if c.Logger.Level == "" {
c.Logger.Level = "info"
}
if c.Logger.Format == "" {
c.Logger.Format = "console"
}
if c.Logger.Output == "" {
c.Logger.Output = "stdout"
}
if c.Logger.MaxSize == 0 {
c.Logger.MaxSize = 100
}
if c.Logger.MaxAge == 0 {
c.Logger.MaxAge = 30
}
if c.Logger.MaxBackups == 0 {
c.Logger.MaxBackups = 3
}
if c.Database.Driver == "" {
c.Database.Driver = "postgres"
}
c.Database.Driver = strings.ToLower(strings.TrimSpace(c.Database.Driver))
switch c.Database.Driver {
case "postgresql":
c.Database.Driver = "postgres"
case "sqlite3":
c.Database.Driver = "sqlite"
}
if c.Database.Port == 0 {
switch c.Database.Driver {
case "postgres":
c.Database.Port = 5432
case "mysql":
c.Database.Port = 3306
}
}
if c.Database.SSLMode == "" {
c.Database.SSLMode = "disable"
}
if c.Database.SQLitePath == "" {
c.Database.SQLitePath = "data/skeleton.db"
}
if c.Database.MaxIdleConns == 0 {
if c.Database.Driver == "sqlite" {
c.Database.MaxIdleConns = 1
} else {
c.Database.MaxIdleConns = 10
}
}
if c.Database.MaxOpenConns == 0 {
if c.Database.Driver == "sqlite" {
c.Database.MaxOpenConns = 1
} else {
c.Database.MaxOpenConns = 100
}
}
if c.Database.ConnMaxLifetime == 0 {
c.Database.ConnMaxLifetime = 60
}
if c.Redis.Host == "" {
c.Redis.Host = "localhost"
}
if c.Redis.Port == 0 {
c.Redis.Port = 6379
}
if c.Redis.Database == 0 {
c.Redis.Database = 0
}
if c.Redis.PoolSize == 0 {
c.Redis.PoolSize = 10
}
if c.Redis.MinIdleConns == 0 {
c.Redis.MinIdleConns = 5
}
if c.Redis.MaxRetries == 0 {
c.Redis.MaxRetries = 3
}
if c.JWT.Secret == "" {
c.JWT.Secret = "change-this-secret-key-in-production"
}
if c.JWT.ExpireHours == 0 {
c.JWT.ExpireHours = 24
}
if c.JWT.AccessExpireMinutes == 0 {
c.JWT.AccessExpireMinutes = c.JWT.ExpireHours * 60
}
if c.JWT.RefreshExpireHours == 0 {
c.JWT.RefreshExpireHours = 24 * 7
}
if c.JWT.Issuer == "" {
c.JWT.Issuer = "HeTianXia"
}
if c.Observability.MetricsPath == "" {
c.Observability.MetricsPath = "/metrics"
}
if c.Observability.TracingService == "" {
c.Observability.TracingService = "skeleton"
}
if c.Observability.TracingEndpoint == "" {
c.Observability.TracingEndpoint = "localhost:4318"
}
}
func (c *Config) GetAddr() string {
return fmt.Sprintf("%s:%d", c.App.Host, c.App.Port)
}