config: GIN_MODE 改为环境变量配置 + Load() 自动加载 .env
main.go 硬编码 gin.ReleaseMode 改成读 cfg.GinMode(默认 release),
方便生产/调试切换 debug/release/test。Load() 开头先调 loadDotenv
(".env"): 缺文件静默跳过,shell env 优先于 .env,跳过注释/空行/
无 '=' 行并 slog.Warn。仅用标准库,不引新依赖。
验证:
- go build / go vet / go test 全部通过
- shell env 覆盖 .env (LISTEN_ADDR=:19090 覆盖 .env 的 :18080)
- 仅 .env 时正确加载 (监听 18080, GIN_MODE=debug 输出 [GIN-debug])
- 无 .env 且无 shell env 时清晰报错 ADMIN_TOKENS is not set
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,9 @@ LOG_LEVEL=info
|
||||
# Log format: text (terminal) or json (log file; default: text)
|
||||
LOG_FORMAT=text
|
||||
|
||||
# Gin mode: debug, release, test (default: release)
|
||||
GIN_MODE=release
|
||||
|
||||
# Analyzer heuristic thresholds (defaults shown)
|
||||
ANALYZER_DATA_SKEW_RATIO=3.0
|
||||
ANALYZER_GC_PRESSURE_RATIO=0.1
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -23,6 +25,7 @@ type Config struct {
|
||||
LogDir string
|
||||
LogLevel string
|
||||
LogFormat string
|
||||
GinMode string
|
||||
AnalyzerDataSkewRatio float64
|
||||
AnalyzerGCPressureRatio float64
|
||||
AnalyzerBottleneckShuffleGB float64
|
||||
@@ -31,6 +34,10 @@ type Config struct {
|
||||
// Load reads configuration from environment variables and returns a populated Config.
|
||||
// Required variables (ADMIN_TOKENS, AGENT_TOKEN) produce clear errors when missing.
|
||||
func Load() (*Config, error) {
|
||||
if err := loadDotenv(".env"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &Config{
|
||||
ListenAddr: ":8080",
|
||||
DataDir: "./data",
|
||||
@@ -40,6 +47,7 @@ func Load() (*Config, error) {
|
||||
LogDir: "./data/logs",
|
||||
LogLevel: "info",
|
||||
LogFormat: "text",
|
||||
GinMode: "release",
|
||||
AnalyzerDataSkewRatio: 3.0,
|
||||
AnalyzerGCPressureRatio: 0.1,
|
||||
AnalyzerBottleneckShuffleGB: 50.0,
|
||||
@@ -75,6 +83,7 @@ func Load() (*Config, error) {
|
||||
cfg.LogDir = envString("LOG_DIR", cfg.LogDir)
|
||||
cfg.LogLevel = envString("LOG_LEVEL", cfg.LogLevel)
|
||||
cfg.LogFormat = envString("LOG_FORMAT", cfg.LogFormat)
|
||||
cfg.GinMode = envString("GIN_MODE", cfg.GinMode)
|
||||
|
||||
cfg.AnalyzerDataSkewRatio, err = parseFloat64("ANALYZER_DATA_SKEW_RATIO", cfg.AnalyzerDataSkewRatio)
|
||||
if err != nil {
|
||||
@@ -97,6 +106,9 @@ func Load() (*Config, error) {
|
||||
if err := validateLogFormat(cfg.LogFormat); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateGinMode(cfg.GinMode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg.SQLitePath = filepath.Join(cfg.DataDir, "spark-mcp.db")
|
||||
|
||||
@@ -192,6 +204,70 @@ func validateLogFormat(format string) error {
|
||||
return fmt.Errorf("config: invalid LOG_FORMAT %q, want text/json", format)
|
||||
}
|
||||
|
||||
func validateGinMode(mode string) error {
|
||||
switch mode {
|
||||
case "debug", "release", "test":
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("config: invalid GIN_MODE %q, want debug/release/test", mode)
|
||||
}
|
||||
|
||||
// loadDotenv reads KEY=VALUE pairs from path and sets them via os.Setenv only
|
||||
// when the variable is not already defined. Missing files are ignored.
|
||||
func loadDotenv(path string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("config: failed to open %s: %w", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
eq := strings.Index(line, "=")
|
||||
if eq < 0 {
|
||||
slog.Warn("config: .env line without '=', skipping", "line", line)
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(line[:eq])
|
||||
value := strings.TrimSpace(line[eq+1:])
|
||||
value = dequote(value)
|
||||
|
||||
if key == "" {
|
||||
slog.Warn("config: .env line with empty key, skipping", "line", line)
|
||||
continue
|
||||
}
|
||||
|
||||
if os.Getenv(key) == "" {
|
||||
if err := os.Setenv(key, value); err != nil {
|
||||
return fmt.Errorf("config: failed to set env %s: %w", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("config: failed to read %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dequote(s string) string {
|
||||
if len(s) >= 2 {
|
||||
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
|
||||
return s[1 : len(s)-1]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// String returns a human-readable representation of the configuration.
|
||||
// Sensitive values (AdminTokens and AgentToken) are summarized, not printed.
|
||||
func (c *Config) String() string {
|
||||
@@ -206,7 +282,7 @@ func (c *Config) String() string {
|
||||
"ListenAddr=%s DataDir=%s SQLitePath=%s AdminTokens=%s AgentToken=%s "+
|
||||
"HTTPClientTimeout=%s MaxResponseBytes=%d SparkSubmitTimeout=%s "+
|
||||
"LogDir=%s LogLevel=%s LogFormat=%s AnalyzerDataSkewRatio=%.1f "+
|
||||
"AnalyzerGCPressureRatio=%.1f AnalyzerBottleneckShuffleGB=%.1f",
|
||||
"AnalyzerGCPressureRatio=%.1f AnalyzerBottleneckShuffleGB=%.1f GinMode=%s",
|
||||
c.ListenAddr,
|
||||
c.DataDir,
|
||||
c.SQLitePath,
|
||||
@@ -221,5 +297,6 @@ func (c *Config) String() string {
|
||||
c.AnalyzerDataSkewRatio,
|
||||
c.AnalyzerGCPressureRatio,
|
||||
c.AnalyzerBottleneckShuffleGB,
|
||||
c.GinMode,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user