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
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
name: collect_battery_metrics
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func CollectBatteryMetrics() ([]PromSample, error)"
|
||||
description: "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: porcentaje, temperatura, estado de carga (booleano), corriente en microamperios y una serie informativa node_battery_health_info con labels health/status/plugged. Best-effort y multiplataforma: en nodos sin termux-battery-status (Linux normales) es un no-op que devuelve slice vacio y error nil; solo emite samples cuando el comando existe y responde JSON valido. El comando corre con timeout de 5s via context."
|
||||
tags: [prometheus, metrics, node-exporter, battery, termux, android, fleet-metrics, infra, monitoring]
|
||||
uses_functions: []
|
||||
uses_types: ["PromSample_go_infra"]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["context", "encoding/json", "os/exec", "time"]
|
||||
params: []
|
||||
output: "slice de PromSample con metricas de bateria. node_battery_percent (0-100), node_battery_temp_celsius, node_battery_charging (1 si carga/lleno/enchufado, si no 0), node_battery_current_ua (microamperios, negativo al descargar) y node_battery_health_info{health,status,plugged} con value 1. En nodos sin termux-api devuelve slice vacio. Error nil siempre en condiciones normales: la funcion traga los fallos de ejecucion/parseo como no-op (slice vacio)."
|
||||
tested: true
|
||||
tests:
|
||||
- "TestCollectBatteryMetrics_ParseDischarging"
|
||||
- "TestCollectBatteryMetrics_ParseCharging"
|
||||
- "TestCollectBatteryMetrics_ParsePluggedNotUnplugged"
|
||||
- "TestCollectBatteryMetrics_ParseFull"
|
||||
- "TestCollectBatteryMetrics_InvalidJSON"
|
||||
test_file_path: "functions/infra/collect_battery_metrics_test.go"
|
||||
file_path: "functions/infra/collect_battery_metrics.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
samples, err := CollectBatteryMetrics()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// En un Linux normal samples sera vacio (no-op); en Android/Termux trae
|
||||
// node_battery_percent, node_battery_temp_celsius, node_battery_charging, etc.
|
||||
// Componer con el resto del capability group fleet-metrics:
|
||||
host, _ := CollectHostMetrics()
|
||||
all := append(host, samples...)
|
||||
body := FormatPromExposition(all, time.Now().UnixMilli())
|
||||
err = PushPromRemote(
|
||||
"https://metrics-xxxx.organic-machine.com/api/v1/import/prometheus",
|
||||
"user", "pass",
|
||||
body,
|
||||
map[string]string{"instance": "pixel-phone"},
|
||||
)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando un nodo de la flota es un movil Android con Termux + termux-api y quieres
|
||||
exponer la salud de su bateria como metricas Prometheus para push a un backend
|
||||
remoto (VictoriaMetrics, Mimir). Llamala junto a `collect_host_metrics_go_infra`
|
||||
en el loop del agente de monitorizacion push y concatena los slices: en moviles
|
||||
añade las series de bateria, en el resto de nodos no aporta nada (no-op seguro),
|
||||
asi puedes usar el MISMO agente en toda la flota sin ramas por plataforma.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Solo produce datos en Termux/Android con termux-api instalado**: necesita el
|
||||
binario `termux-battery-status` (paquete `termux-api` + la app Termux:API). En
|
||||
cualquier otro nodo (Linux de escritorio, VPS, macOS) `exec.LookPath` falla y
|
||||
la funcion es un no-op que devuelve `[]PromSample{}, nil`. No es un error:
|
||||
simplemente no hay bateria que reportar.
|
||||
- **No devuelve error nunca en condiciones normales**: por diseño best-effort,
|
||||
tanto el binario ausente como un comando fallido (timeout, permisos) o un JSON
|
||||
invalido se tragan como slice vacio. La firma mantiene `error` por convencion
|
||||
de impureza, pero el caller no necesita ramificar por error de plataforma.
|
||||
- **Timeout de 5s**: usa `exec.CommandContext` con `context.WithTimeout`. Si
|
||||
termux-api se cuelga, la llamada aborta a los 5s y devuelve no-op.
|
||||
- **node_battery_current_ua puede ser negativo**: convencion de Android — corriente
|
||||
negativa = descarga, positiva = carga. Se reporta tal cual (microamperios).
|
||||
- **node_battery_charging es heuristico**: vale 1 si `status` es `CHARGING` o
|
||||
`FULL`, o si `plugged != "UNPLUGGED"`. Cubre el caso de estar enchufado sin
|
||||
cargar activamente (ej. `NOT_CHARGING` con cargador conectado).
|
||||
- **No incluye la label `instance`**: igual que el resto de colectores del grupo,
|
||||
esa la añade `push_prom_remote_go_infra` via extra_label en el push.
|
||||
- **El parseo esta factorizado** en `parseBatteryJSON` (funcion pura interna) para
|
||||
poder testear los samples sin ejecutar termux-battery-status real.
|
||||
@@ -0,0 +1,246 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
"github.com/shirou/gopsutil/v4/load"
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
)
|
||||
|
||||
// isAndroidHost indica si el host es Android (incluido Termux). Se usa para
|
||||
// evitar rutas de gopsutil que invocan os.FindProcess -> pidfd_open, syscall
|
||||
// bloqueado por el seccomp de Android que mata el proceso con SIGSYS.
|
||||
func isAndroidHost() bool {
|
||||
if os.Getenv("ANDROID_ROOT") != "" || os.Getenv("ANDROID_DATA") != "" {
|
||||
return true
|
||||
}
|
||||
if _, err := os.Stat("/system/build.prop"); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pseudoFstypes son filesystems virtuales que no representan almacenamiento
|
||||
// real y se ignoran al recolectar metricas de particiones.
|
||||
var pseudoFstypes = map[string]bool{
|
||||
"tmpfs": true,
|
||||
"devtmpfs": true,
|
||||
"overlay": true,
|
||||
"squashfs": true,
|
||||
"proc": true,
|
||||
"sysfs": true,
|
||||
"cgroup": true,
|
||||
"cgroup2": true,
|
||||
"devpts": true,
|
||||
"mqueue": true,
|
||||
"debugfs": true,
|
||||
"tracefs": true,
|
||||
"fusectl": true,
|
||||
"configfs": true,
|
||||
"pstore": true,
|
||||
"bpf": true,
|
||||
"securityfs": true,
|
||||
}
|
||||
|
||||
// CollectHostMetrics recolecta metricas del host actual (CPU, memoria, swap,
|
||||
// disco, red, temperaturas y procesos) y las devuelve como un slice de
|
||||
// PromSample con nombres estilo node_exporter simplificados.
|
||||
//
|
||||
// Es robusta: cada grupo de colector se ejecuta en su propio bloque con manejo
|
||||
// de error local. Si un colector secundario falla (red, temperaturas, etc.) se
|
||||
// omite ese grupo sin abortar. Solo retorna error si falla la informacion
|
||||
// basica de host (uptime), que se considera el minimo imprescindible.
|
||||
//
|
||||
// Funciona en Linux amd64 y Android/Termux (linux arm64): las temperaturas son
|
||||
// best-effort y se omiten si no hay sensores disponibles (tipico en Android).
|
||||
func CollectHostMetrics() ([]PromSample, error) {
|
||||
var samples []PromSample
|
||||
|
||||
// --- Host basico: uptime (imprescindible, error si falla) ---
|
||||
uptime, err := host.Uptime()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("collect host uptime: %w", err)
|
||||
}
|
||||
samples = append(samples, PromSample{
|
||||
Name: "node_uptime_seconds",
|
||||
Value: float64(uptime),
|
||||
})
|
||||
|
||||
// --- Load average (linux/darwin; best-effort) ---
|
||||
if avg, err := load.Avg(); err == nil && avg != nil {
|
||||
samples = append(samples,
|
||||
PromSample{Name: "node_load1", Value: avg.Load1},
|
||||
PromSample{Name: "node_load5", Value: avg.Load5},
|
||||
PromSample{Name: "node_load15", Value: avg.Load15},
|
||||
)
|
||||
}
|
||||
|
||||
// --- CPU global (intervalo corto de muestreo) ---
|
||||
if pcts, err := cpu.Percent(200*time.Millisecond, false); err == nil && len(pcts) > 0 {
|
||||
samples = append(samples, PromSample{
|
||||
Name: "node_cpu_percent",
|
||||
Value: pcts[0],
|
||||
})
|
||||
}
|
||||
|
||||
// --- CPU por nucleo ---
|
||||
if pcts, err := cpu.Percent(200*time.Millisecond, true); err == nil {
|
||||
for i, p := range pcts {
|
||||
samples = append(samples, PromSample{
|
||||
Name: "node_cpu_core_percent",
|
||||
Labels: map[string]string{"core": strconv.Itoa(i)},
|
||||
Value: p,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Memoria virtual ---
|
||||
if vm, err := mem.VirtualMemory(); err == nil && vm != nil {
|
||||
samples = append(samples,
|
||||
PromSample{Name: "node_mem_total_bytes", Value: float64(vm.Total)},
|
||||
PromSample{Name: "node_mem_used_bytes", Value: float64(vm.Used)},
|
||||
PromSample{Name: "node_mem_available_bytes", Value: float64(vm.Available)},
|
||||
PromSample{Name: "node_mem_used_percent", Value: vm.UsedPercent},
|
||||
)
|
||||
}
|
||||
|
||||
// --- Swap ---
|
||||
if sw, err := mem.SwapMemory(); err == nil && sw != nil {
|
||||
samples = append(samples,
|
||||
PromSample{Name: "node_swap_total_bytes", Value: float64(sw.Total)},
|
||||
PromSample{Name: "node_swap_used_bytes", Value: float64(sw.Used)},
|
||||
)
|
||||
}
|
||||
|
||||
// --- Particiones fisicas (ignora fstypes pseudo) ---
|
||||
if parts, err := disk.Partitions(false); err == nil {
|
||||
for _, p := range parts {
|
||||
if pseudoFstypes[p.Fstype] {
|
||||
continue
|
||||
}
|
||||
u, err := disk.Usage(p.Mountpoint)
|
||||
if err != nil || u == nil {
|
||||
continue
|
||||
}
|
||||
lbl := map[string]string{"mount": p.Mountpoint}
|
||||
samples = append(samples,
|
||||
PromSample{Name: "node_disk_total_bytes", Labels: lbl, Value: float64(u.Total)},
|
||||
PromSample{Name: "node_disk_used_bytes", Labels: lbl, Value: float64(u.Used)},
|
||||
PromSample{Name: "node_disk_used_percent", Labels: lbl, Value: u.UsedPercent},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Contadores I/O por dispositivo ---
|
||||
if io, err := disk.IOCounters(); err == nil {
|
||||
for dev, c := range io {
|
||||
lbl := map[string]string{"device": dev}
|
||||
samples = append(samples,
|
||||
PromSample{Name: "node_disk_read_bytes", Labels: lbl, Value: float64(c.ReadBytes)},
|
||||
PromSample{Name: "node_disk_write_bytes", Labels: lbl, Value: float64(c.WriteBytes)},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Red por interfaz (excluye loopback "lo") ---
|
||||
if nics, err := net.IOCounters(true); err == nil {
|
||||
for _, n := range nics {
|
||||
if n.Name == "lo" {
|
||||
continue
|
||||
}
|
||||
lbl := map[string]string{"iface": n.Name}
|
||||
samples = append(samples,
|
||||
PromSample{Name: "node_net_recv_bytes", Labels: lbl, Value: float64(n.BytesRecv)},
|
||||
PromSample{Name: "node_net_sent_bytes", Labels: lbl, Value: float64(n.BytesSent)},
|
||||
PromSample{Name: "node_net_recv_errs", Labels: lbl, Value: float64(n.Errin)},
|
||||
PromSample{Name: "node_net_sent_errs", Labels: lbl, Value: float64(n.Errout)},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Temperaturas (best-effort; omite el grupo si falla o no hay sensores) ---
|
||||
if temps, err := sensors.SensorsTemperatures(); err == nil {
|
||||
for _, t := range temps {
|
||||
if t.SensorKey == "" {
|
||||
continue
|
||||
}
|
||||
samples = append(samples, PromSample{
|
||||
Name: "node_temp_celsius",
|
||||
Labels: map[string]string{"sensor": t.SensorKey},
|
||||
Value: t.Temperature,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Procesos: total + top 5 por CPU ---
|
||||
// En Android (Termux) gopsutil process.Processes() llama internamente a
|
||||
// os.FindProcess, que usa el syscall pidfd_open bloqueado por el seccomp de
|
||||
// Android (mata el proceso con SIGSYS, no recuperable). Alli contamos los
|
||||
// PIDs con process.Pids() (que solo lee /proc, sin FindProcess) y omitimos
|
||||
// el top por CPU.
|
||||
if isAndroidHost() {
|
||||
if pids, err := process.Pids(); err == nil {
|
||||
samples = append(samples, PromSample{
|
||||
Name: "node_procs_total",
|
||||
Value: float64(len(pids)),
|
||||
})
|
||||
}
|
||||
} else if procs, err := process.Processes(); err == nil {
|
||||
samples = append(samples, PromSample{
|
||||
Name: "node_procs_total",
|
||||
Value: float64(len(procs)),
|
||||
})
|
||||
|
||||
type procStat struct {
|
||||
pid int32
|
||||
name string
|
||||
cpu float64
|
||||
mem float32
|
||||
}
|
||||
stats := make([]procStat, 0, len(procs))
|
||||
for _, p := range procs {
|
||||
cpuPct, err := p.CPUPercent()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
name, err := p.Name()
|
||||
if err != nil {
|
||||
name = ""
|
||||
}
|
||||
memPct, err := p.MemoryPercent()
|
||||
if err != nil {
|
||||
memPct = 0
|
||||
}
|
||||
stats = append(stats, procStat{pid: p.Pid, name: name, cpu: cpuPct, mem: memPct})
|
||||
}
|
||||
sort.Slice(stats, func(i, j int) bool {
|
||||
return stats[i].cpu > stats[j].cpu
|
||||
})
|
||||
top := stats
|
||||
if len(top) > 5 {
|
||||
top = top[:5]
|
||||
}
|
||||
for _, s := range top {
|
||||
lbl := map[string]string{
|
||||
"pid": strconv.Itoa(int(s.pid)),
|
||||
"name": s.name,
|
||||
}
|
||||
samples = append(samples,
|
||||
PromSample{Name: "node_proc_cpu_percent", Labels: lbl, Value: s.cpu},
|
||||
PromSample{Name: "node_proc_mem_percent", Labels: lbl, Value: float64(s.mem)},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return samples, nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
name: collect_host_metrics
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func CollectHostMetrics() ([]PromSample, error)"
|
||||
description: "Recolecta metricas del host actual (uptime, load, CPU global y por nucleo, memoria, swap, disco por particion fisica e I/O por dispositivo, red por interfaz, temperaturas best-effort y procesos: total + top 5 por CPU) y las devuelve como slice de PromSample con nombres estilo node_exporter simplificados. Robusta: cada grupo de colector tiene manejo de error local; si un colector secundario falla se omite ese grupo sin abortar. Funciona en Linux amd64 y Android/Termux (linux arm64)."
|
||||
tags: [prometheus, metrics, node-exporter, gopsutil, fleet-metrics, infra, monitoring, host]
|
||||
uses_functions: []
|
||||
uses_types: ["PromSample_go_infra"]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["fmt", "sort", "strconv", "time", "github.com/shirou/gopsutil/v4/cpu", "github.com/shirou/gopsutil/v4/disk", "github.com/shirou/gopsutil/v4/host", "github.com/shirou/gopsutil/v4/load", "github.com/shirou/gopsutil/v4/mem", "github.com/shirou/gopsutil/v4/net", "github.com/shirou/gopsutil/v4/process", "github.com/shirou/gopsutil/v4/sensors"]
|
||||
params: []
|
||||
output: "slice de PromSample con las metricas del host. Cada sample lleva nombre estilo node_exporter (node_cpu_percent, node_disk_used_bytes{mount}, etc.) y sus labels. Error solo si falla el uptime de host (informacion basica imprescindible)."
|
||||
tested: true
|
||||
tests:
|
||||
- "TestCollectHostMetrics_ReturnsBasics"
|
||||
- "TestCollectHostMetrics_SamplesWellFormed"
|
||||
test_file_path: "functions/infra/collect_host_metrics_test.go"
|
||||
file_path: "functions/infra/collect_host_metrics.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
samples, err := CollectHostMetrics()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Formatear a exposition Prometheus y enviar a VictoriaMetrics:
|
||||
body := FormatPromExposition(samples, time.Now().UnixMilli())
|
||||
err = PushPromRemote(
|
||||
"https://metrics-xxxx.organic-machine.com/api/v1/import/prometheus",
|
||||
"user", "pass",
|
||||
body,
|
||||
map[string]string{"instance": "lucas-pc"},
|
||||
)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites un snapshot completo de salud del host en formato Prometheus
|
||||
para hacer push a un backend remoto (VictoriaMetrics, Mimir, etc.) en lugar de
|
||||
exponer un endpoint /metrics para scraping. Es el colector base del capability
|
||||
group `fleet-metrics`: combinala con `format_prom_exposition_go_infra` y
|
||||
`push_prom_remote_go_infra` para un agente de monitorizacion push estilo
|
||||
node_exporter. Llamala periodicamente (cron, timer, loop) en cada nodo de la
|
||||
flota.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Bloquea ~400ms**: hace dos llamadas a `cpu.Percent` con intervalo de 200ms
|
||||
cada una (global + por nucleo). No la llames en hot paths ni con periodo < 1s.
|
||||
- **Temperaturas best-effort**: usa `sensors.SensorsTemperatures` (movido del
|
||||
paquete `host` al paquete `sensors` en gopsutil v4). Si no hay sensores
|
||||
(tipico en Android/Termux y muchos VPS) el grupo `node_temp_celsius` se omite
|
||||
sin error.
|
||||
- **Particiones pseudo ignoradas**: tmpfs, devtmpfs, overlay, squashfs, proc,
|
||||
sysfs y similares se filtran. Solo reporta particiones de almacenamiento real.
|
||||
- **Loopback excluido**: la interfaz `lo` no genera metricas de red.
|
||||
- **CPU por proceso necesita dos lecturas**: `CPUPercent()` de gopsutil sobre un
|
||||
proceso recien obtenido puede devolver un valor calculado desde el arranque
|
||||
del proceso, no un delta. Util para ranking relativo del top 5, no como medida
|
||||
instantanea precisa.
|
||||
- **No incluye la label `instance`**: los samples no llevan instance; esa la
|
||||
añade `push_prom_remote_go_infra` via extra_label en el push.
|
||||
- **Permisos**: algunos contadores (procesos de otros usuarios, ciertos sensores)
|
||||
pueden requerir privilegios; los fallos parciales se omiten silenciosamente.
|
||||
@@ -0,0 +1,86 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FormatPromExposition convierte un slice de PromSample en texto con formato
|
||||
// Prometheus exposition. Genera una linea por sample:
|
||||
//
|
||||
// name{k1="v1",k2="v2"} value timestampMs
|
||||
//
|
||||
// Reglas:
|
||||
// - Si timestampMs <= 0, omite el campo timestamp.
|
||||
// - Sin labels: "name value" (sin llaves).
|
||||
// - Las labels se ordenan por clave (salida determinista).
|
||||
// - En los valores de label se escapa: backslash -> \\, comilla -> \", newline -> \n.
|
||||
// - El nombre de metrica se sanitiza a [a-zA-Z0-9_:] (el resto -> _).
|
||||
// - El valor se formatea con strconv.FormatFloat(v, 'g', -1, 64).
|
||||
//
|
||||
// Es una funcion pura: no tiene efectos secundarios y la salida es deterministica
|
||||
// para una entrada dada.
|
||||
func FormatPromExposition(samples []PromSample, timestampMs int64) string {
|
||||
var b strings.Builder
|
||||
for _, s := range samples {
|
||||
b.WriteString(sanitizeMetricName(s.Name))
|
||||
|
||||
if len(s.Labels) > 0 {
|
||||
keys := make([]string, 0, len(s.Labels))
|
||||
for k := range s.Labels {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
b.WriteByte('{')
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
b.WriteString(k)
|
||||
b.WriteString(`="`)
|
||||
b.WriteString(escapeLabelValue(s.Labels[k]))
|
||||
b.WriteByte('"')
|
||||
}
|
||||
b.WriteByte('}')
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(strconv.FormatFloat(s.Value, 'g', -1, 64))
|
||||
|
||||
if timestampMs > 0 {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(strconv.FormatInt(timestampMs, 10))
|
||||
}
|
||||
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// sanitizeMetricName sustituye cualquier caracter fuera de [a-zA-Z0-9_:] por '_'.
|
||||
func sanitizeMetricName(name string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(name))
|
||||
for _, r := range name {
|
||||
if (r >= 'a' && r <= 'z') ||
|
||||
(r >= 'A' && r <= 'Z') ||
|
||||
(r >= '0' && r <= '9') ||
|
||||
r == '_' || r == ':' {
|
||||
b.WriteRune(r)
|
||||
} else {
|
||||
b.WriteByte('_')
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// escapeLabelValue escapa los caracteres especiales del formato exposition en
|
||||
// el valor de una label: backslash, comilla doble y newline.
|
||||
func escapeLabelValue(v string) string {
|
||||
v = strings.ReplaceAll(v, `\`, `\\`)
|
||||
v = strings.ReplaceAll(v, `"`, `\"`)
|
||||
v = strings.ReplaceAll(v, "\n", `\n`)
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: format_prom_exposition
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "func FormatPromExposition(samples []PromSample, timestampMs int64) string"
|
||||
description: "Convierte un slice de PromSample en texto con formato Prometheus exposition (una linea por sample: name{k=\"v\"} value timestampMs). Ordena labels por clave (salida determinista), escapa backslash/comilla/newline en valores de label, sanitiza el nombre de metrica a [a-zA-Z0-9_:], formatea el valor con FormatFloat 'g'. Si timestampMs<=0 omite el timestamp; sin labels omite las llaves. Funcion pura."
|
||||
tags: [prometheus, exposition, metrics, format, fleet-metrics, infra, monitoring]
|
||||
uses_functions: []
|
||||
uses_types: ["PromSample_go_infra"]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: ["sort", "strconv", "strings"]
|
||||
params:
|
||||
- name: samples
|
||||
desc: "slice de PromSample a serializar; cada uno aporta una linea de exposition"
|
||||
- name: timestampMs
|
||||
desc: "timestamp en milisegundos epoch a adjuntar a cada linea; si es <=0 se omite el campo timestamp"
|
||||
output: "string con el texto exposition Prometheus, una linea por sample terminada en \\n. String vacio si samples esta vacio."
|
||||
tested: true
|
||||
tests:
|
||||
- "TestFormatPromExposition"
|
||||
test_file_path: "functions/infra/format_prom_exposition_test.go"
|
||||
file_path: "functions/infra/format_prom_exposition.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
samples := []PromSample{
|
||||
{Name: "node_load1", Value: 0.42},
|
||||
{Name: "node_cpu_core_percent", Labels: map[string]string{"core": "0"}, Value: 12.5},
|
||||
{Name: "node_disk_used_bytes", Labels: map[string]string{"mount": "/"}, Value: 1024},
|
||||
}
|
||||
text := FormatPromExposition(samples, 1700000000000)
|
||||
// node_load1 0.42 1700000000000
|
||||
// node_cpu_core_percent{core="0"} 12.5 1700000000000
|
||||
// node_disk_used_bytes{mount="/"} 1024 1700000000000
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando tengas un slice de PromSample (tipicamente de collect_host_metrics) y
|
||||
necesites serializarlo al formato de texto que entienden los endpoints de
|
||||
ingestion Prometheus (`/api/v1/import/prometheus` de VictoriaMetrics, pushgateway,
|
||||
etc.). Es el paso intermedio del capability group `fleet-metrics`: colecta ->
|
||||
formatea -> empuja. Al ser pura y determinista, tambien sirve para snapshots
|
||||
reproducibles y golden tests.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El timestamp es **milisegundos** epoch (Prometheus exposition usa ms), no
|
||||
segundos. Pasa `time.Now().UnixMilli()`.
|
||||
- `timestampMs <= 0` (incluido 0) omite el campo timestamp por completo.
|
||||
- La label `instance` NO se gestiona aqui: si esta en `Labels` se serializa tal
|
||||
cual, pero la convencion del grupo es dejarla fuera y añadirla en el push via
|
||||
extra_label.
|
||||
- No agrupa por nombre ni emite lineas `# HELP` / `# TYPE`: salida cruda de
|
||||
series, suficiente para ingestion pero no para un endpoint /metrics canonico.
|
||||
- El nombre de metrica se sanitiza de forma destructiva: `node.cpu-percent!` se
|
||||
convierte en `node_cpu_percent_`. Nombra bien los samples en origen.
|
||||
@@ -0,0 +1,12 @@
|
||||
package infra
|
||||
|
||||
// PromSample representa una unica serie de metrica en formato Prometheus:
|
||||
// el nombre de la metrica, sus labels y un valor numerico.
|
||||
//
|
||||
// La label "instance" NO se incluye aqui: la añade el pusher remoto via
|
||||
// extra_label cuando hace el push a VictoriaMetrics.
|
||||
type PromSample struct {
|
||||
Name string // nombre de metrica prometheus, ej "node_cpu_percent"
|
||||
Labels map[string]string // labels de la serie, ej {"core":"0"} (sin la label "instance")
|
||||
Value float64
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PushLokiStream envia lineas de log a un servidor Grafana Loki via su push API.
|
||||
// Construye el cuerpo JSON con la forma {"streams":[{"stream":{labels},"values":[["<ts_ns>","<line>"],...]}]}
|
||||
// y lo manda por POST a endpoint (ej "https://logs-xxxx.organic-machine.com/loki/api/v1/push").
|
||||
//
|
||||
// Reglas:
|
||||
// - timestampsNs y lines deben tener la misma longitud; si no, retorna error antes de hacer la peticion.
|
||||
// - Si len(lines)==0 es un no-op: no hace ninguna peticion y retorna nil.
|
||||
// - labels va tal cual en el campo "stream".
|
||||
// - Si user != "", usa Basic Auth con user/pass.
|
||||
// - Content-Type: application/json. TLS verificado. Timeout 10s.
|
||||
// - Exito = status 2xx (Loki devuelve 204). Si no-2xx, error con el codigo + primeros 200 bytes del cuerpo.
|
||||
func PushLokiStream(endpoint string, user string, pass string, labels map[string]string, timestampsNs []int64, lines []string) error {
|
||||
if len(timestampsNs) != len(lines) {
|
||||
return fmt.Errorf("push_loki_stream: timestampsNs (%d) y lines (%d) tienen longitudes distintas", len(timestampsNs), len(lines))
|
||||
}
|
||||
|
||||
// No-op cuando no hay lineas.
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
values := make([][2]string, len(lines))
|
||||
for i := range lines {
|
||||
values[i] = [2]string{strconv.FormatInt(timestampsNs[i], 10), lines[i]}
|
||||
}
|
||||
|
||||
stream := labels
|
||||
if stream == nil {
|
||||
stream = map[string]string{}
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"streams": []map[string]any{
|
||||
{
|
||||
"stream": stream,
|
||||
"values": values,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("push_loki_stream: marshal body: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("push_loki_stream: build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if user != "" {
|
||||
req.SetBasicAuth(user, pass)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("push_loki_stream: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 200))
|
||||
return fmt.Errorf("push_loki_stream: HTTP %d: %s", resp.StatusCode, string(snippet))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
name: push_loki_stream
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func PushLokiStream(endpoint string, user string, pass string, labels map[string]string, timestampsNs []int64, lines []string) error"
|
||||
description: "Envia lineas de log a un servidor Grafana Loki via su push API. Construye el cuerpo JSON {\"streams\":[{\"stream\":{labels},\"values\":[[\"<ts_ns>\",\"<line>\"],...]}]} y lo POSTea al endpoint. Soporta Basic Auth opcional, valida que timestamps y lineas tengan igual longitud, es no-op si no hay lineas, y exige status 2xx. Solo stdlib, TLS verificado."
|
||||
tags: [loki, grafana, logs, push, metrics, http, json, stdlib, infra, fleet-metrics]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["bytes", "encoding/json", "fmt", "io", "net/http", "strconv", "time"]
|
||||
params:
|
||||
- name: endpoint
|
||||
desc: "URL completa del push API de Loki (ej https://logs-xxxx.organic-machine.com/loki/api/v1/push)"
|
||||
- name: user
|
||||
desc: "usuario para Basic Auth; si es cadena vacia no se envia Authorization"
|
||||
- name: pass
|
||||
desc: "password para Basic Auth; solo se usa cuando user != ''"
|
||||
- name: labels
|
||||
desc: "labels del stream Loki (ej {instance:lucas, job:journald, unit:ssh.service}); van tal cual en el campo stream"
|
||||
- name: timestampsNs
|
||||
desc: "timestamps en nanosegundos desde epoch, uno por linea; debe tener la misma longitud que lines"
|
||||
- name: lines
|
||||
desc: "lineas de log a enviar, alineadas posicionalmente con timestampsNs; si esta vacio la funcion es no-op"
|
||||
output: "error si la peticion falla, las longitudes no coinciden o el status no es 2xx; nil en exito (incluido el no-op de 0 lineas)"
|
||||
tested: true
|
||||
tests:
|
||||
- "JSON enviado tiene estructura streams/stream/values correcta"
|
||||
- "longitudes desiguales dan error antes del POST"
|
||||
- "len lines cero es no-op sin peticion"
|
||||
- "Basic Auth presente cuando user no vacio"
|
||||
- "status 500 produce error"
|
||||
test_file_path: "functions/infra/push_loki_stream_test.go"
|
||||
file_path: "functions/infra/push_loki_stream.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
labels := map[string]string{
|
||||
"instance": "lucas",
|
||||
"job": "journald",
|
||||
"unit": "ssh.service",
|
||||
}
|
||||
nowNs := time.Now().UnixNano()
|
||||
ts := []int64{nowNs, nowNs + 1}
|
||||
lines := []string{
|
||||
"Accepted publickey for lucas from 10.0.0.2",
|
||||
"session opened for user lucas",
|
||||
}
|
||||
|
||||
err := PushLokiStream(
|
||||
"https://logs-abcd.organic-machine.com/loki/api/v1/push",
|
||||
"tenant1", // user para Basic Auth (vacio = sin auth)
|
||||
"s3cr3t", // pass
|
||||
labels,
|
||||
ts,
|
||||
lines,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites enviar lineas de log a Grafana Loki desde un agente o servicio Go
|
||||
(ej. reenviar journald, eventos de una app, o lineas de un tailer) sin arrastrar el
|
||||
cliente oficial de Loki. Util para alimentar dashboards de la flota (`fleet-metrics`)
|
||||
con logs etiquetados por instancia/job/unit. Pasa los logs ya batcheados: un solo
|
||||
stream por llamada con sus labels.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `timestampsNs` y `lines` deben tener exactamente la misma longitud; si no, retorna
|
||||
error ANTES de hacer la peticion (no envia nada).
|
||||
- `len(lines)==0` es un no-op deliberado: retorna `nil` sin tocar la red. Comprueba el
|
||||
caso vacio en el caller si necesitas distinguir "no habia logs" de "envio ok".
|
||||
- Loki exige timestamps en NANOSEGUNDOS. Pasar segundos o milisegundos hace que las
|
||||
lineas caigan fuera de la ventana de retencion y Loki las rechace silenciosamente.
|
||||
- Dentro de un mismo stream las entradas deberian ir en orden creciente de timestamp;
|
||||
Loki puede rechazar entradas fuera de orden segun su config.
|
||||
- Exito = status 2xx (Loki normalmente devuelve 204 No Content). Un 4xx/5xx produce
|
||||
error con el codigo + primeros 200 bytes del cuerpo de respuesta para diagnostico.
|
||||
- TLS verificado (sin InsecureSkipVerify) y `http.Client` con timeout de 10s fijo. Para
|
||||
endpoints con certificado interno hace falta CA confiable en el sistema.
|
||||
- Secretos (`user`/`pass`): nunca hardcodear — resolver desde `pass`/vault en el caller.
|
||||
@@ -0,0 +1,60 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PushPromRemote hace POST del body (texto en formato Prometheus exposition) al
|
||||
// endpoint dado, tipicamente el import de VictoriaMetrics
|
||||
// (".../api/v1/import/prometheus").
|
||||
//
|
||||
// - Si user != "" usa Basic Auth con user/pass.
|
||||
// - extraLabels se adjuntan como query params repetidos
|
||||
// "extra_label=clave=valor" URL-encoded. VictoriaMetrics añade esas labels a
|
||||
// TODAS las series del push (util para la label "instance").
|
||||
// - Content-Type: text/plain.
|
||||
// - http.Client con Timeout 10s y TLS verificado.
|
||||
// - Exito = status 2xx (VictoriaMetrics devuelve 204). Si no-2xx, retorna error
|
||||
// con el codigo y los primeros 200 bytes del cuerpo de respuesta.
|
||||
func PushPromRemote(endpoint string, user string, pass string, body string, extraLabels map[string]string) error {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse endpoint %q: %w", endpoint, err)
|
||||
}
|
||||
|
||||
if len(extraLabels) > 0 {
|
||||
q := u.Query()
|
||||
for k, v := range extraLabels {
|
||||
q.Add("extra_label", k+"="+v)
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "text/plain")
|
||||
if user != "" {
|
||||
req.SetBasicAuth(user, pass)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("push to %q: %w", endpoint, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 200))
|
||||
return fmt.Errorf("push to %q failed: status %d: %s", endpoint, resp.StatusCode, string(snippet))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
name: push_prom_remote
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func PushPromRemote(endpoint string, user string, pass string, body string, extraLabels map[string]string) error"
|
||||
description: "Hace POST de un body en formato Prometheus exposition a un endpoint remoto (ej VictoriaMetrics /api/v1/import/prometheus). Soporta Basic Auth (si user!=\"\"), adjunta extraLabels como query params repetidos extra_label=clave=valor (VictoriaMetrics los añade a todas las series del push), Content-Type text/plain, http.Client con Timeout 10s y TLS verificado. Exito = status 2xx; si no, error con codigo + primeros 200 bytes del cuerpo de respuesta."
|
||||
tags: [prometheus, push, victoriametrics, remote-write, fleet-metrics, infra, monitoring, http]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["fmt", "io", "net/http", "net/url", "strings", "time"]
|
||||
params:
|
||||
- name: endpoint
|
||||
desc: "URL completa del endpoint de ingestion, ej https://metrics-xxxx.organic-machine.com/api/v1/import/prometheus"
|
||||
- name: user
|
||||
desc: "usuario para Basic Auth; si es cadena vacia no se envia Authorization"
|
||||
- name: pass
|
||||
desc: "password para Basic Auth; se ignora si user esta vacio"
|
||||
- name: body
|
||||
desc: "texto en formato Prometheus exposition (tipicamente salida de format_prom_exposition)"
|
||||
- name: extraLabels
|
||||
desc: "labels a adjuntar a todas las series via extra_label, ej {\"instance\":\"lucas\"}; puede ser nil"
|
||||
output: "nil si el push devuelve status 2xx (VictoriaMetrics responde 204). Error si la request falla, el endpoint es invalido, o el status no es 2xx (con codigo y snippet del cuerpo)."
|
||||
tested: true
|
||||
tests:
|
||||
- "TestPushPromRemote"
|
||||
test_file_path: "functions/infra/push_prom_remote_test.go"
|
||||
file_path: "functions/infra/push_prom_remote.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
body := FormatPromExposition(samples, time.Now().UnixMilli())
|
||||
err := PushPromRemote(
|
||||
"https://metrics-xxxx.organic-machine.com/api/v1/import/prometheus",
|
||||
"ingest-user", "ingest-pass",
|
||||
body,
|
||||
map[string]string{"instance": "lucas-pc"},
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("push fallo: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando un nodo de la flota tiene que **empujar** sus metricas a un backend
|
||||
central (VictoriaMetrics, Mimir, pushgateway) en vez de exponer un /metrics para
|
||||
scraping. Es el paso final del capability group `fleet-metrics`:
|
||||
collect_host_metrics -> format_prom_exposition -> push_prom_remote. Tipica en
|
||||
nodos detras de NAT, moviles (Termux) o cualquier host al que el servidor central
|
||||
no puede alcanzar para hacer pull.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **extra_label es clave=valor como un solo valor de query**: para {"instance":"lucas"}
|
||||
produce `?extra_label=instance%3Dlucas` (el `=` interno se URL-encodea a `%3D`).
|
||||
VictoriaMetrics lo aplica a todas las series del push; otros backends pueden no
|
||||
soportar este parametro.
|
||||
- **Secretos**: nunca hardcodees `user`/`pass` — resuelvelos desde `pass`/vault.
|
||||
- **TLS verificado** (sin InsecureSkipVerify): un endpoint con certificado
|
||||
autofirmado fallara. Usa un certificado valido o un proxy de confianza.
|
||||
- **Timeout 10s**: un backend lento o un body enorme puede dar timeout. Trocea
|
||||
pushes muy grandes si es necesario.
|
||||
- **204 No Content es exito**: VictoriaMetrics no devuelve 200. La funcion acepta
|
||||
cualquier 2xx; no asumas 200 al testear contra el real.
|
||||
- **El cuerpo de error se trunca a 200 bytes**: suficiente para diagnostico
|
||||
rapido, no para el detalle completo del backend.
|
||||
@@ -7,12 +7,15 @@ require (
|
||||
github.com/charmbracelet/bubbles v1.0.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02
|
||||
github.com/jackc/pgx/v5 v5.9.1
|
||||
github.com/marcboeker/go-duckdb v1.8.5
|
||||
github.com/mattn/go-sqlite3 v1.14.44
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/shirou/gopsutil/v4 v4.26.5
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/yuin/goldmark v1.8.2
|
||||
github.com/zalando/go-keyring v0.2.8
|
||||
@@ -40,23 +43,24 @@ require (
|
||||
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||
github.com/creack/pty v1.1.24 // indirect
|
||||
github.com/danieljoos/wincred v1.2.3 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.7.1 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
github.com/google/flatbuffers v25.1.24+incompatible // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
@@ -67,6 +71,7 @@ require (
|
||||
github.com/paulmach/orb v0.12.0 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.25 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/rs/zerolog v1.35.1 // indirect
|
||||
@@ -76,7 +81,10 @@ require (
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
go.mau.fi/util v0.9.9 // indirect
|
||||
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||
|
||||
@@ -47,6 +47,8 @@ github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
@@ -55,6 +57,8 @@ github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
@@ -70,6 +74,7 @@ github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLK
|
||||
github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -104,6 +109,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0=
|
||||
github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
@@ -139,6 +146,8 @@ github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcR
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -147,6 +156,8 @@ github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
|
||||
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
@@ -170,6 +181,10 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
@@ -182,6 +197,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
|
||||
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
@@ -224,8 +241,10 @@ golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: PromSample
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
algebraic: product
|
||||
definition: |
|
||||
type PromSample struct {
|
||||
Name string // nombre de metrica prometheus, ej "node_cpu_percent"
|
||||
Labels map[string]string // labels de la serie, ej {"core":"0"} (sin la label "instance")
|
||||
Value float64
|
||||
}
|
||||
description: "Una unica serie de metrica en formato Prometheus: nombre, labels y valor numerico. Es la unidad que producen los colectores (collect_host_metrics) y consume el formateador (format_prom_exposition). NO incluye la label instance: esa la añade el pusher remoto via extra_label."
|
||||
tags: [prometheus, metrics, sample, fleet-metrics, infra, monitoring]
|
||||
uses_types: []
|
||||
file_path: "functions/infra/prom_sample.go"
|
||||
---
|
||||
|
||||
## Campos
|
||||
|
||||
- `Name`: nombre de la metrica en formato Prometheus, ej `node_cpu_percent`, `node_disk_used_bytes`. Se sanitiza a `[a-zA-Z0-9_:]` en el formateador.
|
||||
- `Labels`: mapa clave/valor de labels de la serie, ej `{"core":"0"}`, `{"mount":"/"}`. Puede ser `nil` o vacio (serie sin labels). NO debe incluir la label `instance` — esa se añade en el push remoto.
|
||||
- `Value`: valor numerico de la metrica. Se formatea con `strconv.FormatFloat(v, 'g', -1, 64)`.
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
samples := []PromSample{
|
||||
{Name: "node_load1", Labels: nil, Value: 0.42},
|
||||
{Name: "node_cpu_core_percent", Labels: map[string]string{"core": "0"}, Value: 12.5},
|
||||
{Name: "node_disk_used_bytes", Labels: map[string]string{"mount": "/"}, Value: 1.2e10},
|
||||
}
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- Es un tipo producto puro: solo datos, sin metodos.
|
||||
- Vive en `functions/infra/prom_sample.go` (mismo paquete Go que los colectores y el formateador) para evitar imports cruzados entre paquetes del registry.
|
||||
- Lo producen `collect_host_metrics_go_infra` y lo consume `format_prom_exposition_go_infra`.
|
||||
Reference in New Issue
Block a user