# Captura por disparo

Guarda en el agente muestras a cadencia completa en torno a una condición que fijas tú, tráelas por HTTP y cuenta los disparos que no se capturaron.

Source: https://docs.jmrp.io/mikroscope/es/record/triggers/

Fija una condición en el agente, y guardará las muestras alrededor del momento
en que salta, a cadencia completa, sin ninguna grabación en marcha. Trae la
captura por HTTP y dibújala con `plot`.

## Capturas

- **Una captura son las muestras que ya existen**, guardadas a cadencia completa
  alrededor del momento en que saltó una condición. Solo el agente puede
  guardarlas, porque solo el agente tiene todas las muestras. El anillo ya guarda
  los últimos 60 s por defecto, así que los segundos anteriores a un disparo no
  cuestan nada; los posteriores solo cuestan la espera. La ventana por defecto
  son 5 s a cada lado. Un `CAPTURE_PRE_S` mayor que el anillo (`--buffer`) es una
  ventana que el anillo no puede dar.
- **No decide nada sobre el significado.** Una condición es una comparación que
  configuraste tú. El campo que comparó y el valor que la hizo saltar viajan en
  la cabecera de la captura. La captura son las mismas líneas de deltas crudos
  que envía `/snapshot`; nada se convierte en un porcentaje ni en un veredicto.
- **No copia líneas.** El anillo guarda cada muestra como una línea
  precodificada e inmutable, y una captura fija las líneas que necesita, así que
  un disparo cuesta una copia de las cabeceras de las entradas una vez y nada por
  tick. El diseño lo estima en unos 3 µs para una ventana de 10 s a 10 Hz.
- **Las condiciones corren en el propio bucle del muestreador**, entre leer una
  muestra y meterla en el anillo, nunca en una segunda goroutine. No hay
  lenguaje de expresiones, a propósito: un analizador es una dependencia y una
  superficie de ataque, y una expresión que el operador puede escribir en el
  camino caliente del muestreador es una forma de volver lento el router.

## Configuración

El agente lee seis variables de la envlist de su contenedor. Dos de ellas tienen
una opción de `install`.

| Variable del agente    | Opción de `install` | Por defecto                                        | Valores aceptados                             | Qué ajusta                                                                                 |
| ---------------------- | ------------------- | -------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `TRIGGERS`             | `--triggers`        | `softnet-drop,oom,kmsg<=3,reset,irq-err,flash-bad` | las condiciones de abajo, separadas por comas | Qué condiciones arman una captura.                                                         |
| `CAPTURE_MB`           | `--capture-mb`      | `4`                                                | `0`–`256`                                     | El presupuesto de bytes fijados del anillo, en MiB. `0` desactiva la función.              |
| `CAPTURE_PRE_S`        | ninguna             | `5`                                                | `1`–`60`                                      | Segundos que se guardan antes de la muestra que disparó.                                   |
| `CAPTURE_POST_S`       | ninguna             | `5`                                                | `1`–`60`                                      | Segundos que se guardan después.                                                           |
| `CAPTURE_POLICY`       | ninguna             | `first`                                            | `first`, `last`                               | Con el presupuesto lleno: `first` rechaza la captura nueva, `last` expulsa la más antigua. |
| `TRIGGER_REFRACTORY_S` | ninguna             | `10`                                               | `0`–`3600`                                    | Tiempo de silencio por condición después de disparar.                                      |

- `install` siempre escribe `CAPTURE_MB`, y escribe `TRIGGERS` solo cuando se da
  `--triggers`; sin ella el agente usa su conjunto por defecto. No escribe
  ninguna de las otras cuatro, así que un agente instalado funciona con sus
  valores por defecto. `mikroscope plan` muestra las entradas de la envlist antes
  de escribir nada.
- `--triggers` pasa por el propio analizador del agente antes de la primera
  conexión. Una condición desconocida, un umbral fuera de rango, una comilla o un
  punto y coma hacen fallar la orden con código de salida 2 sin escribir nada.
- El agente vuelve a analizar la lista al arrancar, porque una envlist se puede
  editar a mano en el router. Un valor que rechace allí hace que se niegue a
  funcionar, con una línea en su salida estándar, que RouterOS lleva a su log.

## Condiciones

El conjunto por defecto son todas las condiciones sin umbral salvo `squeeze` —
`softnet-drop`, `oom`, `reset`, `irq-err` y `flash-bad`, cada una de las cuales
dispara cuando el kernel cuenta algo que normalmente no cuenta — más `kmsg<=3`.
Las condiciones de nivel no están en él: sus umbrales los eliges tú.

| Condición      | Dispara cuando                                                                            | `field` en la cabecera       | Umbral       | En el conjunto por defecto |
| -------------- | ----------------------------------------------------------------------------------------- | ---------------------------- | ------------ | -------------------------- |
| `softnet-drop` | alguna cola softnet descartó un paquete en la muestra                                     | `softnet[N].dropped`         | ninguno      | sí                         |
| `squeeze`      | alguna cola softnet se quedó sin presupuesto (`time_squeeze`) en la muestra               | `softnet[N].time_squeeze`    | ninguno      | no                         |
| `oom`          | el kernel mató algo por falta de memoria (`vm.oom_kill` se movió)                         | `vm.oom_kill`                | ninguno      | sí                         |
| `reset`        | un contador retrocedió de una forma que no es un desbordamiento de 32 bits                | `resets`                     | ninguno      | sí                         |
| `irq-err`      | la fila `Err` de `/proc/interrupts` se movió                                              | `irq_err`                    | ninguno      | sí                         |
| `flash-bad`    | el recuento de bloques defectuosos de una partición YAFFS subió desde la muestra anterior | `flash[<device>].bad_blocks` | ninguno      | sí                         |
| `kmsg<=N`      | un registro del log del kernel de severidad N o más grave (0 es emergencia, 3 error)      | `events.level`               | `0`–`7`      | `kmsg<=3`                  |
| `busy>=X`      | la proporción de ocupación de algún núcleo está en X o por encima                         | `cpu[N].busy_ratio`          | `0.05`–`1`   | no                         |
| `slip>=X`      | el intervalo de la muestra fue de al menos X periodos del muestreador                     | `dt_ns/period`               | `1.1`–`100`  | no                         |
| `memfall>=N`   | `MemAvailable` cayó N o más en un tick                                                    | `mem.MemAvailable fall (MB)` | `1`–`100000` | no                         |

- Cuando una condición abarca varios núcleos, colas o particiones, la cabecera
  nombra el primero que coincidió.
- `memfall` compara los kB de `/proc/meminfo` divididos entre 1 024, así que su
  N está en MiB aunque el campo diga MB.
- `kmsg<=N` necesita el log del kernel, que el agente solo lee en un contenedor
  privilegiado ([Modo privilegiado](https://docs.jmrp.io/mikroscope/es/limits/privileged/));
  `flash-bad` necesita una partición YAFFS. Una condición cuya fuente no existe
  nunca dispara.
- `squeeze` no es de las de por defecto porque los time squeezes pueden ser
  ruido de fondo en un router sano ([medido](https://docs.jmrp.io/mikroscope/es/about/status/#campaign-squeeze-2026-09-16)),
  y una regla que salte
  ante cualquier squeeze salta entonces sin parar. La regla de microrráfagas del
  colector exige en su lugar un episodio de tres desviaciones.
- Una captura también se puede armar a mano, con `POST /capture` (abajo). Su
  causa es `manual` y su `field` es el motivo que diste.

## Ciclo de una captura

1. Una condición es cierta en la muestra S. Si esa condición disparó hace menos
   de `TRIGGER_REFRACTORY_S` × cadencia muestras (esos segundos a la cadencia
   nominal), el disparo se **suprime** (motivo `refractory`). Si otra captura aún
   está recogiendo su ventana, el disparo se **suprime** (motivo `pending`): se
   recoge una captura cada vez, sea cual sea la condición que la armó.
2. Si no, se arma una captura para la ventana desde S − `CAPTURE_PRE_S` ×
   cadencia hasta S + `CAPTURE_POST_S` × cadencia, y se pone en cola una línea
   `{"trigger":{…}}` para el flujo.
3. Cuando la muestra del final de la ventana está en el anillo, la captura fija
   las líneas del anillo de esa ventana. Si el anillo ya no guarda ninguna de
   ellas, la captura se **rechaza** (`empty`).
4. Si los bytes de la ventana por sí solos superan el presupuesto, se
   **rechaza** (`budget`). Si el presupuesto está lleno, `first` la rechaza
   (`budget`) y `last` expulsa las capturas más antiguas hasta que quepa.

Una captura cuya ventana tiene menos de `(pre + post) × rate + 1` muestras se
conserva con `complete: false` en vez de quedarse corta en silencio. Eso ocurre
cuando el anillo no guardaba la ventana entera: un disparo dentro de los
`CAPTURE_PRE_S` primeros segundos tras arrancar el agente, o un anillo
(`BUFFER_S`) más corto que la ventana. Cuando el agente se detiene, recoge una
captura pendiente con lo que guarda el anillo, pero no puede servirla: las
capturas están en memoria, y el servidor HTTP se detiene con el agente.

Las capturas viven en la memoria del agente. Nada las escribe a disco, así que un
reinicio del contenedor, un `upgrade` o un reinicio del router pierde las que aún
no se han descargado.

### Tamaño de una captura

El tamaño de una captura es el número de muestras de su ventana por el tamaño de
línea. Una línea con todas las fuentes activas, PMU incluido, ocupa 3 230 B.
Por aritmética a partir de esa línea:

| Cadencia | Ventana por defecto (5 s + 5 s) | Capturas en los 4 MiB por defecto |
| -------- | ------------------------------- | --------------------------------- |
| 10 Hz    | 101 muestras, unos 330 kB       | 12                                |
| 50 Hz    | 501 muestras, unos 1,6 MB       | 2                                 |
| 100 Hz   | 1 001 muestras, unos 3,2 MB     | 1                                 |

Una placa mayor — más núcleos, más líneas de interrupción — tiene líneas más
largas. El campo `bytes` de cada captura es la cifra real.

El presupuesto es memoria que el agente retiene además de su anillo: una línea
fijada sigue viva cuando el anillo ya la ha dejado atrás, así que el agente
comprueba ambos juntos al arrancar.

- Cuando el agente puede leer el `memory.max` del contenedor y unos
  `rate × buffer ×` 3 456 B más `CAPTURE_MB` lo
  superan, se niega a arrancar, nombrando los tres ajustes que bajar o
  `--memory-max` para subir.
- Avisa si el doble de eso supera su límite blando de memoria de Go,
  `MEM_LIMIT_MB` (de 8 a 1 024 MiB). El valor por defecto del propio agente para
  `MEM_LIMIT_MB` es 14. `install` lo deriva en cambio del anillo (cadencia ×
  buffer × línea, × 2,5, como mínimo 16 y como mucho tres cuartos de
  `--memory-max`), lo que escribe 16 con los valores por defecto, y
  `--mem-limit-mb` lo fija a mano.
- Esa derivación no cuenta `CAPTURE_MB`, así que un presupuesto de capturas
  grande puede seguir provocando el aviso. [Coste del agente](https://docs.jmrp.io/mikroscope/es/cost/)
  explica por qué importa esa segunda proporción.

## Leer capturas

Cuatro endpoints en el agente. Cada uno necesita el token bearer cuando el
agente lo tiene, y cada uno responde `404` con
`captures disabled (CAPTURE_MB=0)` cuando la función está desactivada.

| Petición                 | Respuesta                                                                                                                                                                                                      |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /captures`          | El índice, en JSON.                                                                                                                                                                                            |
| `GET /captures/<id>`     | Una línea de cabecera `{"capture":{…}}`, y después las líneas de muestra tal cual, como NDJSON.                                                                                                                |
| `DELETE /captures/<id>`  | Libera la parte del presupuesto de esa captura; `204`.                                                                                                                                                         |
| `POST /capture?reason=…` | Arma una captura manual en la muestra más reciente: `{"id":N,"armed":true}`, o `409` cuando hay una captura pendiente o el disparo manual está en su ventana refractaria. El motivo por defecto es `operator`. |

```sh
curl -s -H "Authorization: Bearer $MIKROSCOPE_TOKEN" http://172.30.10.2:9123/captures
curl -s -H "Authorization: Bearer $MIKROSCOPE_TOKEN" http://172.30.10.2:9123/captures/3 > cap3.jsonl
mikroscope plot --in cap3.jsonl
curl -s -X DELETE -H "Authorization: Bearer $MIKROSCOPE_TOKEN" http://172.30.10.2:9123/captures/3
```

El índice lleva la `policy`, `budget_bytes`, los `bytes` retenidos, la captura
aún `pending` si la hay, los `triggers` configurados y una entrada por captura:
`id`, `cause`, `condition`, `field`, `value`, `threshold`, `fire_seq`,
`fire_mono_ns`, `fire_wall_ns`, `first_seq`, `last_seq`, `samples`, `bytes` y
`complete`.

Las líneas de muestra de `GET /captures/<id>` son idénticas byte a byte a lo que
sirve `/snapshot` para las mismas muestras, así que una herramienta que lee un
snapshot no necesita un analizador nuevo; `plot` salta la línea de cabecera. La
CLI no tiene orden para las capturas: usa cualquier cliente HTTP.

> **Las capturas necesitan el camino directo**
>
> El transporte relay tira a través de `/tool fetch`, que devuelve como mucho 64 512 B y no envía token. Una captura con los valores por defecto ocupa unos 330 kB, cinco veces el tope de la petición. Trae las capturas
> desde un equipo que llegue al agente directamente, o a través de `--expose`: [Acceso
> por red](https://docs.jmrp.io/mikroscope/es/install/reaching-the-agent/) cubre ambos.

## Tratamiento en el colector

Cuando se arma una captura, el agente coloca una línea antes de la muestra en la
que disparó, en `/stream` y en `/snapshot?since=` (no en `/snapshot?seconds=`):

```text
{"trigger":{"id":3,"cause":"busy>=0.95","field":"cpu[2].busy_ratio","value":1,"threshold":0.95,"seq":48213,"wall_ns":1789000000000000000}}
```

- Los valores de arriba son ilustrativos. La línea es de un tipo propio, como la
  línea `{"gap":…}`, no un campo de la muestra, así que el esquema de la muestra
  no cambia.
- El agente guarda las últimas 64 líneas de disparo para quien tire de él, así
  que quien va más de 64 disparos por detrás nunca ve las más antiguas. Un
  disparo suprimido no produce ninguna.
- Una captura manual dispara sobre la muestra más reciente que ya está en el
  anillo, así que quien ya ha recibido esa muestra no recibe línea de disparo
  para ella; lee `/captures` en su lugar.

`forward` reconoce la línea, nunca la confunde con una muestra, la cuenta y se la
entrega a cada destino como anotación. La captura en sí se queda en el agente, en
`/captures/<id>`.

| Destino                      | Registro del disparo                             |
| ---------------------------- | ------------------------------------------------ |
| InfluxDB                     | la medida `mikroscope_trigger`                   |
| SQL                          | la tabla `mikroscope_trigger`                    |
| Prometheus (la del colector) | `mikroscope_collector_triggers_total{cause}`     |
| Elasticsearch                | un documento `trigger`                           |
| Graphite                     | `trigger.<cause>`                                |
| Loki                         | una línea `source="trigger"`                     |
| OTLP                         | la suma `mikroscope.trigger.fired{cause}`        |
| fichero                      | la propia línea                                  |

Los cinco dashboards de Grafana llevan una anotación `triggers`, desactivada por
defecto en la barra de conmutadores, junto a una `detections` que está activada.
Cada una lee su propio almacén:

| Dashboard            | Consulta de la anotación                                                                                                                                                                              |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| InfluxDB, PostgreSQL | las filas de `mikroscope_trigger`                                                                                                                                                                     |
| Prometheus           | el `mikroscope_trigger_fired_total` del colector                                                                                                                                                      |
| Elasticsearch        | el filtro Lucene `kind:trigger AND host.keyword:$host`, con el `field` del documento como texto y su `cause` como etiqueta (`kind:detection`, `message` y `rule` para las detecciones)                |
| Graphite             | `aliasByNode($prefix.$host.trigger.*, 3)`: un marcador por punto titulado con la causa, sin texto, porque Graphite no guarda cadenas (`detection.*` y la regla para las detecciones)                  |

Las anotaciones de Elasticsearch y Graphite se comprueban como JSON generado; no
se han [ejecutado en Grafana](https://docs.jmrp.io/mikroscope/es/about/status/#not-tested).

`record` no reconoce la línea: [Grabar, marcar y
dibujar](https://docs.jmrp.io/mikroscope/es/record/#disparadores-durante-una-grabación) dice qué hace
con ella.

## Disparos suprimidos

El `/metrics` del colector lleva las familias que dicen cuánto no vieron las
capturas, construidas a partir de los contadores de `GET /sampler` del agente,
que el colector lee al arrancar y cada minuto. Cada par de condición y motivo se
expone desde el principio, a 0 hasta que ocurre, así que un panel puede mostrar
«0 hasta ahora».

| Familia                                                 | Tipo    | Significado                                                                                                     |
| ------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------- |
| `mikroscope_trigger_fired_total{condition}`             | counter | Veces que cada condición armó una captura; `condition="manual"` aparece cuando se ha armado una captura manual. |
| `mikroscope_trigger_suppressed_total{condition,reason}` | counter | Veces que una condición fue cierta y no se armó nada: `refractory` o `pending`.                                 |
| `mikroscope_capture_refused_total{reason}`              | counter | Capturas recogidas y después no conservadas: `budget` o `empty`.                                                |
| `mikroscope_captures_held`                              | gauge   | Capturas retenidas en este momento.                                                                             |
| `mikroscope_capture_bytes`                              | gauge   | Bytes del anillo que fijan las capturas retenidas.                                                              |
| `mikroscope_capture_budget_bytes`                       | gauge   | El presupuesto, a partir de `CAPTURE_MB`.                                                                       |
| `mikroscope_capture_bytes_served_total`                 | counter | Bytes entregados por `/captures/<id>`.                                                                          |

El colector no puede recalcularlas a partir de las muestras; las lee del
`GET /sampler` del agente y las representa en su propia exposición, así que un
único trabajo de scrape sobre el colector las lleva.
[Prometheus](https://docs.jmrp.io/mikroscope/es/sinks/prometheus/) tiene el detalle.

## Limitaciones

- **El conjunto de capturas es una muestra de los eventos, nunca un censo.** La
  ventana refractaria y el presupuesto de bytes acotan una tormenta de disparos,
  y se recoge una captura cada vez. `mikroscope_trigger_suppressed_total` y
  `mikroscope_capture_refused_total` son cuánto no se vio.
- **Cadencia completa no es detalle completo.** Una captura guarda muestras a la
  cadencia del muestreador: a 10 Hz nada más corto que 100 ms es visible con
  fiabilidad, y una proporción de ocupación sigue moviéndose en los escalones de
  tick del kernel. [Límites de resolución](https://docs.jmrp.io/mikroscope/es/limits/) fija ese
  límite, no la captura.
- **Una ventana puede quedarse corta.** Una que el anillo no guardaba entera se
  sirve con `complete: false`. Una cortada porque el agente se detuvo se recoge
  pero nunca se sirve, porque las capturas se detienen con el agente.
- **Descargar le cuesta al router.** Una descarga corre en el mismo núcleo que el
  muestreador, y se contabiliza en `mikroscope_capture_bytes_served_total` igual
  que se le carga a cualquiera que tire del agente.

El coste de un disparo en el equipo, una tormenta sostenida de disparos, las
capturas a 50 o 100 Hz y el tamaño de capturas reales están en la lista de lo no [probado](https://docs.jmrp.io/mikroscope/es/about/status/#not-tested).

## Véase también

- [Grabar, marcar y dibujar](https://docs.jmrp.io/mikroscope/es/record/): una grabación que empiezas
  tú, con marcadores, y el gráfico que `plot` dibuja también a partir de una
  captura.
- [Endpoints HTTP](https://docs.jmrp.io/mikroscope/es/reference/http/): `/captures`, `/stream` y
  `/snapshot` junto al resto.
- [Métricas de Prometheus](https://docs.jmrp.io/mikroscope/es/reference/metrics/): las familias de
  disparo y captura con todas las demás familias de la exposición del colector.
- [Límites de resolución](https://docs.jmrp.io/mikroscope/es/limits/): lo que la cadencia completa
  puede y no puede resolver.
