package config import ( "encoding/json" "fmt" "os" "path/filepath" "strings" ) type Config struct { BackendURL string `json:"backendUrl"` Identifier string `json:"identifier"` APIKey string `json:"apiKey"` ReportIntervalSec int `json:"reportIntervalSec"` LogLevel string `json:"logLevel"` RootFS string `json:"rootFs"` MonitorPort int `json:"monitorPort"` } type Overrides struct { BackendURL string Identifier string APIKey string ReportIntervalSec int LogLevel string RootFS string MonitorPort int } func Load(path string) (Config, error) { return LoadWithOverrides(path, Overrides{}) } func LoadWithOverrides(path string, overrides Overrides) (Config, error) { // If running inside a container with the host FS bind-mounted, prefer // the host's /etc/hostname for the default identifier. The container's // own os.Hostname() is a docker-assigned random string under // network_mode: host (UTS namespace is not shared). rootFSEarly := getenvAny([]string{"HF_MONITER_ROOTFS", "HF_MONITOR_ROOTFS"}, "") defaultIdentifier := hostHostname(rootFSEarly) if defaultIdentifier == "" { defaultIdentifier = hostnameOr("unknown-host") } cfg := Config{ BackendURL: getenvAny([]string{"HF_MONITER_BACKEND_URL", "HF_MONITOR_BACKEND_URL"}, "https://monitor.hangman-lab.top"), Identifier: getenvAny([]string{"HF_MONITER_IDENTIFIER", "HF_MONITOR_IDENTIFIER"}, defaultIdentifier), APIKey: getenvAny([]string{"HF_MONITER_API_KEY", "HF_MONITOR_API_KEY"}, ""), ReportIntervalSec: getenvIntAny([]string{"HF_MONITER_REPORT_INTERVAL", "HF_MONITOR_REPORT_INTERVAL"}, 30), LogLevel: getenvAny([]string{"HF_MONITER_LOG_LEVEL", "HF_MONITOR_LOG_LEVEL"}, "info"), RootFS: getenvAny([]string{"HF_MONITER_ROOTFS", "HF_MONITOR_ROOTFS"}, ""), } if path != "" { if fileCfg, err := loadFile(path); err == nil { merge(&cfg, fileCfg) } else if !os.IsNotExist(err) { return cfg, fmt.Errorf("load config file: %w", err) } } // env always wins over file cfg.BackendURL = getenvAny([]string{"HF_MONITER_BACKEND_URL", "HF_MONITOR_BACKEND_URL"}, cfg.BackendURL) cfg.Identifier = getenvAny([]string{"HF_MONITER_IDENTIFIER", "HF_MONITOR_IDENTIFIER"}, cfg.Identifier) if v := getenvAny([]string{"HF_MONITER_API_KEY", "HF_MONITOR_API_KEY"}, ""); v != "" { cfg.APIKey = v } cfg.ReportIntervalSec = getenvIntAny([]string{"HF_MONITER_REPORT_INTERVAL", "HF_MONITOR_REPORT_INTERVAL"}, cfg.ReportIntervalSec) cfg.LogLevel = getenvAny([]string{"HF_MONITER_LOG_LEVEL", "HF_MONITOR_LOG_LEVEL"}, cfg.LogLevel) cfg.RootFS = getenvAny([]string{"HF_MONITER_ROOTFS", "HF_MONITOR_ROOTFS"}, cfg.RootFS) cfg.MonitorPort = getenvIntAny([]string{"MONITOR_PORT", "HF_MONITOR_PORT"}, cfg.MonitorPort) if overrides.BackendURL != "" { cfg.BackendURL = overrides.BackendURL } if overrides.Identifier != "" { cfg.Identifier = overrides.Identifier } if overrides.APIKey != "" { cfg.APIKey = overrides.APIKey } if overrides.ReportIntervalSec > 0 { cfg.ReportIntervalSec = overrides.ReportIntervalSec } if overrides.LogLevel != "" { cfg.LogLevel = overrides.LogLevel } if overrides.RootFS != "" { cfg.RootFS = overrides.RootFS } if overrides.MonitorPort > 0 { cfg.MonitorPort = overrides.MonitorPort } if cfg.BackendURL == "" { return cfg, fmt.Errorf("backendUrl is required") } if cfg.Identifier == "" { return cfg, fmt.Errorf("identifier is required") } if cfg.ReportIntervalSec <= 0 { cfg.ReportIntervalSec = 30 } applyHostFSEnv(cfg.RootFS) return cfg, nil } func loadFile(path string) (Config, error) { var cfg Config buf, err := os.ReadFile(path) if err != nil { return cfg, err } if err := json.Unmarshal(buf, &cfg); err != nil { return cfg, err } return cfg, nil } func merge(dst *Config, src Config) { if src.BackendURL != "" { dst.BackendURL = src.BackendURL } if src.Identifier != "" { dst.Identifier = src.Identifier } if src.APIKey != "" { dst.APIKey = src.APIKey } if src.ReportIntervalSec > 0 { dst.ReportIntervalSec = src.ReportIntervalSec } if src.LogLevel != "" { dst.LogLevel = src.LogLevel } if src.RootFS != "" { dst.RootFS = src.RootFS } if src.MonitorPort > 0 { dst.MonitorPort = src.MonitorPort } } func getenvAny(keys []string, fallback string) string { for _, key := range keys { if v := os.Getenv(key); v != "" { return v } } return fallback } func getenvIntAny(keys []string, fallback int) int { for _, key := range keys { if v := os.Getenv(key); v != "" { var out int if _, err := fmt.Sscanf(v, "%d", &out); err == nil && out > 0 { return out } } } return fallback } func hostnameOr(fallback string) string { if name, err := os.Hostname(); err == nil && name != "" { return name } return fallback } // hostHostname reads the hostname from /etc/hostname. Used when // Monitor runs inside a container and wants the host's hostname rather // than the container's UTS namespace hostname (which docker randomizes // unless hostname: is set). func hostHostname(rootFS string) string { if rootFS == "" { return "" } buf, err := os.ReadFile(filepath.Join(rootFS, "etc", "hostname")) if err != nil { return "" } return strings.TrimSpace(string(buf)) } func applyHostFSEnv(rootFS string) { if rootFS == "" { return } setIfEmpty("HOST_PROC", filepath.Join(rootFS, "proc")) setIfEmpty("HOST_SYS", filepath.Join(rootFS, "sys")) setIfEmpty("HOST_ETC", filepath.Join(rootFS, "etc")) setIfEmpty("HOST_VAR", filepath.Join(rootFS, "var")) setIfEmpty("HOST_RUN", filepath.Join(rootFS, "run")) } func setIfEmpty(key, value string) { if os.Getenv(key) == "" { _ = os.Setenv(key, value) } }