feat(infra): grupo fleet-metrics — collect_host_metrics, format_prom_exposition, push_prom_remote, push_loki_stream, collect_battery_metrics + tipo PromSample (gopsutil; Android-safe: sin exec/pidfd, procesos via /proc)
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
// batteryStatus modela el JSON que imprime `termux-battery-status` (binario del
|
||||
// paquete termux-api en Android/Termux). Solo se declaran los campos que
|
||||
// consumimos como metricas.
|
||||
type batteryStatus struct {
|
||||
Health string `json:"health"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
Plugged string `json:"plugged"`
|
||||
Status string `json:"status"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Current float64 `json:"current"`
|
||||
}
|
||||
|
||||
// CollectBatteryMetrics recolecta metricas de bateria de un dispositivo
|
||||
// Android via el comando `termux-battery-status` (paquete termux-api) y las
|
||||
// devuelve como slice de PromSample con nombres estilo node_exporter.
|
||||
//
|
||||
// Es best-effort y diseñada para correr en cualquier nodo de la flota,
|
||||
// incluidos Linux normales donde `termux-battery-status` NO existe: en ese
|
||||
// caso (binario no encontrado, comando fallido o JSON invalido) devuelve un
|
||||
// slice vacio y error nil — NO es un fallo, simplemente no hay bateria que
|
||||
// reportar. Solo emite samples cuando el comando existe y responde JSON valido.
|
||||
//
|
||||
// El comando se ejecuta con un timeout de 5s via context para no colgar el
|
||||
// agente de monitorizacion si termux-api se queda sin responder.
|
||||
func CollectBatteryMetrics() ([]PromSample, error) {
|
||||
// Localizamos el binario por ruta absoluta con os.Stat en vez de
|
||||
// exec.LookPath: en Android el syscall faccessat2 que usa LookPath esta
|
||||
// bloqueado por seccomp y mata el proceso con SIGSYS. Si no esta presente
|
||||
// (Linux normal), no hay bateria que reportar (no-op).
|
||||
bin := findTermuxBattery()
|
||||
if bin == "" {
|
||||
return []PromSample{}, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
out, err := exec.CommandContext(ctx, bin).Output()
|
||||
if err != nil {
|
||||
// Comando presente pero fallido (timeout, sin permisos, etc.): no-op.
|
||||
return []PromSample{}, nil
|
||||
}
|
||||
|
||||
samples, err := parseBatteryJSON(out)
|
||||
if err != nil {
|
||||
// JSON inesperado o invalido: no-op, no abortamos al caller.
|
||||
return []PromSample{}, nil
|
||||
}
|
||||
return samples, nil
|
||||
}
|
||||
|
||||
// findTermuxBattery devuelve la ruta absoluta del binario termux-battery-status
|
||||
// si existe, o "" si no. Usa os.Stat (permitido por seccomp en Android) en vez
|
||||
// de exec.LookPath (que invoca faccessat2 y crashea con SIGSYS en Android).
|
||||
func findTermuxBattery() string {
|
||||
candidates := []string{}
|
||||
if prefix := os.Getenv("PREFIX"); prefix != "" {
|
||||
candidates = append(candidates, prefix+"/bin/termux-battery-status")
|
||||
}
|
||||
candidates = append(candidates, "/data/data/com.termux/files/usr/bin/termux-battery-status")
|
||||
for _, c := range candidates {
|
||||
if _, err := os.Stat(c); err == nil {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// BatterySamplesFromJSON parsea la salida JSON de `termux-battery-status` y
|
||||
// produce los PromSample de bateria. Es pura y exportada para que un caller que
|
||||
// ya tenga el JSON (por ejemplo leido de un fichero, util en Android donde el
|
||||
// agente no puede ejecutar subprocesos) lo convierta sin volver a ejecutar el
|
||||
// comando.
|
||||
func BatterySamplesFromJSON(data []byte) ([]PromSample, error) {
|
||||
return parseBatteryJSON(data)
|
||||
}
|
||||
|
||||
// parseBatteryJSON parsea la salida JSON de `termux-battery-status` y produce
|
||||
// los PromSample de bateria. Es pura: no ejecuta comandos ni toca el entorno,
|
||||
// lo que la hace testeable con un JSON fijo. Devuelve error solo si el JSON no
|
||||
// es valido o no tiene la forma esperada.
|
||||
func parseBatteryJSON(data []byte) ([]PromSample, error) {
|
||||
var bs batteryStatus
|
||||
if err := json.Unmarshal(data, &bs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// charging = 1 si el status indica carga/lleno o si esta enchufado.
|
||||
var charging float64
|
||||
if bs.Status == "CHARGING" || bs.Status == "FULL" || bs.Plugged != "UNPLUGGED" {
|
||||
charging = 1
|
||||
}
|
||||
|
||||
samples := []PromSample{
|
||||
{Name: "node_battery_percent", Value: bs.Percentage},
|
||||
{Name: "node_battery_temp_celsius", Value: bs.Temperature},
|
||||
{Name: "node_battery_charging", Value: charging},
|
||||
{Name: "node_battery_current_ua", Value: bs.Current},
|
||||
{
|
||||
Name: "node_battery_health_info",
|
||||
Labels: map[string]string{
|
||||
"health": bs.Health,
|
||||
"status": bs.Status,
|
||||
"plugged": bs.Plugged,
|
||||
},
|
||||
Value: 1,
|
||||
},
|
||||
}
|
||||
return samples, nil
|
||||
}
|
||||
Reference in New Issue
Block a user