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,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
|
||||
}
|
||||
Reference in New Issue
Block a user