Files
fn_registry/functions/infra/collect_battery_metrics.go
T

121 lines
4.2 KiB
Go

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
}