Test de contrato de lectura/escritura de métricas
Test de contrato de lectura/escritura de métricas
Propósito
Archivo: tests/monitoring/test_metrics_read_write_contract.py
Valida el contrato entre:
- Escritores de métricas:
MetricsWriter(servidor) y el contrato demonitoring/metric_names.py(Agente) — ver [[entity—monitoring—service—metric-names]] - Lectores de métricas:
MetricsReader(consultas a VictoriaMetrics)
Garantiza que ninguna consulta del lector pregunte por una métrica que nadie escribe — evita errores de dedo, métricas retiradas olvidadas, y falsos negativos silenciosos.
Origen
Descubierto durante la ronda de mantenimiento del 06-08-2026 (task #213): el cálculo de disponibilidad (get_uptime) consultaba ping_reachable, pero esa métrica nunca llegaba a PROD porque el mapa de ingest del Agente no la produce. El test tradicional del CI no lo cazó (llevaba meses en verde con el fallo dentro).
A46 fase 1 (auditoría #261, PR #441, commit d48b681a, v1.85.4, 2026-08-27): hasta aquí el mapa (METRIC_TYPE_MAP) vivía inline en terminal/api/sentinel_ingest.py y este test lo leía con regex sobre el código fuente — el mismo patrón de fallo que dejó pasar el #259 y el #213 (“una capa escribe un nombre y otra lee otro”, detectado tarde porque nadie compara ambas capas contra una fuente única). Ahora el mapa vive en monitoring/metric_names.py y este test importa la tabla directamente en vez de rascarla con regex.
Componentes
1. _metricas_leidas() → set[str]
Extrae por regex los nombres literales que MetricsReader consulta en VictoriaMetrics:
def _metricas_leidas() -> set[str]:
return set(re.findall(r"([a-z_][a-z0-9_]*)\{\{tenant_id", READER.read_text(...)))
Busca patrones como ping_packet_loss_percent{{tenant_id=... en el cuerpo de metrics_reader.py.
2. _metricas_escritas() → set[str]
Extrae de dos vías de escritura:
-
MetricsWriter (servidor):
_create_metric("<nombre>", ...) # Busca `_create_metric("ping_latency_ms", ...)` -
Contrato del ingest del Agente (
monitoring/metric_names.py, desde A46 — antes se leía con regex desdeterminal/api/sentinel_ingest.py):from monitoring.metric_names import vm_names_written escritas |= vm_names_written() # union de AGENT_TO_VM.values() + IF_VM_NAMES.values()
Devuelve la unión de ambas.
3. Tests parametrizados
test_toda_metrica_leida_la_escribe_alguien()
def test_toda_metrica_leida_la_escribe_alguien():
huerfanas = _metricas_leidas() - _metricas_escritas()
assert not huerfanas, f"Métricas sin escritor: {sorted(huerfanas)}"
Si el lector consulta algo que nadie escribe → FALLA.
test_el_lector_consulta_algo()
Guarda anti-fallo-mudo: si el regex de extracción se rompe (porque cambió la forma de construir las consultas), el test de arriba pasaría vacío en silencio. Este verifica que _metricas_leidas() devuelva al menos 5 resultados.
test_disponibilidad_sale_de_una_metrica_viva(metrica)
Parametrizado para ping_packet_loss_percent. Específicamente verifica:
get_uptimesí contiene esa métrica en su cuerpo.- El contrato de
monitoring/metric_names.pysí la produce (metrica in AGENT_TO_VM.values()).
@pytest.mark.parametrize("metrica", ["ping_packet_loss_percent"])
def test_disponibilidad_sale_de_una_metrica_viva(metrica):
cuerpo = READER.read_text(...)
get_uptime = cuerpo[...índices de get_uptime...]
assert metrica in get_uptime, "get_uptime ya no usa ping_packet_loss_percent"
from monitoring.metric_names import AGENT_TO_VM
assert metrica in AGENT_TO_VM.values(), f"{metrica} ya no la produce el ingest"
Test hermano: contrato Agente → ingest (A18/A27)
tests/monitoring/test_contrato_agente_cadencia.py (23 tests, mismo PR #441) verifica el lado complementario: que todo metric_type que el Agente emite de verdad (leído por regex de terminal/agent/sentinel/*.py) lo acepta el ingest vía monitoring.metric_names.vm_name_for(). Este test de contrato (lector↔escritor) verifica que lo que se escribe tiene lector; el hermano (Agente↔ingest) verifica que lo que el Agente manda de verdad el ingest lo entiende — cierran el círculo completo Agente → ingest → VictoriaMetrics → lector.
Límite honesto (QUÉ SÍ y QUÉ NO detecta)
✅ SÍ detecta:
- Error de dedo en un nombre de métrica (
ping_reachabelvsping_reachable). - Métrica retirada del escritor pero olvidada en una consulta del lector.
- Nueva consulta que pida una métrica inexistente.
❌ NO detecta:
- Qué vía de escritura está viva en PROD:
ping_reachablesí la declaraMetricsWriter, pero el ingest del Agente (vía viva en PROD) no la produce. El test pasaba porque ambas vías existen en el código. El fallo solo fue visible comparando código + datos reales en producción. - Cambios en la lógica de traducción del Agente sin cambiar el contrato de
monitoring/metric_names.py. - Series reales que se dejan de escribir (cambios en el Agente sin reflejo en el servidor).
Conclusión: es una red barata que atrapa errores de configuración, pero no sustituye el monitoreo de datos reales.
Fuentes
READER = ROOT / "monitoring" / "services" / "metrics_reader.py"WRITER = ROOT / "monitoring" / "services" / "metrics_writer.py"monitoring/metric_names.py— contrato único de nombres (desde A46); ver [[entity—monitoring—service—metric-names]]terminal/api/sentinel_ingest.py— consume el contrato víavm_name_for(); ya NO declara el mapa inline (solo re-exporta símbolos por compatibilidad)
Véase también
- [[feature—monitoring—uptime-calculo-desde-packet-loss]]
- [[entity—monitoring—service—metrics-reader]]
- [[entity—monitoring—service—metrics-writer]]
- [[entity—terminal—service—sentinel-ingest]]
- [[concept—observability—metricas-victoria-metrics]]
- [[entity—monitoring—service—metric-names]] — contrato único de nombres del que depende este test (A46)