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:
tao.chen
2026-07-13 13:48:35 +08:00
co-authored by Claude
parent 756a88a800
commit 7f8960823b
3 changed files with 82 additions and 2 deletions
+3
View File
@@ -31,6 +31,9 @@ LOG_LEVEL=info
# Log format: text (terminal) or json (log file; default: text) # Log format: text (terminal) or json (log file; default: text)
LOG_FORMAT=text LOG_FORMAT=text
# Gin mode: debug, release, test (default: release)
GIN_MODE=release
# Analyzer heuristic thresholds (defaults shown) # Analyzer heuristic thresholds (defaults shown)
ANALYZER_DATA_SKEW_RATIO=3.0 ANALYZER_DATA_SKEW_RATIO=3.0
ANALYZER_GC_PRESSURE_RATIO=0.1 ANALYZER_GC_PRESSURE_RATIO=0.1
+78 -1
View File
@@ -2,7 +2,9 @@
package config package config
import ( import (
"bufio"
"fmt" "fmt"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -23,6 +25,7 @@ type Config struct {
LogDir string LogDir string
LogLevel string LogLevel string
LogFormat string LogFormat string
GinMode string
AnalyzerDataSkewRatio float64 AnalyzerDataSkewRatio float64
AnalyzerGCPressureRatio float64 AnalyzerGCPressureRatio float64
AnalyzerBottleneckShuffleGB float64 AnalyzerBottleneckShuffleGB float64
@@ -31,6 +34,10 @@ type Config struct {
// Load reads configuration from environment variables and returns a populated Config. // Load reads configuration from environment variables and returns a populated Config.
// Required variables (ADMIN_TOKENS, AGENT_TOKEN) produce clear errors when missing. // Required variables (ADMIN_TOKENS, AGENT_TOKEN) produce clear errors when missing.
func Load() (*Config, error) { func Load() (*Config, error) {
if err := loadDotenv(".env"); err != nil {
return nil, err
}
cfg := &Config{ cfg := &Config{
ListenAddr: ":8080", ListenAddr: ":8080",
DataDir: "./data", DataDir: "./data",
@@ -40,6 +47,7 @@ func Load() (*Config, error) {
LogDir: "./data/logs", LogDir: "./data/logs",
LogLevel: "info", LogLevel: "info",
LogFormat: "text", LogFormat: "text",
GinMode: "release",
AnalyzerDataSkewRatio: 3.0, AnalyzerDataSkewRatio: 3.0,
AnalyzerGCPressureRatio: 0.1, AnalyzerGCPressureRatio: 0.1,
AnalyzerBottleneckShuffleGB: 50.0, AnalyzerBottleneckShuffleGB: 50.0,
@@ -75,6 +83,7 @@ func Load() (*Config, error) {
cfg.LogDir = envString("LOG_DIR", cfg.LogDir) cfg.LogDir = envString("LOG_DIR", cfg.LogDir)
cfg.LogLevel = envString("LOG_LEVEL", cfg.LogLevel) cfg.LogLevel = envString("LOG_LEVEL", cfg.LogLevel)
cfg.LogFormat = envString("LOG_FORMAT", cfg.LogFormat) cfg.LogFormat = envString("LOG_FORMAT", cfg.LogFormat)
cfg.GinMode = envString("GIN_MODE", cfg.GinMode)
cfg.AnalyzerDataSkewRatio, err = parseFloat64("ANALYZER_DATA_SKEW_RATIO", cfg.AnalyzerDataSkewRatio) cfg.AnalyzerDataSkewRatio, err = parseFloat64("ANALYZER_DATA_SKEW_RATIO", cfg.AnalyzerDataSkewRatio)
if err != nil { if err != nil {
@@ -97,6 +106,9 @@ func Load() (*Config, error) {
if err := validateLogFormat(cfg.LogFormat); err != nil { if err := validateLogFormat(cfg.LogFormat); err != nil {
return nil, err return nil, err
} }
if err := validateGinMode(cfg.GinMode); err != nil {
return nil, err
}
cfg.SQLitePath = filepath.Join(cfg.DataDir, "spark-mcp.db") 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) 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. // String returns a human-readable representation of the configuration.
// Sensitive values (AdminTokens and AgentToken) are summarized, not printed. // Sensitive values (AdminTokens and AgentToken) are summarized, not printed.
func (c *Config) String() string { func (c *Config) String() string {
@@ -206,7 +282,7 @@ func (c *Config) String() string {
"ListenAddr=%s DataDir=%s SQLitePath=%s AdminTokens=%s AgentToken=%s "+ "ListenAddr=%s DataDir=%s SQLitePath=%s AdminTokens=%s AgentToken=%s "+
"HTTPClientTimeout=%s MaxResponseBytes=%d SparkSubmitTimeout=%s "+ "HTTPClientTimeout=%s MaxResponseBytes=%d SparkSubmitTimeout=%s "+
"LogDir=%s LogLevel=%s LogFormat=%s AnalyzerDataSkewRatio=%.1f "+ "LogDir=%s LogLevel=%s LogFormat=%s AnalyzerDataSkewRatio=%.1f "+
"AnalyzerGCPressureRatio=%.1f AnalyzerBottleneckShuffleGB=%.1f", "AnalyzerGCPressureRatio=%.1f AnalyzerBottleneckShuffleGB=%.1f GinMode=%s",
c.ListenAddr, c.ListenAddr,
c.DataDir, c.DataDir,
c.SQLitePath, c.SQLitePath,
@@ -221,5 +297,6 @@ func (c *Config) String() string {
c.AnalyzerDataSkewRatio, c.AnalyzerDataSkewRatio,
c.AnalyzerGCPressureRatio, c.AnalyzerGCPressureRatio,
c.AnalyzerBottleneckShuffleGB, c.AnalyzerBottleneckShuffleGB,
c.GinMode,
) )
} }
+1 -1
View File
@@ -65,7 +65,7 @@ func run() error {
defer db.Close() defer db.Close()
logger.Info("storage.open", "path", cfg.SQLitePath) logger.Info("storage.open", "path", cfg.SQLitePath)
gin.SetMode(gin.ReleaseMode) gin.SetMode(cfg.GinMode)
r := gin.New() r := gin.New()
r.Use(gin.Recovery()) r.Use(gin.Recovery())