package config import ( "bytes" "fmt" "net" "os" "time" "gopkg.in/yaml.v3" ) const DefaultPath = "config/local.yaml" // Config 汇总整个网关服务的运行参数。 type Config struct { App AppConfig `yaml:"app"` GRPC GRPCConfig `yaml:"grpc"` } // AppConfig 描述应用自身参数。 type AppConfig struct { Name string `yaml:"name"` Env string `yaml:"env"` HTTPAddr string `yaml:"http_addr"` ShutdownTimeout time.Duration `yaml:"shutdown_timeout"` } // GRPCConfig 聚合所有下游 gRPC 服务配置。 type GRPCConfig struct { User UpstreamConfig `yaml:"user"` Pay UpstreamConfig `yaml:"pay"` } // UpstreamConfig 描述单个下游服务的节点和容错策略。 type UpstreamConfig struct { // Target 保留兼容旧配置;内部会归一化到 Targets。 Target string `yaml:"target,omitempty"` Targets []string `yaml:"targets"` Timeout time.Duration `yaml:"timeout"` Retry RetryConfig `yaml:"retry"` CircuitBreaker CircuitBreakerConfig `yaml:"circuit_breaker"` HealthCache HealthCacheConfig `yaml:"health_cache"` } // RetryConfig 控制单次请求在多个节点间的重试行为。 type RetryConfig struct { MaxAttempts int `yaml:"max_attempts"` Backoff time.Duration `yaml:"backoff"` } // CircuitBreakerConfig 控制连续失败后的断路时长。 type CircuitBreakerConfig struct { FailureThreshold int `yaml:"failure_threshold"` OpenTimeout time.Duration `yaml:"open_timeout"` } // HealthCacheConfig 控制 readiness 对下游健康状态缓存多久。 type HealthCacheConfig struct { TTL time.Duration `yaml:"ttl"` } // Load 从 YAML 文件加载配置,并补齐默认值和校验。 func Load(path string) (Config, error) { data, err := os.ReadFile(path) if err != nil { return Config{}, fmt.Errorf("read config file %s: %w", path, err) } cfg := defaultConfig() decoder := yaml.NewDecoder(bytes.NewReader(data)) decoder.KnownFields(true) if err := decoder.Decode(&cfg); err != nil { return Config{}, fmt.Errorf("decode config file %s: %w", path, err) } cfg.normalize() if err := validate(cfg); err != nil { return Config{}, err } return cfg, nil } func defaultConfig() Config { return Config{ App: AppConfig{ Name: "chatappgateway", Env: "local", HTTPAddr: ":8080", ShutdownTimeout: 10 * time.Second, }, GRPC: GRPCConfig{ User: defaultUpstreamConfig("127.0.0.1:9001"), Pay: defaultUpstreamConfig("127.0.0.1:9002"), }, } } func defaultUpstreamConfig(target string) UpstreamConfig { return UpstreamConfig{ Targets: []string{target}, Timeout: 3 * time.Second, Retry: RetryConfig{ MaxAttempts: 2, Backoff: 200 * time.Millisecond, }, CircuitBreaker: CircuitBreakerConfig{ FailureThreshold: 3, OpenTimeout: 10 * time.Second, }, HealthCache: HealthCacheConfig{ TTL: 2 * time.Second, }, } } func (c *Config) normalize() { c.GRPC.User = normalizeUpstreamConfig(c.GRPC.User) c.GRPC.Pay = normalizeUpstreamConfig(c.GRPC.Pay) } func normalizeUpstreamConfig(cfg UpstreamConfig) UpstreamConfig { if len(cfg.Targets) == 0 && cfg.Target != "" { cfg.Targets = []string{cfg.Target} } cfg.Target = "" return cfg } func validate(cfg Config) error { if cfg.App.Name == "" { return fmt.Errorf("app.name is required") } if _, err := net.ResolveTCPAddr("tcp", cfg.App.HTTPAddr); err != nil { return fmt.Errorf("app.http_addr is invalid: %w", err) } if cfg.App.ShutdownTimeout <= 0 { return fmt.Errorf("app.shutdown_timeout must be greater than 0") } if err := validateUpstream("grpc.user", cfg.GRPC.User); err != nil { return err } if err := validateUpstream("grpc.pay", cfg.GRPC.Pay); err != nil { return err } return nil } func validateUpstream(name string, cfg UpstreamConfig) error { if len(cfg.Targets) == 0 { return fmt.Errorf("%s.targets must contain at least one target", name) } for _, target := range cfg.Targets { if _, err := net.ResolveTCPAddr("tcp", target); err != nil { return fmt.Errorf("%s.targets contains invalid target %q: %w", name, target, err) } } if cfg.Timeout <= 0 { return fmt.Errorf("%s.timeout must be greater than 0", name) } if cfg.Retry.MaxAttempts <= 0 { return fmt.Errorf("%s.retry.max_attempts must be greater than 0", name) } if cfg.Retry.Backoff < 0 { return fmt.Errorf("%s.retry.backoff must be greater than or equal to 0", name) } if cfg.CircuitBreaker.FailureThreshold <= 0 { return fmt.Errorf("%s.circuit_breaker.failure_threshold must be greater than 0", name) } if cfg.CircuitBreaker.OpenTimeout <= 0 { return fmt.Errorf("%s.circuit_breaker.open_timeout must be greater than 0", name) } if cfg.HealthCache.TTL <= 0 { return fmt.Errorf("%s.health_cache.ttl must be greater than 0", name) } return nil }