Files

101 lines
3.4 KiB
Go

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
}