package main import ( "encoding/json" "os" "strconv" ) // Config holds the agent runtime configuration. It is read from an optional // JSON file and can be overridden by environment variables, which is handy for // systemd drop-ins and for deploying the same binary to many nodes. type Config struct { Node string `json:"node"` // value of the "instance" label attached to every series HubURL string `json:"hub_url"` // full metrics ingest URL, e.g. https://metrics-…/api/v1/import/prometheus LokiURL string `json:"loki_url"` // full Loki push URL, e.g. https://logs-…/loki/api/v1/push (empty disables log shipping) User string `json:"user"` // basic-auth user, shared by metrics and logs (empty disables auth) Pass string `json:"pass"` // basic-auth password IntervalSec int `json:"interval_sec"` // metrics push period in seconds (default 15) // Extra labels attached to every metric series and log stream of this node, // e.g. {"role": "vps"}. Enables filtering in Grafana (per-role dashboards). Labels map[string]string `json:"labels"` // Android/Termux exec workaround: the standard Go binary cannot exec // subprocesses there (seccomp blocks pidfd_open with SIGSYS). When set, the // agent reads battery JSON from this file (written by a shell helper) instead // of running termux-battery-status itself. BatteryFile string `json:"battery_file"` // When set, the agent tails this log file (written by a shell `logcat` // helper) and ships it to Loki, instead of exec-ing journald/logcat. LogFile string `json:"log_file"` } // defaultConfig returns the baseline configuration: the machine hostname as the // node name and a 15-second push interval. func defaultConfig() Config { host, _ := os.Hostname() return Config{Node: host, IntervalSec: 15} } // loadConfig reads the JSON file at path (when non-empty) and then applies // environment overrides. Recognised env vars: FLEET_NODE, FLEET_HUB_URL, // FLEET_USER, FLEET_PASS, FLEET_INTERVAL. func loadConfig(path string) (Config, error) { cfg := defaultConfig() if path != "" { b, err := os.ReadFile(path) if err != nil { return cfg, err } if err := json.Unmarshal(b, &cfg); err != nil { return cfg, err } } if v := os.Getenv("FLEET_NODE"); v != "" { cfg.Node = v } if v := os.Getenv("FLEET_HUB_URL"); v != "" { cfg.HubURL = v } if v := os.Getenv("FLEET_LOKI_URL"); v != "" { cfg.LokiURL = v } if v := os.Getenv("FLEET_BATTERY_FILE"); v != "" { cfg.BatteryFile = v } if v := os.Getenv("FLEET_LOG_FILE"); v != "" { cfg.LogFile = v } if v := os.Getenv("FLEET_USER"); v != "" { cfg.User = v } if v := os.Getenv("FLEET_PASS"); v != "" { cfg.Pass = v } if v := os.Getenv("FLEET_INTERVAL"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 { cfg.IntervalSec = n } } if cfg.IntervalSec <= 0 { cfg.IntervalSec = 15 } if cfg.Node == "" { cfg.Node, _ = os.Hostname() } return cfg, nil } // extraLabels returns the labels attached to every series/stream: the node's // instance plus any custom labels (e.g. role). The optional extra map is merged // last (used to add per-stream labels like job/unit for logs). func (cfg Config) extraLabels(extra map[string]string) map[string]string { m := map[string]string{"instance": cfg.Node} for k, v := range cfg.Labels { m[k] = v } for k, v := range extra { m[k] = v } return m }