121 lines
4.2 KiB
Go
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
|
|
}
|