Ir al contenido

Captura por disparo

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.

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

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.

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); 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), 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.
  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.

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 explica por qué importa esa segunda proporción.

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.
Ventana de terminal
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.

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=):

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

record no reconoce la línea: Grabar, marcar y dibujar dice qué hace con ella.

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 tiene el detalle.

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