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 (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
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user