# Otros destinos

El fichero JSONL, stdout, SQL y PostgreSQL, Loki, OTLP, Graphite, Elasticsearch y Telegraf: qué lleva cada destino, cómo entrega y qué no cabe en su protocolo.

Source: https://docs.jmrp.io/mikroscope/es/sinks/other/

Además de [Prometheus](https://docs.jmrp.io/mikroscope/es/sinks/prometheus/) e [InfluxDB
3](https://docs.jmrp.io/mikroscope/es/sinks/influxdb/), `forward` escribe en nueve destinos más. La cola, la
espera entre reintentos y los contadores que comparten están en [Ejecutar el
colector](https://docs.jmrp.io/mikroscope/es/sinks/).

## Cobertura por destino

| 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](https://docs.jmrp.io/mikroscope/es/limits/privileged/).

## Fichero

```sh
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.

## Salida estándar

```sh
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](https://docs.jmrp.io/mikroscope/es/about/status/#not-tested).

- 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.

## Script SQL (`--sql`)

```sh
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.

### PostgreSQL (`--postgres`)

```sh
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](https://docs.jmrp.io/mikroscope/es/dashboards/import-and-check/#cinco-de-cinco-por-dos-caminos-distintos).

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.

> **Una contraseña en un DSN se ve en la lista de procesos**
>
> Una cadena de conexión pasada por línea de órdenes la puede leer cualquier proceso de la
> máquina. Pon la contraseña en `~/.pgpass` o en `PGPASSWORD`, o en
> `MIKROSCOPE_POSTGRES_DSN` dentro de un fichero de entorno, y no en la opción.

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](https://docs.jmrp.io/mikroscope/es/about/status/#las-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](https://docs.jmrp.io/mikroscope/es/sinks/device-info/)                              |

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](https://docs.jmrp.io/mikroscope/es/about/status/#verified-monitor-traffic-keys)), 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.

> **Una tubería hacia psql puede bloquear el colector**
>
> El destino SQL es síncrono, sin cola. Con `--sql -` alimentando `psql`, un `psql` que se retrasa —
> cada `INSERT` es su propia transacción y su propio commit — llena la tubería, y la siguiente
> escritura bloquea el bucle de extracción del colector en vez de descartar
> ([sin medir](https://docs.jmrp.io/mikroscope/es/about/status/#not-tested)). Escribe un fichero y aplícalo después, o
> envuelve el fichero en `BEGIN`/`COMMIT` a mano.

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.

## Loki

```sh
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](https://grafana.com/docs/loki/latest/reference/loki-http-api/)
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](https://grafana.com/docs/loki/latest/operations/multi-tenancy/) 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](https://docs.jmrp.io/mikroscope/es/sinks/detections/), 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:

```text
{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](https://docs.jmrp.io/mikroscope/es/about/status/#not-tested).

## OTLP

```sh
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](https://opentelemetry.io/docs/specs/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](https://docs.jmrp.io/mikroscope/es/about/status/#las-baterías-de-pruebas)). 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](https://docs.jmrp.io/mikroscope/es/sinks/derive/) 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](https://docs.jmrp.io/mikroscope/es/about/status/#not-tested)).

## Graphite

```sh
mikroscope forward --graphite carbon:2003 --graphite-prefix mikroscope
```

`--graphite` (`MIKROSCOPE_GRAPHITE_ADDR`) escribe el [protocolo de texto
plano](https://graphite.readthedocs.io/en/latest/feeding-carbon.html) 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](https://docs.jmrp.io/mikroscope/es/about/status/#not-tested).

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](https://docs.jmrp.io/mikroscope/es/dashboards/import-and-check/#la-fuente-de-datos-de-graphite)).

## Elasticsearch y OpenSearch

```sh
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](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk) y la de
[OpenSearch](https://docs.opensearch.org/latest/api-reference/document-apis/bulk/) 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](https://docs.jmrp.io/mikroscope/es/about/status/#not-tested) 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](https://docs.jmrp.io/mikroscope/es/dashboards/import-and-check/#la-fuente-de-datos-de-elasticsearch)).
Contra OpenSearch esa fuente de datos [no se ha probado](https://docs.jmrp.io/mikroscope/es/about/status/#not-tested).

## Telegraf

```sh
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`](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/http_listener_v2/README.md): 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](https://docs.jmrp.io/mikroscope/es/about/status/#campaign-line-protocol-2026-09-12)), 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](https://docs.jmrp.io/mikroscope/es/about/status/#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](https://docs.jmrp.io/mikroscope/es/about/status/#not-tested).

## Véase también

- [Ejecutar el colector](https://docs.jmrp.io/mikroscope/es/sinks/): la cola, la espera entre reintentos y los contadores que
  comparten estos destinos.
- [InfluxDB 3](https://docs.jmrp.io/mikroscope/es/sinks/influxdb/): las medidas que llevan `--stdout lp` y `--telegraf`.
- [Reglas de detección](https://docs.jmrp.io/mikroscope/es/sinks/detections/): lo que dicen las líneas, filas y documentos de
  detección.
- [Variables de entorno](https://docs.jmrp.io/mikroscope/es/reference/environment/): cada `MIKROSCOPE_*` que lee un
  destino.
