Descripción
Endpoint HTTP POST que recibe lotes de métricas del Agente Local (ping, TCP, SNMP, HTTP) y las procesa para actualizar:
- VictoriaMetrics — almacenamiento de series de tiempo (acepta todo, incluyendo histórico de 24h en reconexiones WS). Desde v1.83.1 la escritura es bloqueante: el endpoint espera el resultado real antes de responder (ver «Cambios en v1.83.1»).
- MonitoringTarget.last_status / last_check / last_packet_loss — estado ACTUAL en la UI de CreaRack (solo métricas frescas).
- Alertas (
monitoring.services.alert_service) — evalúa las alertas del tenant sobre el lote y difunde lo que dispara/cierra (ver «Cambios en v1.159.0»).
Ubicación: terminal/api/sentinel_ingest.py → función receive_bulk_metrics()
Ruta: POST /api/agent/metrics/bulk
Autenticación: Bearer token (generado por generate_agent_token(), app terminal).
El problema que resolvemos
El Agente Local re-envía automáticamente 24 horas de métricas históricas en cada reconexión del WebSocket (push_historical). Esto es correcto para gráficas (VictoriaMetrics dedup por timestamp) pero causaba que los replays viejos sobrescribieran el estado actual de los targets.
Caso de fallo capturado (2026-07-17, org 1):
- WS con flapping (desconexiones/reconexiones frecuentes).
- Replays de métricas de la mañana (loss=100, latency=0) re-llegaban cada pocos segundos.
- Los campos
last_statusylast_packet_lossdel target se pisaban constantemente. - Resultado: 130 targets clavados en “down” aunque la LAN estuviera viva (87 realmente arriba).
- Se re-empujaron 12.4M métricas históricas en 10h vs 547k producidas.
Solución: Guard de frescura (_TARGET_STATE_FRESHNESS = 120s)
Solo se procesan para actualizar estado vivo las métricas con timestamp < 2 minutos de antigüedad. Las más antiguas siguen yendo a VM (gráficas) pero no tocan MonitoringTarget.
_TARGET_STATE_FRESHNESS = 120 # segundos
for m in payload.metrics:
if m.timestamp < state_freshness_floor:
continue # VictoriaMetrics SÍ lo recibe, MonitoringTarget NO
Request / Response Schema
Header: Authorization: Bearer <agent_token>
Body (JSON): lista metrics[] con timestamp, target_id, metric_type (ping_latency, ping_loss, tcp_connect_ms, http_response_ms, http_status_code, snmp_response_ms), value.
200 — {"accepted": N, "rejected": 0, "errors": []}. 503 — VictoriaMetrics rechazó el lote. 500 — excepción durante el procesamiento (desde v1.83.1).
El Agente Local (terminal/agent/core/sync.py) reintenta con backoff ante cualquier 5xx, y desde el Agente 2.21.1 solo marca un lote como sincronizado si la respuesta llegó sin errors y con accepted + rejected cubriendo todas las filas enviadas.
Comportamiento
- Validación de timestamp: rechaza
> 1h futuroo> 7 días pasado; entre 120s y 7 días entra a VM solo. - Actualización de MonitoringTarget (solo timestamp fresco):
last_latency_ms,last_packet_loss,last_status,last_check. - Broadcast a canales WS:
metric_updatea los clientes suscritos al target (ver «Cambios en v1.159.0» para el nuevo agrupado por equipo). - Escritura a VictoriaMetrics — bloqueante (desde v1.83.1):
MetricsWriter.write_blocking()espera el resultado real; 503/500 si VM rechaza o falla. - Evaluación de alertas:
alert_service.evaluate_alerts_for_ingest(tenant_id, samples)sobre el lote (ver «Cambios en v1.159.0»).
Cambios en v1.61.2 / v1.83.1
- v1.61.2 (
a1a2dcf):_TARGET_STATE_FRESHNESS = 120, guard en el loop, test de regresión de replays. - v1.83.1 (
72239ae4, PR #430):MetricsWriter.write_blocking()— el endpoint responde 503/500 si VM no confirma, en vez de 200 conaccepted=0. Límite honesto: loswrite_*_syncdel camino cloud (fuera de este endpoint) siguen en fire-and-forget a propósito.
Cambios en v1.159.0 — mega-auditoría ronda 4, tanda T28 (25-09-2026, PR #607)
Con la organización de demostración (~480 equipos) cada lote del Agente generaba de golpe: un mensaje WS por equipo aunque nada hubiera cambiado, un UPDATE por fila aunque el estado fuera idéntico, y un async_to_sync por equipo para cada aviso (≈960 viajes a Valkey por lote solo en la organización de demo). Esta ronda ataca el volumen sin tocar qué se detecta.
E1-rendimiento-backend-01 — solo los eventos de alerta NUEVOS se anuncian (monitoring/services/alert_service.py:251-272,425-427): last_triggered se actualiza como mucho una vez por alerta y lote, en vez de en cada muestra que la sigue cumpliendo. La difusión (broadcast_alert_sync) se mueve a transaction.on_commit: antes salía dentro de la transacción de la petición, y el navegador podía recargar la lista de alertas antes de que el evento nuevo estuviera realmente guardado en BD.
E1-rendimiento-backend-05 / E2-rendimiento-frontend-agente-15 — un metric_update por equipo, no por muestra (terminal/api/sentinel_ingest.py:295-309,363-392,660-680, nuevo módulo terminal/api/sentinel_live.py): el lote agrupa las muestras por equipo (SNMP + si cambió su last_status) y emite un único mensaje WS por equipo, con un único async_to_sync por lote entero (antes, uno por equipo). La escritura en BD usa .only() sobre los campos de estado y bulk_update solo de las filas que de verdad cambiaron — al resto se les toca únicamente last_check.
B-55 — los equipos con solo tráfico evalúan sus alertas de ancho de banda: antes, un equipo que solo mandaba muestras de ancho de banda (sin ping en el mismo lote) no disparaba nunca sus alertas bandwidth_above/bandwidth_below por esta vía, porque la evaluación colgaba de que hubiera también una muestra de ping. Ahora se evalúan igual, sin depender de si el mismo lote trae ping.
Efecto colateral cerrado en el mismo PR (E1-06 del bloque de racks, no de monitoring): un lote sin la métrica que una alerta global vigila (p. ej. solo tráfico frente a una alerta de ping/down_for) ya no cierra ni reabre el evento agrupado global — antes, la ausencia de esa métrica en un lote se leía como “ya no se cumple” y el siguiente lote la reabría, re-anunciando el mismo evento.
Cliente: ObservatoryWebSocket.js recarga la lista de targets y las alertas activas al reconectar (?v=25), para no depender de haber recibido cada metric_update mientras estuvo desconectado.
Tests: tests/api/test_mega25_r4_ingesta.py (347 líneas) — dedup de last_triggered, un solo async_to_sync por lote, alertas de ancho de banda sin ping en el lote, tormentas que sobreviven a un lote sin su métrica. tests/monitoring/test_deuda_274_tanda_a.py::TestSentinelAlertBroadcast envuelve el POST en django_capture_on_commit_callbacks(execute=True) porque el aviso sale ahora tras el commit (ninguna aserción cambia).
Flujo de datos (actualizado v1.159.0)
Agent Local (.exe) — ping/TCP cada 15-30s, 24h history en reconexión
│ WebSocket
▼
receive_bulk_metrics() (sentinel_ingest.py)
│
├─ fresco → MonitoringTarget (.only() + bulk_update de filas cambiadas, last_check al resto)
│ │
│ └─ agrupado por equipo → sentinel_live.py → 1 metric_update/equipo, 1 async_to_sync/lote
│
├─ stale (>120s) → solo VictoriaMetrics
│
├─ alert_service.evaluate_alerts_for_ingest() → solo eventos NUEVOS → broadcast_alert_sync (transaction.on_commit)
│
└─ MetricsWriter.write_blocking() → 200 (accepted=N) | 503 (VM rechaza) | 500 (excepción)
Véase también
- [[entity—monitoring—model—monitoring-target]]
- [[entity—terminal—model—agent-instance]]
- [[concept—monitoring—agent-architecture]]
- [[concept—monitoring—victoriadb-deduplication]]
- [[incident—20260824—breaker-mixto-silencia-snmp-ccib]]
Referenciado desde
- Guard de frescura en MonitoringTarget — Prevenir sobrescritura de estado por replays históricos
- Incidente 24-08-2026 · el cortacircuitos mixto silenció el SNMP vivo del CCIB 15 minutos
- Incidente 24-08-2026 · el motor SNMP quedaba corrupto tras cuelgues seguidos — timeout duro + autocuración (Agente 2.21.2→2.21.3)
- Incidente: 130 targets clavados en DOWN con WS flapeando (org 1, 2026-07-17)
- monitoring/metric_names.py — contrato único de nombres de métrica (Agente → VictoriaMetrics)