Files
bc-netts-energy/internal/config/config.go
2025-11-03 19:26:48 +08:00

275 lines
7.1 KiB
Go

package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"gopkg.in/yaml.v3"
)
const (
defaultConfigFile = "config.yaml"
envPrefix = "NETTS"
)
// Config represents the full application configuration.
type Config struct {
Server ServerConfig `yaml:"server"`
Netts NettsConfig `yaml:"netts"`
Energy EnergyConfig `yaml:"energy"`
Logging LoggingConfig `yaml:"logging"`
}
// ServerConfig controls HTTP server behaviour.
type ServerConfig struct {
Address string `yaml:"address"`
ReadTimeout Duration `yaml:"readTimeout"`
WriteTimeout Duration `yaml:"writeTimeout"`
IdleTimeout Duration `yaml:"idleTimeout"`
ShutdownTimeout Duration `yaml:"shutdownTimeout"`
}
// NettsConfig contains credentials and settings for the Netts API.
type NettsConfig struct {
APIKey string `yaml:"apiKey"`
BaseURL string `yaml:"baseUrl"`
RealIP string `yaml:"realIp"`
CallbackURL string `yaml:"callbackUrl"`
HTTPTimeout Duration `yaml:"httpTimeout"`
Retry Retry `yaml:"retry"`
}
// Retry controls API retry behaviour.
type Retry struct {
MaxAttempts int `yaml:"maxAttempts"`
Backoff Duration `yaml:"backoff"`
MaxBackoff Duration `yaml:"maxBackoff"`
}
// EnergyConfig controls orchestration thresholds.
type EnergyConfig struct {
AutoAddHost bool `yaml:"autoAddHost"`
MinCycles int `yaml:"minCycles"`
TargetCycles int `yaml:"targetCycles"`
MinEnergyThreshold int `yaml:"minEnergyThreshold"`
PostOrderWait Duration `yaml:"postOrderWait"`
DefaultAnalyzeValue string `yaml:"defaultAnalyzeValue"`
}
// LoggingConfig controls logger behaviour.
type LoggingConfig struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
}
// Load reads configuration from the provided path or default locations.
func Load(path string) (*Config, error) {
cfg := defaultConfig()
configFile, err := resolveConfigPath(path)
if err != nil {
return nil, err
}
if configFile != "" {
raw, err := os.ReadFile(configFile)
if err != nil {
return nil, fmt.Errorf("read config file %q: %w", configFile, err)
}
if err := yaml.Unmarshal(raw, &cfg); err != nil {
return nil, fmt.Errorf("parse config file %q: %w", configFile, err)
}
}
applyDefaults(&cfg)
applyEnvOverrides(&cfg)
if err := cfg.Validate(); err != nil {
return nil, err
}
return &cfg, nil
}
// DefaultConfig returns a configuration populated with defaults.
func defaultConfig() Config {
return Config{
Server: ServerConfig{
Address: ":8080",
ReadTimeout: Duration(10 * time.Second),
WriteTimeout: Duration(10 * time.Second),
IdleTimeout: Duration(60 * time.Second),
ShutdownTimeout: Duration(15 * time.Second),
},
Netts: NettsConfig{
BaseURL: "https://netts.io",
HTTPTimeout: Duration(15 * time.Second),
Retry: Retry{
MaxAttempts: 3,
Backoff: Duration(2 * time.Second),
MaxBackoff: Duration(10 * time.Second),
},
},
Energy: EnergyConfig{
AutoAddHost: true,
MinCycles: 3,
TargetCycles: 10,
MinEnergyThreshold: 32000,
PostOrderWait: Duration(3 * time.Second),
DefaultAnalyzeValue: "100.00",
},
Logging: LoggingConfig{
Level: "info",
Format: "text",
},
}
}
func applyDefaults(cfg *Config) {
if strings.TrimSpace(cfg.Server.Address) == "" {
cfg.Server.Address = ":8080"
}
cfg.Server.ReadTimeout.SetDefault(10 * time.Second)
cfg.Server.WriteTimeout.SetDefault(10 * time.Second)
cfg.Server.IdleTimeout.SetDefault(60 * time.Second)
cfg.Server.ShutdownTimeout.SetDefault(15 * time.Second)
if strings.TrimSpace(cfg.Netts.BaseURL) == "" {
cfg.Netts.BaseURL = "https://netts.io"
}
cfg.Netts.HTTPTimeout.SetDefault(15 * time.Second)
if cfg.Netts.Retry.MaxAttempts <= 0 {
cfg.Netts.Retry.MaxAttempts = 3
}
cfg.Netts.Retry.Backoff.SetDefault(2 * time.Second)
cfg.Netts.Retry.MaxBackoff.SetDefault(10 * time.Second)
if cfg.Energy.MinCycles <= 0 {
cfg.Energy.MinCycles = 3
}
if cfg.Energy.TargetCycles <= 0 {
cfg.Energy.TargetCycles = 10
}
if cfg.Energy.TargetCycles < cfg.Energy.MinCycles {
cfg.Energy.TargetCycles = cfg.Energy.MinCycles
}
if cfg.Energy.MinEnergyThreshold <= 0 {
cfg.Energy.MinEnergyThreshold = 32000
}
cfg.Energy.PostOrderWait.SetDefault(3 * time.Second)
if cfg.Energy.DefaultAnalyzeValue == "" {
cfg.Energy.DefaultAnalyzeValue = "100.00"
}
if cfg.Logging.Level == "" {
cfg.Logging.Level = "info"
}
if cfg.Logging.Format == "" {
cfg.Logging.Format = "text"
}
}
func applyEnvOverrides(cfg *Config) {
if v := os.Getenv(envVar("API_KEY")); v != "" {
cfg.Netts.APIKey = v
}
if v := os.Getenv(envVar("BASE_URL")); v != "" {
cfg.Netts.BaseURL = v
}
if v := os.Getenv(envVar("REAL_IP")); v != "" {
cfg.Netts.RealIP = v
}
if v := os.Getenv(envVar("CALLBACK_URL")); v != "" {
cfg.Netts.CallbackURL = v
}
if v := os.Getenv(envVar("AUTO_ADD_HOST")); v != "" {
cfg.Energy.AutoAddHost = strings.EqualFold(v, "true") || v == "1"
}
if v := os.Getenv(envVar("MIN_CYCLES")); v != "" {
if parsed, err := parseInt(v); err == nil {
cfg.Energy.MinCycles = parsed
}
}
if v := os.Getenv(envVar("TARGET_CYCLES")); v != "" {
if parsed, err := parseInt(v); err == nil {
cfg.Energy.TargetCycles = parsed
}
}
if v := os.Getenv(envVar("MIN_ENERGY_THRESHOLD")); v != "" {
if parsed, err := parseInt(v); err == nil {
cfg.Energy.MinEnergyThreshold = parsed
}
}
if v := os.Getenv(envVar("LOG_LEVEL")); v != "" {
cfg.Logging.Level = v
}
if v := os.Getenv(envVar("LOG_FORMAT")); v != "" {
cfg.Logging.Format = v
}
}
// Validate performs configuration validation.
func (c *Config) Validate() error {
var problems []string
if strings.TrimSpace(c.Netts.APIKey) == "" {
problems = append(problems, "netts.apiKey is required (or NETTS_API_KEY env)")
}
if !strings.HasPrefix(c.Netts.BaseURL, "http") {
problems = append(problems, "netts.baseUrl must be an absolute URL")
}
if c.Energy.MinCycles <= 0 {
problems = append(problems, "energy.minCycles must be > 0")
}
if c.Energy.TargetCycles < c.Energy.MinCycles {
problems = append(problems, "energy.targetCycles must be >= energy.minCycles")
}
if c.Energy.MinEnergyThreshold <= 0 {
problems = append(problems, "energy.minEnergyThreshold must be > 0")
}
if len(problems) > 0 {
return errors.New(strings.Join(problems, "; "))
}
return nil
}
func resolveConfigPath(path string) (string, error) {
if path != "" {
if _, err := os.Stat(path); err != nil {
return "", fmt.Errorf("config file %q: %w", path, err)
}
return path, nil
}
searchPaths := []string{
defaultConfigFile,
filepath.Join("config", defaultConfigFile),
filepath.Join("..", defaultConfigFile),
}
for _, candidate := range searchPaths {
if _, err := os.Stat(candidate); err == nil {
return candidate, nil
}
}
return "", nil
}
func envVar(key string) string {
return fmt.Sprintf("%s_%s", envPrefix, strings.ToUpper(strings.ReplaceAll(key, ".", "_")))
}
func parseInt(value string) (int, error) {
var parsed int
_, err := fmt.Sscanf(value, "%d", &parsed)
if err != nil {
return 0, fmt.Errorf("parse int %q: %w", value, err)
}
return parsed, nil
}