Three related fixes for running Monitor inside a container with /:/host:ro bind-mounted and network_mode: host. * config: when HF_MONITER_ROOTFS is set, read the default identifier from <rootFS>/etc/hostname instead of os.Hostname(). Under network_mode: host the UTS namespace is not shared, so os.Hostname() returns a random docker-assigned string that changes across recreations, causing the backend to treat each restart as a new server. * telemetry: log gopsutil errors from BuildPayload instead of silently swallowing them. Previously a missing /host mount would send a payload full of zeroed fields with no indication of failure. * docker-compose: drop the 'ports:' block. It is silently ignored under network_mode: host (the bridge server binds directly on the host's 127.0.0.1:MONITOR_PORT).
204 lines
5.7 KiB
Go
204 lines
5.7 KiB
Go
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 <rootFS>/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)
|
|
}
|
|
}
|