Ir al contenido

Otros destinos

Además de Prometheus e InfluxDB 3, forward escribe en nueve destinos más. La cola, la espera entre reintentos y los contadores que comparten están en Ejecutar el colector.

Destino Muestras del kernel Texto del log del kernel Capa de la API Derivados y detecciones Disparos, huecos, equipo
file líneas del agente tal cual dentro de las líneas de muestra líneas {"api":…} {"derived":…}, {"detection":…} los tres, como líneas
stdout json líneas del agente tal cual dentro de las líneas de muestra líneas {"api":…} como el fichero los tres, como líneas
stdout lp las medidas de InfluxDB cuentas por nivel, puerto y tipo de evento las medidas de InfluxDB las medidas de InfluxDB los tres
SQL (--sql, --postgres) una tabla por fuente filas mikroscope_event tablas, más mikroscope_api_error mikroscope_derived, mikroscope_detection los tres
Loki no una línea por registro solo errores por orden solo detecciones los tres, como líneas
OTLP sums y gauges no gauges, y una cuenta de errores gauges, un sum de detecciones los tres
Graphite una ruta por valor no rutas rutas solo las partes numéricas
Elasticsearch un documento por tick un documento por registro un documento por lectura, sin errores en el documento del kernel, un documento por detección los tres
Telegraf las medidas de InfluxDB cuentas por nivel, puerto y tipo de evento las medidas de InfluxDB las medidas de InfluxDB los tres

Los registros del log del kernel solo existen cuando el contenedor corre con privileged=yes; consulta Modo privilegiado.

Ventana de terminal
mikroscope forward --file timeline.jsonl

El fichero se abre y se trunca, con modo 0600. Cada línea es un objeto JSON, y su primera clave dice qué es:

  • una muestra del kernel — la propia línea del agente, byte a byte, tal como la sirvió /snapshot;
  • {"trigger":…} — el marcador de captura del agente, también tal cual;
  • {"derived":…} — los valores de la etapa de derivación, en la línea siguiente a la muestra a la que pertenecen, para que un lector que solo quiere muestras en bruto se salte ese tipo;
  • {"detection":…}, {"device":…}, {"api":…}, {"sampler":…} y {"gap":…}.

Las escrituras son síncronas a través de un búfer de 64 KiB, y los contadores van en eventos. Una escritura que el sistema de ficheros rechaza cuenta un error y un descarte.

Ventana de terminal
mikroscope forward --stdout=lp | telegraf --config …
mikroscope forward --stdout=json | jq

--stdout lp renderiza protocolo de líneas de InfluxDB con el propio codificador del destino de InfluxDB, así que una tubería muestra exactamente lo que enviaría --influx. --stdout json escribe los mismos tipos de línea que el fichero. Cualquier otro valor se rechaza antes de que empiece la ejecución.

La salida estándar se puede atascar — un lector lento llena el búfer de la tubería y la escritura se bloquea sin nada que la acote — así que este destino va en cola: un lote por segundo, cada lote escrito en una sola llamada de líneas completas para que un lector nunca vea un registro a medias, 64 KiB × --queue-seconds de presupuesto, descartando primero lo más antiguo. Un lote que un lector atascado deja crecer por encima del presupuesto entero se cierra antes. Ese presupuesto se dimensionó para una muestra en protocolo de líneas de unos 1,2 KiB. Para json, cuyas líneas llevan fuentes que el protocolo de líneas omite y son más grandes, no se ha medido.

  • Una tubería rota no llega a los contadores: Go deja SIGPIPE sin capturar en la salida estándar, así que el proceso termina.
  • Al cerrar, el destino espera como mucho 3 s a que el lector acepte el último lote, así que un lector que dejó de leer no puede colgar forward.
  • forward imprime también su resumen de salida por la salida estándar, así que una tubería hacia telegraf termina cada ejecución con líneas que el consumidor no puede interpretar.
Ventana de terminal
mikroscope forward --sql out.sql --for 10m && psql -f out.sql
mikroscope forward --sql - | psql # mira el aviso de abajo

--sql escribe texto PostgreSQL — una cabecera DDL y luego un INSERT por fila — a un fichero, o a la salida estándar con -. No usa driver a propósito: hablar el protocolo de red de PostgreSQL necesita un driver de terceros, así que el texto SQL es la interfaz y psql es dueño de la conexión. El coste es que el destino no puede saber si una fila se guardó; cuenta los eventos que escribió.

La cabecera se puede aplicar por sí sola y es idempotente:

  • SET standard_conforming_strings = on; — para que una barra invertida en un mensaje del kernel nunca se convierta en un escape y se trague las sentencias siguientes;
  • CREATE TABLE IF NOT EXISTS para cada tabla, cada una con una clave primaria que empieza por time, host;
  • con --sql-hypertable, una llamada create_hypertable de TimescaleDB sobre time para cada tabla, con if_not_exists => TRUE.

Cada INSERT termina en ON CONFLICT DO NOTHING, así que aplicar el mismo fichero dos veces no hace nada en vez de abortar por clave duplicada. Una fila es un instante inmutable de un delta de contador, nunca un total acumulado que un fichero posterior corrija.

Ventana de terminal
mikroscope forward --postgres 'postgres://usuario@host/mikroscope?sslmode=require'

--postgres escribe en un PostgreSQL que está en marcha, en vez de en un fichero que alguien reproduce después. --sql y --postgres son el mismo destino dos veces: el que conecta envía las sentencias que produce el de fichero, desde un único renderizador. No dos que hoy coinciden — los dashboards que este proyecto genera para el store postgres tienen que ser verdad en un despliegue que usara cualquiera de los dos, y un segundo renderizador pasaría un recuento de filas y aun así desviaría una columna.

El fichero sigue siendo lo que quieres cuando la base de datos está donde el colector no llega, cuando la carga debe ocurrir más tarde o bajo revisión, o cuando quien lee no es PostgreSQL: las sentencias son SQL corriente y otro motor puede tomarlas. Los dos pueden funcionar a la vez, contra bases distintas o la misma — ON CONFLICT DO NOTHING hace que el solape no haga nada.

Lo que puede la conexión y no puede un fichero:

  • Sabe si una fila se guardó. Un lote va dentro de una transacción y entra entero o no entra, así que un reintento tras un fallo no puede dejar medio evento atrás.
  • Comprueba la suposición que la cabecera solo puede pedir. SET standard_conforming_strings = on es un consejo en un fichero que el operador puede ejecutar sin él. Por una conexión el ajuste se puede leer de vuelta, así que se lee — y un servidor que responde off se rechaza con el motivo en vez de escribirse, porque un mensaje del kernel terminado en barra invertida escaparía su propia comilla de cierre y todo lo que viniera detrás se interpretaría como contenido de cadena.
  • Sabe describir su propia fuente de datos de Grafana, cosa que el destino de fichero no podrá nunca: ver Fuente de datos por destino.

A diferencia de --sql, --postgres va en cola como los destinos remotos: un lote por segundo, 64 KiB × --queue-seconds de presupuesto descartando primero el lote más antiguo, cada lote una transacción con un tiempo de espera de 30 s, y los contadores en lotes. Así, un servidor lento nunca bloquea el bucle de extracción, cosa que la tubería hacia psql sí puede hacer.

Lo que cede: nada del esquema, y una dependencia. --postgres enlaza pgx en el binario del colector. El agente no lo enlaza — el agente enlaza procfs, sample, agent y la biblioteca estándar, así que su imagen no lleva nada nuevo, y el trabajo agent-size de la CI la sigue manteniendo por debajo del presupuesto de 8 MiB.

El DSN es una opción y no un secreto solo de entorno porque las convenciones de libpq son justamente el punto: pgx lee PGPASSWORD, ~/.pgpass y el fichero de servicio igual que psql, así que un DSN sin contraseña funciona como en cualquier otro cliente de PostgreSQL. MIKROSCOPE_POSTGRES_DSN fija el valor por defecto.

La suite de extremo a extremo con docker ejecuta los dos destinos a la vez, el script a una base de datos y la conexión a otra, y comprueba que las dos guardan las mismas filas (baterías de pruebas).

Tabla Clave tras time, host Guarda
mikroscope_cpu cpu user_ticks … steal_ticks, busy_ratio, dt_ns
mikroscope_softnet cpu processed, dropped, time_squeeze
mikroscope_irq irq name, count sumado sobre las CPU
mikroscope_mem — niveles: cada campo de /proc/meminfo que lee el agente, en _kb
mikroscope_load — niveles: medias de carga, running, threads, procs_blocked
mikroscope_stat — deltas: ctxt, intr, forks, irq_total, irq_err, pgfault, pgmajfault
mikroscope_self — delta cpu_us; niveles rss, cgroup_mem; eventos de cgroup throttled, throttled_us, oom_kill, NULL sin cgroup2; resets, kmsg_dropped, seq, wake_ns, read_ns
mikroscope_buddy node, zone, block_order free_blocks, una fila por zona y orden
mikroscope_mtd device partition, contadores ECC tal como se leen, umbrales NULL donde no se publican
mikroscope_psi — microsegundos de bloqueo; ninguna fila donde el kernel no tiene PSI
mikroscope_sample — seq, dt_ns, mono_ns: una fila por tick
mikroscope_softirq kind, cpu count, por vector y núcleo
mikroscope_perf counter, cpu count de la PMU, enabled_ns, running_ns; ninguna fila sin privileged=yes y una PMU
mikroscope_vm — deltas de /proc/vmstat: fallos, reservas y liberaciones, escaneos y robos de reclaim, bloqueos, oom_kill, swap
mikroscope_vm_level — niveles de /proc/vmstat: nr_free_pages, nr_dirty, nr_writeback, páginas de slab
mikroscope_cpufreq cpu khz, y max_khz donde la placa publica uno
mikroscope_irq_cpu irq, cpu name, count por núcleo
mikroscope_thermal zone celsius, critical_celsius
mikroscope_slab cache active_objs, limit_objs (NULL para toda caché salvo nf_conntrack)
mikroscope_disk device deltas de lectura y escritura, io_s; inflight es un nivel
mikroscope_flash device deltas de desgaste; bad_blocks y free_chunks son niveles
mikroscope_event kernel_seq un registro del log del kernel: level, facility, time_usec, message, y port, kind
mikroscope_api_system — cpu_load, memoria, free_hdd, uptime_s, version
mikroscope_api_core cpu el porcentaje load, irq, disk de RouterOS
mikroscope_api_health name value
mikroscope_api_iface interface label, tasas, y cinco columnas de pérdidas que son NULL donde el router no devolvió la clave
mikroscope_api_conntrack — entries, el último valor repetido a la cadencia de la API
mikroscope_api_ifinfo interface qué es cada interfaz: default_name, type, role, bridge, label, mtu
mikroscope_api_ifcounter interface, counter value, en formato largo, con el nombre de contador del propio RouterOS
mikroscope_api_error message una por cada orden de la API que falló
mikroscope_gap seq_from, seq_to el rango perdido, con el reloj del colector
mikroscope_trigger id cause, field, value, threshold, seq
mikroscope_derived — seq, mem_pressure, burst, suspect, valores por paquete NULL donde no se calcularon
mikroscope_derived_iface interface los cuatro deltas de bytes y las dos proporciones del fast path
mikroscope_detection rule, key seq, value, threshold, message
mikroscope_sampler — ticks, slipped, captures_held, capture_bytes, capture_budget_bytes, capture_served_bytes
mikroscope_trigger_count condition fired
mikroscope_trigger_suppressed condition, reason count
mikroscope_capture_refused reason count
mikroscope_device, mikroscope_device_thermal, mikroscope_device_cpufreq, mikroscope_device_cadence —, zone, cpu, source el flujo de datos del equipo

Las columnas nunca necesitan comillas: las columnas de ticks son user_ticks y compañía porque user es una palabra reservada, y block_order porque order lo es. dt_ns va solo en mikroscope_cpu y mikroscope_sample, así que una tasa sobre cualquier otra tabla de deltas se une a mikroscope_sample por (time, host) para tener el intervalo real en vez de suponer el periodo nominal. Las cuatro últimas tablas se leen del GET /sampler del agente a la cadencia de salud del colector, no las produce un tick.

mikroscope_api_ifinfo guarda una fila por interfaz, escrita al arrancar el colector y en cada relectura de --labels-every (5 minutos por defecto), así que una consulta la une por interface para darle a cualquier serie de interfaz un tipo, un rol y el comentario del puerto. default_name es el nombre de fábrica de un puerto físico y está vacío para un bridge, una VLAN o un túnel; mtu es el actual-mtu de RouterOS, NULL donde el router no lo publica. En mikroscope_event, port es el puerto que nombra el registro — su nombre actual en RouterOS cuando el inventario de la capa de la API lo da, y el nombre por defecto de la placa si no — y kind es lo que le pasó: link-up, link-down, los estados stp-*, own-address (el bridge recibió una trama con su propia MAC como origen, la firma de un bucle de capa 2) u other. Ambas son NULL para un registro que no nombra ningún puerto.

Una fuente que el despliegue no puede leer no emite ninguna fila. Un valor que no se midió es NULL, nunca 0: un router puede no devolver ninguna clave de errores de interfaz (verificado), y un 0 ahí afirmaría una medida que nunca se hizo. El texto se fuerza donde PostgreSQL lo rechazaría — un byte NUL se elimina, el UTF-8 inválido pasa a U+FFFD — y un float NaN o infinito pasa a NULL. TIMESTAMPTZ resuelve a 1 µs, así que dos muestras más cercanas que eso chocarían en la clave primaria; a 10 Hz están a 100 ms.

Comparado con InfluxDB, al destino SQL le falta exactamente una cosa: la tabla de recuentos del log del kernel. Lleva dos que InfluxDB no tiene: el texto del log del kernel en mikroscope_event, y los errores de la capa de la API.

Tamaños según el fixture de pruebas del destino — dos núcleos, una cola softnet, una interrupción, sin fuentes privilegiadas —, no según un router:

  • Un evento del kernel se renderiza en 1 375 B de SQL y un evento de la API en 1 138 B, así que 10 Hz más la capa de la API a 1 Hz son unos 14 KiB/s de fichero tras la cabecera de 5,6 KiB del fixture.
  • Los mismos dos eventos en protocolo de líneas son 716 B y 608 B, unas 1,9× más pequeños, aunque parte de eso es contenido que las filas SQL llevan y el protocolo de líneas no.
  • Con las fuentes privilegiadas presentes el evento del kernel crece hasta 2 749 B.
  • La cabecera de las cuarenta y tres tablas que declara el destino, generada por el propio header() del destino, son 10 482 B, unos 10,2 KiB —13 966 B con las sentencias de hypertable de TimescaleDB.
Ventana de terminal
export MIKROSCOPE_LOKI_URL=http://host:3100/loki/api/v1/push
mikroscope forward --loki "$MIKROSCOPE_LOKI_URL" --loki-tenant team-a

Loki recibe los eventos de la línea temporal, no sus muestras. Una muestra es una medida y pertenece a un almacén de métricas; 10 Hz de números en un almacén de logs son una copia más lenta y más grande. Lo que llega es lo que pasó una vez, en un momento conocido, enviado como JSON al endpoint de push de Loki, /loki/api/v1/push. Un token bearer sale de MIKROSCOPE_LOKI_TOKEN, y --loki-tenant (MIKROSCOPE_LOKI_TENANT) fija X-Scope-OrgID, la cabecera de la que un Loki multi-tenant lee el tenant.

Los streams llevan tres etiquetas y ninguna más — host, source y level — porque Loki indexa las etiquetas y la cardinalidad es un coste:

source level Una línea por
kmsg el del propio registro: emerg … debug registro del log del kernel, marcado en su tick
gap warn rango de secuencia perdido, marcado cuando el colector lo notó
api err orden de la capa de la API que falló
detection warn detección, en la muestra que la provocó
trigger info disparo de captura, en el momento del disparo; la captura se queda en el agente
device info registro de datos del equipo: board, kernel, cores, privileged, cgroup, sources, hash

La línea de un registro del kernel es el mensaje seguido de pares logfmt — level, facility, prio, kseq, us (los microsegundos del propio kernel desde el arranque), seq (la muestra) y, cuando el registro nombra un puerto, iface (el nombre del kernel), ros_iface (el nombre en RouterOS), port_event (lo que pasó: link-up, link-down, un estado stp-*, own-address u other), label (el comentario del puerto, entre comillas) y role (sus listas de interfaces). ros_iface es el nombre actual del puerto en RouterOS cuando el inventario de la capa de la API lo da, y el nombre por defecto de la placa si no; label y role salen también de ese inventario, así que una ejecución sin capa de la API no lleva ninguno de los dos. El puerto va en la línea, no en una etiqueta, porque un stream por puerto multiplica el número de streams por un campo que LogQL extrae bajo demanda:

{source="kmsg"} | logfmt | ros_iface="ether2"
{source="kmsg"} | logfmt | port_event="own-address"

Cada registro se marca con el reloj de pared de su tick, nunca con su propia marca desde el arranque, que lo fecharía en 1970 más el uptime y Loki lo rechazaría. Los registros de un mismo tick están separados 1 ns, en orden creciente, porque un stream de Loki se ordena solo por marca de tiempo y una pareja «blocking state» y luego «learning state» puede llegar dentro de un mismo tick de 100 ms con el mismo nivel: su orden es la señal. El desplazamiento es como mucho 63 ns, acotado por el límite del agente de 64 registros por tick.

Un push por segundo, 64 KiB × --queue-seconds de presupuesto. Una línea renderizada mide unos 150 B con su envoltorio JSON, así que a los 1,49 /s de registros del log del kernel que produjo un bucle de capa 2 activo, un segundo de presupuesto guarda horas de ese tráfico. La entrega durante una tormenta del log del kernel no se ha medido.

Ventana de terminal
mikroscope forward --otlp http://collector:4318/v1/metrics

--otlp envía métricas de OpenTelemetry a un receptor OTLP/HTTP en la codificación JSON, una petición por segundo. Un token bearer sale de MIKROSCOPE_OTLP_TOKEN. JSON en vez de protobuf es una decisión de dependencias: protobuf añadiría un generador de código y un runtime. La especificación de OTLP dice que un servidor SHOULD —debería, no está obligado a— aceptar cargas codificadas en JSON en el mismo puerto que el protobuf binario, así que un receptor que solo acepte protobuf la cumple al pie de la letra. El OpenTelemetry Collector acepta JSON (probado). El recurso lleva host.name (la etiqueta de host) y service.name=mikroscope; el scope lleva la versión del colector.

El mapeo es la razón de que este destino sea barato de consumir. mikroscope envía deltas en bruto, y OTLP tiene un sitio exacto para ellos: cada contador es un Sum con AGGREGATION_TEMPORALITY_DELTA e isMonotonic=true. En los contadores de la muestra del kernel, startTimeUnixNano = el reloj de pared de la muestra menos su intervalo real, y timeUnixNano = su reloj de pared — así al receptor se le dice el intervalo que cubre cada delta en vez de que adivine el nominal. Las demás sumas no llevan intervalo: mikroscope.api.errors, mikroscope.collector.gaps y mikroscope.collector.gap.samples no tienen hora de inicio, y mikroscope.trigger.fired y mikroscope.detection tienen una hora de inicio igual a su hora. Cada nivel es un Gauge. De la capa del kernel no hay nada dividido de antemano salvo mikroscope.cpu.busy_ratio; los gauges mikroscope.derived.* son la etapa de derivación del colector.

Tipo Métrica Atributos
Sum mikroscope.cpu.ticks cpu, mode
Sum mikroscope.context_switches, mikroscope.interrupts, mikroscope.forks, mikroscope.self.cpu.time —
Sum mikroscope.self.throttled_periods, mikroscope.self.throttled_time, mikroscope.self.oom_kills (solo con cgroup2) —
Sum mikroscope.irq.count; mikroscope.irq.total, mikroscope.irq.errors irq, name; —
Sum mikroscope.softnet; mikroscope.sched cpu, kind
Sum mikroscope.softirq; mikroscope.vm.events kind
Sum mikroscope.psi.stalled resource, scope
Sum mikroscope.flash, mikroscope.disk; mikroscope.disk.io_time (ms) device, kind; device
Sum mikroscope.api.errors; mikroscope.collector.gaps, mikroscope.collector.gap.samples tier; —
Sum mikroscope.trigger.fired; mikroscope.detection cause; rule
Gauge mikroscope.cpu.busy_ratio, mikroscope.cpu.frequency cpu
Gauge mikroscope.sample.dt, mikroscope.sample.seq, mikroscope.threads, mikroscope.procs_blocked —
Gauge mikroscope.memory (KiB), mikroscope.self.memory, mikroscope.vm.pages kind
Gauge mikroscope.load window
Gauge mikroscope.thermal.temperature zone
Gauge mikroscope.slab.objects, mikroscope.slab.limit cache
Gauge mikroscope.memory.buddy_free_blocks node, zone, order
Gauge mikroscope.mtd.ecc; mikroscope.mtd.bitflip_threshold, mikroscope.mtd.ecc_strength device, partition, kind; device, partition
Gauge mikroscope.flash.blocks; mikroscope.disk.io_in_progress device, kind; device
Gauge mikroscope.api.cpu_load, mikroscope.api.uptime; mikroscope.api.memory —; kind
Gauge mikroscope.api.core; mikroscope.api.health cpu, kind; name
Gauge mikroscope.api.interface interface, kind, y label, type, role donde el inventario los tiene
Gauge mikroscope.api.interface.counter; mikroscope.api.conntrack.entries interface, counter, y label, type, role donde el inventario los tiene; —
Gauge mikroscope.derived.memory_pressure, mikroscope.derived.cycles_per_packet, ….instructions_per_packet, ….cache_misses_per_packet, ….packets_per_irq —
Gauge mikroscope.derived.fastpath_share interface, direction
Gauge mikroscope.device.cores; mikroscope.device.thermal.critical, mikroscope.device.thermal.polling (s) board, kernel, hash; zone
Gauge mikroscope.device.cpu.frequency_max, mikroscope.device.cpu.frequency_min; mikroscope.device.source_cadence cpu; source, reason
Gauge mikroscope.sampler.{ticks,slipped,trigger_fired,trigger_suppressed}; mikroscope.capture.{held,bytes,budget_bytes,served_bytes,refused} condition, reason

Los miembros de reclaim y swap de mikroscope.vm.events se omiten cuando valen cero: en un Sum de deltas, un punto ausente y un punto a cero significan lo mismo. Los contadores por puerto son gauges de un total acumulado, no Sums, porque el destino no tiene tiempo de inicio para un contador que RouterOS lleva desde el arranque. Un gauge cuyo valor es NaN o infinito se descarta. Los registros del log del kernel no se emiten: su sitio son los logs OTLP en /v1/logs, que este destino no implementa.

Un éxito parcial de OTLP — un 2xx cuyo cuerpo rechaza algunos puntos — cuenta como escrito y se registra, no se reintenta, que es lo que la especificación de OTLP exige a un cliente. El rechazo es determinista (un receptor OTLP de Prometheus que rechaza un punto más antiguo que su ventana es el caso habitual), así que el mismo lote se rechazaría igual.

Tamaños según un fixture de pruebas de dos núcleos, no según un router: una muestra del kernel se renderiza en 8 132 B de JSON OTLP frente a 716 B de protocolo de líneas, unas 11×, el precio de repetir las claves de los atributos y de poner entre comillas cada entero de 64 bits. Un segundo de muestras a 10 Hz más una muestra de la API son 68 868 B, así que el presupuesto de 64 KiB por segundo guarda aproximadamente un segundo de atraso por segundo y los 60 s por defecto unos 57 lotes. Una placa con más núcleos y un top-K real de interrupciones renderiza más (sin medir).

Ventana de terminal
mikroscope forward --graphite carbon:2003 --graphite-prefix mikroscope

--graphite (MIKROSCOPE_GRAPHITE_ADDR) escribe el protocolo de texto plano de carbon — path value timestamp, una línea por valor — sobre una única conexión TCP persistente. Graphite no tiene etiquetas, así que cada dimensión es un nodo de ruta bajo <prefix>.<host>.; --graphite-prefix vale mikroscope por defecto. En un nodo solo sobreviven letras ASCII, dígitos, _, - y :, cualquier otro byte pasa a _, y un valor vacío pasa a none, así que la profundidad de una ruta nunca cambia.

Rutas De
sample.{seq,dt_ns}, stat.{ctxt,intr,forks,procs_blocked,irq_total,irq_err} la muestra
cpu.<n>.{user,nice,system,idle,iowait,irq,softirq,steal,busy_ratio,freq_khz} /proc/stat, cpufreq
softnet.<n>.{processed,dropped,time_squeeze}, irq.<id>.<name>.count, softirq.<kind>.count softnet, interrupts, softirqs
mem.<campo>_kb para cada nivel de /proc/meminfo que lee el agente (total, free, available, cached, slab, sunreclaim, sreclaimable, dirty, writeback, anon, buffers, active, inactive, shmem, mapped, kernel_stack, page_tables, committed, commit_limit), load.{load1,load5,load15,running,threads} meminfo, loadavg
vm.{pgfault,pgmajfault,pgscan_kswapd,pgscan_direct,pgsteal_kswapd,pgsteal_direct,allocstall,oom_kill}, vmg.{nr_free_pages,nr_dirty,nr_writeback} vmstat
self.{cpu_us,rss_bytes,cgroup_mem,throttled,throttled_us,oom_kill} el coste propio del agente
psi.*_us, sched.<n>.{run_ns,wait_ns} solo donde el kernel los tiene
thermal.<index>.celsius, slab.<cache>.{active_objs,limit_objs}, buddy.<node>.<zone>.order_<n> thermal, slab, buddyinfo
mtd.<dev>.<counter>, flash.<dev>.<counter>, disk.<dev>.{reads,read_sectors,writes,write_sectors,io_s,inflight} flash y dispositivos de bloques
api.system.<field>, api.core.<n>.{load,irq,disk}, api.health.<name>, api.conntrack.entries la capa de la API
api.iface.<if>.{rx_bps,tx_bps,rx_pps,tx_pps,<loss>,fp_rx_share,fp_tx_share}, api.ifcounter.<if>.<counter> la capa de la API y la etapa de derivación
derived.{mem_pressure,cycles_per_packet,instructions_per_packet,cache_misses_per_packet,packets_per_irq} la etapa de derivación
trigger.<cause>, detection.<rule> — el valor 1 en cada evento disparos y detecciones
device.{cores,conntrack_max,cgroup_mem_max}, device.thermal.<zone>.*, device.cpufreq.<n>.*, device.cadence.<source>.hz el flujo de datos del equipo
sampler.{ticks,slipped,captures_held,capture_bytes,capture_budget_bytes,capture_served_bytes}, sampler.trigger_fired.<condition>, sampler.trigger_suppressed.<condition>.<reason>, sampler.capture_refused.<reason> los contadores del propio agente, en la cadencia de salud
collector.gap.{samples,from,to} huecos, con el reloj del colector

Lo que el protocolo no puede prometer, y lo que cada límite cambia en un panel de Graphite:

  • Marcas de tiempo en segundos enteros. La retención más fina de Whisper es un segundo, así que a 10 Hz nueve de cada diez muestras caen en una casilla que ya tiene valor y carbon se queda con el último escrito. Es una lectura válida para un nivel (mem, load, thermal, freq_khz, slab, vmg) y se queda corta para un delta: una suma sobre cpu.0.user ve más o menos una décima parte de los ticks que envió el agente. Sumar los deltas de cada segundo en el destino arreglaría las rutas de deltas y no las de niveles; ese intercambio no se ha hecho. Un consumidor que necesite cada tick tiene el destino de InfluxDB o el de fichero.
  • Sin respuesta. written cuenta lotes entregados al socket, no puntos que carbon guardó. Una escritura en un socket que carbon ya cerró funciona una vez y falla en la siguiente, así que el destino reintenta una vez con una conexión nueva; el lote que entró en el socket cerrado se pierde, uno por cada reinicio de carbon.
  • Zonas térmicas por índice, no por nombre, porque la cadena de tipo de una zona no es única; el nombre se conserva en los destinos de fichero e InfluxDB.
  • Menos detalle: se descartan las cuentas de interrupciones por CPU, los softirqs se suman sobre las CPU, no se emite cpu.total (sumSeries sobre cpu.*.user lo da), se omiten pgalloc, pgfree y los contadores de swap, y se descartan los registros del log del kernel porque Graphite solo guarda números. Las cadenas de placa, kernel y governor no tienen forma en Graphite.

El presupuesto de bytes es de 256 KiB por segundo en cola, cuatro veces el de los demás: un tick de cuatro núcleos con todas las fuentes presentes se renderiza en 6 411 B en 131 líneas en el fixture de pruebas del destino, no en un router, así que 10 Hz son unos 63 KiB/s. Por encima de 10 Hz no se ha medido.

El dashboard de Graphite lee estas rutas. Grafana pregunta a la API web de Graphite, no al puerto de carbon en el que escribe el destino, así que --grafana necesita esa dirección en --grafana-datasource-url, o una fuente de datos de Graphite que ya tengas en --grafana-datasource-uid (Fuente de datos de Graphite).

Ventana de terminal
export MIKROSCOPE_ELASTIC_AUTH=elastic:… # o una clave de API
mikroscope forward --elastic http://opensearch:9200 --elastic-index 'mikroscope-%Y.%m.%d'

--elastic (MIKROSCOPE_ELASTIC_URL) escribe a través de la API bulk que comparten los dos productos: la de Elasticsearch y la de OpenSearch aceptan el mismo JSON delimitado por saltos de línea. /_bulk se añade a la raíz de un clúster, conservando cualquier query string. Un MIKROSCOPE_ELASTIC_AUTH con user:password se envía como autenticación básica, y cualquier otra cosa como Authorization: ApiKey; las credenciales incrustadas en la URL se ocultan en el nombre del destino que se imprime.

Un documento por evento, con kind para distinguirlos:

  • kernel — un tick del agente: ticks por CPU con busy y busy_ratio, stat, mem, load, vm, self, cada fuente opcional solo cuando se leyó (sin clave, nunca un cero), y los valores de la etapa de derivación bajo derived;
  • event — un registro del log del kernel: priority, level, facility, seq, time_usec, message y, cuando el registro nombra un puerto, iface, ros_iface, port_event (lo que le pasó al puerto), label y role;
  • api — una lectura de la API: system, cores, health, ifaces, iface_counters, el último número de conexiones, las proporciones del fast path bajo fastpath, e inventory en las rondas que leen qué es cada interfaz. Cada entrada de ifaces lleva label, type, role y bridge, y cada entrada de iface_counters lleva comment, type, role y bridge, donde el inventario los tiene. Los errores por orden de la capa de la API no se escriben;
  • gap (from, to, lost), device, detection, trigger y sampler.

Cada documento lleva @timestamp con el reloj del agente (el del colector para huecos y registros del equipo) y host. El nombre del índice expande %Y, %m y %d — solo esos — contra esa marca de tiempo, así que un lote que queda en cola pasada la medianoche cae en el día en que se muestreó, y se pasa a minúsculas porque el clúster rechaza la petición entera si el nombre de índice tiene mayúsculas.

La acción es index con un _id construido a partir del tipo, el host, la marca de tiempo del documento en nanosegundos y, donde existe, el número de secuencia que distingue documentos del mismo instante, así que un lote que el clúster aplicó pero cuya respuesta se perdió se reenvía sin duplicar nada. La marca de tiempo forma parte de la identidad porque la secuencia del agente vuelve a empezar desde 1 en cada arranque: sin ella, las muestras de un agente reiniciado sobrescribirían las anteriores del día, cada una con un 201.

Una petición bulk responde 200 aunque se hayan rechazado todos sus elementos. El destino lee el veredicto de cada elemento, suma uno a dropped por cada documento rechazado, y registra el primer motivo una vez por minuto — por separado de los fallos de entrega, porque un conflicto de mapeo y un clúster inalcanzable piden acciones distintas. Los lotes se cierran una vez por segundo o al llegar a 1 MiB, lo que ocurra primero.

Tamaños según el fixture de dos núcleos del destino, no según un router: 1 077 B de NDJSON para una muestra del kernel sin fuentes opcionales, 1 952 B repartidos en dos documentos con las fuentes opcionales y un registro del log del kernel. Una muestra de cuatro núcleos no se ha renderizado en este formato.

--grafana construye una fuente de datos de Elasticsearch de Grafana (tipo elasticsearch) a partir de --elastic: @timestamp como campo de tiempo, un patrón de índice que abarca cada índice que genera --elastic-index, y MIKROSCOPE_ELASTIC_AUTH enviada igual que la envía el destino (Fuente de datos de Elasticsearch). Contra OpenSearch esa fuente de datos no se ha probado.

Ventana de terminal
mikroscope forward --telegraf http://host:8186/telegraf
mikroscope forward --telegraf tcp://host:8094

--telegraf (MIKROSCOPE_TELEGRAF_URL) envía los mismos registros de protocolo de líneas que el destino de InfluxDB — llama a ese codificador en vez de copiarlo — para que las propias salidas de Telegraf los repartan a sistemas para los que este repositorio no tiene destino. Telegraf deja pasar la marca de tiempo sin cambiarla.

El esquema del endpoint elige el transporte:

  • http:// o https:// — envía a una entrada http_listener_v2 o influxdb_v2_listener. Un host:port sin más se interpreta como HTTP, y a un endpoint HTTP sin ruta se le da /telegraf, la ruta que da la configuración de ejemplo de http_listener_v2: un listener responde 404 en / y el cuerpo no dice por qué. Un MIKROSCOPE_TELEGRAF_TOKEN con user:password se envía como autenticación básica, y cualquier otra cosa como Authorization: Token ….
  • tcp:// — registros delimitados por saltos de línea a un socket_listener, con una conexión nueva por lote.
  • udp:// — datagramas de como mucho 1 432 bytes, cortados en los límites de registro para que ningún datagrama lleve media línea. No hay confirmación: written cuenta lotes que aceptó el kernel local, un datagrama perdido por el camino es invisible, y un reintento tras un fallo a mitad de lote puede entregar algunos registros dos veces. Usa http:// o tcp:// para cualquier cosa que importe.

El presupuesto es el del destino de InfluxDB, 64 KiB por segundo en cola: a unos 1,2 KiB por muestra a 10 Hz (medido), unos 5 minutos de atraso con los 60 s por defecto. El codificador compartido escribe las lecturas de /system/health en el orden de un map de Go, así que los registros dentro de un lote no tienen un orden estable — 12 renderizados de un map de 8 nombres dieron 7 órdenes. Cada registro lleva su propia marca de tiempo, así que no se pierde ni se desfasa nada.

Cada destino de esta página se prueba contra un receptor local que comprueba los bytes que acepta su protocolo. Las baterías de pruebas listan los almacenes reales contra los que corre además cada destino, y lo que no se ha ejecutado contra un almacén ni alimentado desde un router está en la lista de lo no probado.