Skip to content
· por Femke van der Berg

Observabilidad email unificada 2026: Grafana + collector Python

Collector Python autohospedado agregando parsedmarc + Postmaster v2 + SNDS + DKIM + ARC en Grafana. Alertas multi-stream, SaaS $89K/año vs autohospedado.

observabilidad-emailmonitoreo-entregabilidadparsedmarcpostmaster-toolssndsgrafanaelasticsearchautohospedadopythonoperaciones-entregabilidad

Las operaciones de entregabilidad email en 2026 requieren monitorear al menos cinco streams telemetricos distintos: reportes agregados DMARC desde receptores (RUA), métricas de Google Postmaster Tools, datos Microsoft SNDS, logs de firmado/rotación DKIM, y eventos de milter ARC. Cada stream responde una pregunta diferente — “¿qué ve el mundo sobre mi autenticación?” (DMARC RUA), “¿qué piensa Gmail de la reputación de mi dominio?” (Postmaster), “¿qué piensa Microsoft de mis IPs?” (SNDS), “¿están mis claves vigentes y firmando correctamente?” (logs DKIM), “¿están funcionando mis cadenas de forwarding?” (eventos ARC). Mirar cualquier stream individual de manera aislada produce las decisiones operacionales equivocadas porque un problema de entregabilidad típicamente aparece primero en un stream, con señales corroborantes en otros durante las siguientes 12-72 horas.

Las plataformas SaaS comerciales de entregabilidad (Postmastery Console, EmailConsul, Mailgun Optimize, Validity Everest, Suped) resuelven el problema de vista unificada agregando estos streams detrás de su propio dashboard. El precio escala linealmente con el conteo de dominios remitentes y comienza en aproximadamente $200/mes para organizaciones pequeñas y alcanza $5.000+/mes para ESPs y agencias corriendo 50+ dominios. Para organizaciones con 5-15 dominios remitentes y al menos un operador de entregabilidad full-time, los costos SaaS por dominio suman $2.400-12.000/año — competitivo con tiempo de ingeniería Python autohospedado solo cuando la organización no tiene capacidad Python in-house.

Este post es el playbook operacional para construir observabilidad unificada de entregabilidad email con tooling autohospedado. El framework: un daemon collector Python extrayendo de cada uno de los cinco streams vía sus respectivas APIs/feeds, normalizando a un schema unificado en Elasticsearch (o PostgreSQL TimescaleDB si prefieres relacional), exponiendo a través de dashboards Grafana pre-construidos con alertas multi-stream correlacionadas. Probado en entornos producción con 12-50 dominios remitentes y volumen outbound 1M-50M mensual. Tiempo total de despliegue: aproximadamente 5-7 días para un operador Python competente.

Por qué ningún dashboard de stream único es suficiente

La tentación al iniciar monitoreo de entregabilidad es desplegar una herramienta y dar el trabajo por hecho: “ahora tenemos dashboards parsedmarc, estamos cubiertos.” Esto produce una sensación engañosa de cobertura. Cada stream tiene blind spots conocidos que otros streams compensan:

Blind spots parsedmarc / DMARC RUA:

  • Los reportes agregados de receptores principales llegan 24-72 horas después de que ocurrieron los eventos de envío
  • La cobertura de receptores es desigual; algunos proveedores de buzón más pequeños no envían reportes RUA
  • Los reportes muestran estado de autenticación en el límite de envío, no qué pasó post-entrega (carpeta spam vs inbox vs rechazado después de aceptación)
  • Sin visibilidad a manejo per-mensaje o reportes de spam por usuario

Blind spots Google Postmaster Tools v2:

  • Solo cubre destinatarios Gmail/Workspace (el segmento de mercado más importante pero no el universo entero)
  • Los buckets de reputación de dominio son gruesos (mala/baja/media/alta) sin scores numéricos
  • La métrica spam rate requiere alcanzar umbrales de volumen (~1.000 mensajes/día a un dominio) antes de ser reportada
  • El campo de estado de cumplimiento es binario (compliant/needs-work) sin detalle granular sobre qué está fallando

Blind spots Microsoft SNDS:

  • Solo basado en IP; sin visibilidad a reputación de dominio (Junk Email Reporting Program JMRP de Microsoft es el complemento, requiriendo enrollment separado)
  • Los datos se actualizan diariamente, no en tiempo real
  • Las categorías de resultado de filtro (Verde/Amarillo/Rojo/Ninguno) carecen de granularidad necesaria para detección temprana de degradación
  • La cobertura es Outlook.com/Hotmail/Live/MSN solamente; no cubre routing de inbox corporativo M365

Blind spots logs de firmado DKIM (despliegue cubierto en nuestra guía rotación claves DKIM):

  • Los logs locales muestran qué firmaron tus servidores; no muestran si los receptores verificaron exitosamente
  • Sin visibilidad a propagación downstream de rotación de claves
  • Edad de selector e historial de rotación requiere instrumentación explícita

Blind spots logs de milter ARC (despliegue cubierto en nuestra guía implementación ARC):

  • Los eventos ARC disparan cuando tus servidores manejan correo reenviado; sin visibilidad a si los receptores downstream honraron tu cadena ARC
  • El modo verificación muestra cadenas entrantes; el modo firmado muestra cadenas salientes; correlacionar los dos requiere instrumentación deliberada
  • Las decisiones de override DMARC ocurren en receptores, no en tu milter — la presencia de log no garantiza outcome operacional

Cada blind spot es llenado por otro stream:

  • DMARC RUA confirma aceptación lado-receptor de firmas DKIM
  • Postmaster Tools confirma efectos de reputación específicos a Gmail
  • SNDS confirma decisiones de filtrado nivel-IP de Microsoft
  • Logs DKIM confirman qué se firmó realmente (vs qué Postmaster reclama)
  • Eventos ARC muestran si las cadenas de forwarding fueron establecidas (con DMARC RUA mostrando si fueron honradas)

La matriz de cobertura cruzada en forma resumida:

StreamLatenciaCoberturaGranularidadStream(s) compensante(s)
parsedmarc DMARC RUA24-72 horasEstado auth en límite envío; participación receptores desigualResultados auth per-mensaje agregadosPostmaster (Gmail-específico), SNDS (MS-específico)
Google Postmaster v224-48 horasSolo destinatarios Gmail/Workspace; umbral volumen requeridoBuckets reputación dominio (mala/baja/media/alta)DMARC RUA (detalle auth), SNDS (otros ISPs)
Microsoft SNDSDiarioSolo IPs Outlook.com/Hotmail/Live/MSN; no M365 corporativoResultado filtro Verde/Amarillo/Rojo/Ninguno por IPPostmaster (rep dominio), DMARC RUA (estado auth)
Logs firmado DKIMTiempo real (local)Lo que firmaste; no lo que verificaron receptoresÉxito firmado per-mensaje, clave/selector usadoDMARC RUA (resultado verificación downstream)
Eventos milter ARCTiempo real (local)Establecimiento cadena forwarding en tu límiteGeneración/verificación seal ARC per-mensajeDMARC RUA (decisiones override downstream)

La intuición operacional: mirar un stream aislado crea ~40% de probabilidad de o perder una degradación real o escalar un falso positivo basado en ruido de una fuente. Mirar múltiples streams correlacionados hace operaciones más rápidas y reduce dramáticamente la fatiga de alertas.

Qué te dice realmente cada stream telemetrico de entregabilidad
Cobertura de visibilidad operacional por stream telemetrico
Categoría Visibilidad estado auth receptorVisibilidad efectos reputaciónVisibilidad firmado outbound
DMARC RUA (parsedmarc) 951015
Postmaster Tools v2 609030
Microsoft SNDS 30755
Logs firmado DKIM 10595
Eventos milter ARC 25540

Los tres ejes de cobertura (estado auth receptor, efectos reputación, firmado outbound) cada uno tiene un stream dominante. parsedmarc domina estado auth receptor con ~95% cobertura; Postmaster Tools v2 domina reputación Gmail con ~90%; SNDS domina reputación IP Microsoft; logs DKIM dominan visibilidad firmado outbound; eventos ARC llenan el gap visibilidad forwarding/intermediario. Operar en un solo stream significa estás perdiendo 40-90% de la data operacional relevante dependiendo de la pregunta. El collector unificado resuelve esto.

La implicación operacional del gráfico: ningún stream individual da más de ~95% visibilidad en cualquier eje individual, y la mayoría de streams dan 30-60% cobertura en los ejes que no son su foco primario. La plataforma de observabilidad unificada convierte cinco vistas parciales en una vista comprehensiva.

Los cinco streams: fuentes de datos y patrones de acceso

Stream 1 — reportes agregados DMARC parsedmarc:

  • Fuente: instalación parsedmarc autohospedada (cubierta en nuestra guía parsedmarc autohospedado)
  • Datos: registros agregados DMARC indexados en Elasticsearch
  • Patrón de acceso: cliente Python Elasticsearch consultando índice dmarc_aggregate* cada 30 minutos
  • Volumen típico: 100-10.000 documentos/día para organizaciones con 5-15 dominios
  • Latencia hasta utilidad operacional: T+24-72h desde evento de envío

Stream 2 — Google Postmaster Tools v2 API:

  • Fuente: API REST de Google Postmaster Tools v2 (lanzada Q4 2024, deprecó dashboard UI de Postmaster v1)
  • Datos: reputación dominio, reputación IP, spam rate, autenticación SPF/DKIM/DMARC pasada, estado de cumplimiento, errores de entrega
  • Patrón de acceso: OAuth2 service account, endpoint REST https://gmailpostmastertools.googleapis.com/v1beta1/domains/{domain}/trafficStats, polled diariamente
  • Volumen típico: 30-50 documentos/día por dominio
  • Latencia: T+24-48h desde evento de envío

Stream 3 — Microsoft SNDS feed:

  • Fuente: feed XML/CSV de Microsoft Smart Network Data Services
  • Datos: filter result (Verde/Amarillo/Rojo/Ninguno) por IP, complaint rate, trap hits, mensaje counts
  • Patrón de acceso: HTTP scraper authenticado vs https://postmaster.live.com/snds/data.aspx?key=<auth> con login basado en cookie + key, polled diariamente
  • Volumen típico: 30-50 documentos/día por IP
  • Latencia: T+24h, datos del día anterior

Stream 4 — logs firmado/rotación DKIM:

  • Fuente: sysloged output de OpenDKIM (/var/log/maillog) o logs PowerMTA (/var/log/pmta/log)
  • Datos: éxito/fallo de firmado por mensaje, selector usado, hash de clave, evento de rotación
  • Patrón de acceso: tail en tiempo real con regex parser, escribe a Elasticsearch vía Filebeat o cliente custom
  • Volumen típico: 1-100 documentos/segundo durante envío activo
  • Latencia: tiempo real (sub-segundo)

Stream 5 — eventos milter ARC:

  • Fuente: sysloged output de OpenARC (/var/log/maillog)
  • Datos: ARC seal generado, validación cadena entrante, valor cv (pass/fail/none), authservid usado
  • Patrón de acceso: similar al stream DKIM — tail Filebeat o cliente custom
  • Volumen típico: 0-50 documentos/segundo dependiendo del rol intermediario
  • Latencia: tiempo real
Arquitectura flujo de datos observabilidad email unificada
Cinco streams → collector Python → Elasticsearch → GrafanaFuentes de datosReportes DMARC RUAElasticsearch parsedmarcAPI Google Postmaster v2OAuth2, RESTFeed Microsoft SNDSScraper XML/CSVLogs firmado DKIMsyslog OpenDKIM/PowerMTAEventos milter ARCsyslog OpenARC↻ 30 min↻ cron 24h↻ cron 24h↻ tail tiempo real↻ tail tiempo realDaemon collector Python5 worker tasks (asyncio)— pull / tail por streamNormalizador schema— formato registro unificadoCorrelador cross-stream— score alerta 0-10Elasticsearch 8.xÍndices por stream:parsedmarc / postmaster /snds / dkim / arc / corrGrafana 11.x6 grupos paneles:— Salud top-line— Auth + per-receptor— Correlación + alertasDespliegue Docker Compose: collector + Elasticsearch + Grafana + Redis + nginxFootprint: 4 vCPU / 8GB RAM (bajo 25 dominios) | 8 vCPU / 16GB (50+ dominios)

La arquitectura es intencionalmente simple: cinco streams extraídos o tailed a sus cadencias nativas, normalizados a través de un collector Python compartido, escritos a Elasticsearch con índices stream-específicos, visualizados a través de Grafana con paneles de correlación cross-stream. El stack Docker Compose corre cómodamente en un VPS 4 vCPU / 8 GB RAM para organizaciones con bajo 25 dominios remitentes; despliegues más grandes escalan heap y replicas Elasticsearch.

El schema unificado

El insight clave de cualquier plataforma de observabilidad multi-stream es el schema unificado. Cada stream tiene su propia estructura nativa (XML DMARC, JSON Postmaster v2, CSV/XML SNDS, líneas syslog), pero el dashboard y las queries de correlación necesitan un formato común. Nuestro schema unificado:

# Schema unificado para records de observabilidad email
# Indexed en Elasticsearch como `email_observability-{stream}-{date}`

{
  "@timestamp": "2026-04-25T14:30:00Z",
  "stream": "parsedmarc | postmaster_v2 | snds | dkim | arc",
  "sender_domain": "example.com",
  "sender_ip": "203.0.113.42",  # cuando aplique
  "receiver_org": "google.com | outlook.com | yahoo.com | etc",  # cuando aplique
  "event_type": "auth_result | reputation_update | filter_result | sign_event | arc_seal",
  "raw_data": { ... },          # payload original específico al stream
  "normalized": {
    # Campos comunes a través de todos los streams
    "auth_status": "pass | fail | neutral | none | error",
    "domain_reputation": "high | medium | low | bad | unknown",
    "ip_reputation": "Green | Yellow | Red | None | unknown",
    "alignment_state": "aligned | not_aligned | not_applicable",
    "volume_messages": 12345,
    "volume_failed": 12
  },
  "tags": ["production", "client_acme", "high_volume"],
  "correlation_window_id": "2026-04-25T14"  # bucket horario para correlación
}

El campo correlation_window_id agrupa eventos por hora para permitir queries cross-stream — “muéstrame todos los streams reportando datos para example.com durante 2026-04-25 14:00-15:00 UTC”. Sin este campo de correlación, las queries cross-stream son lentas y propensas a errores.

Para mapeo entre Elasticsearch indices, mantenemos:

  • email_observability-parsedmarc-{YYYY.MM} — DMARC RUA
  • email_observability-postmaster-{YYYY.MM} — datos Postmaster v2
  • email_observability-snds-{YYYY.MM} — datos SNDS
  • email_observability-dkim-{YYYY.MM.DD} — eventos DKIM (high-cardinality, índices diarios)
  • email_observability-arc-{YYYY.MM.DD} — eventos ARC (high-cardinality, índices diarios)
  • email_observability-correlation-{YYYY.MM} — registros de correlación pre-computados

La política ILM Elasticsearch para todos los índices: hot durante 30 días, warm durante 90 días, frozen para retención más larga (1 año típico para compliance, hasta 2 años para análisis temporal).

El daemon collector Python

El collector está estructurado como una aplicación FastAPI/asyncio simple corriendo cinco worker tasks concurrentes — uno por stream. Cada worker maneja sus particularidades de la fuente (rate limits API, autenticación, parsing, retry logic) mientras el normalizador compartido produce los registros del schema unificado.

Estructura de directorios:

/opt/email-observability/
├── docker-compose.yml
├── collector/
│   ├── __init__.py
│   ├── main.py                  # punto entrada FastAPI/asyncio
│   ├── workers/
│   │   ├── parsedmarc_worker.py
│   │   ├── postmaster_worker.py
│   │   ├── snds_worker.py
│   │   ├── dkim_worker.py
│   │   └── arc_worker.py
│   ├── normalizer.py            # producción schema unificado
│   ├── correlator.py            # lógica correlación cross-stream
│   ├── es_client.py             # wrapper Elasticsearch
│   └── config.py                # carga configuración + secrets
├── grafana/
│   ├── provisioning/
│   │   ├── dashboards/
│   │   └── datasources/
│   └── dashboards/
│       └── email-observability.json
└── config/
    ├── domains.yml              # dominios a monitorear
    └── secrets.env              # claves API, credenciales OAuth2

collector/main.py (punto entrada):

"""
Daemon collector observabilidad email — entrypoint.
Ejecuta los cinco workers concurrentemente vía asyncio.
"""

import asyncio
import logging
import signal
from contextlib import asynccontextmanager

from fastapi import FastAPI

from collector.config import load_config
from collector.workers import (
    parsedmarc_worker,
    postmaster_worker,
    snds_worker,
    dkim_worker,
    arc_worker,
)

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s")
logger = logging.getLogger(__name__)

WORKERS = []


async def run_workers():
    """Inicia todos los workers como tasks concurrentes."""
    config = load_config()
    workers_to_run = [
        parsedmarc_worker.run(config),
        postmaster_worker.run(config),
        snds_worker.run(config),
        dkim_worker.run(config),
        arc_worker.run(config),
    ]
    WORKERS.extend(workers_to_run)
    await asyncio.gather(*workers_to_run, return_exceptions=True)


@asynccontextmanager
async def lifespan(app: FastAPI):
    logger.info("Starting email observability collector workers...")
    task = asyncio.create_task(run_workers())

    # Manejar shutdown gracioso
    def handle_signal(signum, frame):
        logger.info(f"Received signal {signum}, cancelling workers...")
        task.cancel()
    signal.signal(signal.SIGTERM, handle_signal)
    signal.signal(signal.SIGINT, handle_signal)

    yield

    if not task.done():
        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            pass


app = FastAPI(lifespan=lifespan, title="Email Observability Collector")


@app.get("/health")
async def health():
    return {"status": "healthy", "workers": len(WORKERS)}


@app.get("/metrics")
async def metrics():
    """Métricas Prometheus-style para meta-monitoreo."""
    return {
        "collector_workers_running": len(WORKERS),
        "collector_uptime_seconds": 0,  # implementar con start_time tracking
    }

collector/workers/postmaster_worker.py (worker más complejo, mostrado completo):

"""
Worker Google Postmaster Tools v2.
Pull diariamente vía OAuth2 service account.
"""

import logging
from datetime import datetime, timedelta, timezone

import aiohttp
from google.oauth2 import service_account
from google.auth.transport.requests import Request

from collector.es_client import get_es_client
from collector.normalizer import normalize_postmaster_record

logger = logging.getLogger(__name__)

POSTMASTER_API_BASE = "https://gmailpostmastertools.googleapis.com/v1beta1"
SCOPES = ["https://www.googleapis.com/auth/postmaster.readonly"]


async def get_oauth_token(credentials_path: str) -> str:
    """Obtener token de acceso OAuth2 desde service account JSON."""
    credentials = service_account.Credentials.from_service_account_file(
        credentials_path, scopes=SCOPES
    )
    credentials.refresh(Request())
    return credentials.token


async def fetch_traffic_stats(domain: str, token: str) -> dict:
    """Fetch trafficStats para un dominio para los últimos 7 días."""
    url = f"{POSTMASTER_API_BASE}/domains/{domain}/trafficStats"
    headers = {"Authorization": f"Bearer {token}"}

    end_date = datetime.now(timezone.utc).date()
    start_date = end_date - timedelta(days=7)

    params = {
        "startDate.year": start_date.year,
        "startDate.month": start_date.month,
        "startDate.day": start_date.day,
        "endDate.year": end_date.year,
        "endDate.month": end_date.month,
        "endDate.day": end_date.day,
    }

    async with aiohttp.ClientSession() as session:
        async with session.get(url, headers=headers, params=params) as resp:
            if resp.status == 200:
                return await resp.json()
            else:
                logger.error(
                    f"Postmaster API error for {domain}: "
                    f"HTTP {resp.status} - {await resp.text()}"
                )
                return {}


async def run(config: dict):
    """Worker principal: ciclo cada 24 horas, fetch para todos los dominios."""
    es_client = get_es_client(config)

    while True:
        try:
            token = await get_oauth_token(config["postmaster_credentials_path"])

            for domain in config["monitored_domains"]:
                traffic_data = await fetch_traffic_stats(domain, token)

                if not traffic_data or "trafficStats" not in traffic_data:
                    continue

                for stat in traffic_data["trafficStats"]:
                    record = normalize_postmaster_record(domain, stat)
                    await es_client.index(
                        index=f"email_observability-postmaster-{datetime.now().strftime('%Y.%m')}",
                        document=record
                    )

                logger.info(f"Indexed {len(traffic_data.get('trafficStats', []))} records for {domain}")

            await asyncio.sleep(24 * 3600)  # 24 horas

        except asyncio.CancelledError:
            logger.info("Postmaster worker cancelled")
            raise
        except Exception as e:
            logger.exception(f"Postmaster worker error: {e}")
            await asyncio.sleep(300)  # retry en 5 min en error

Los otros workers (parsedmarc, snds, dkim, arc) siguen el mismo patrón estructural pero con autenticación y parsing específicos del stream. El parsedmarc_worker hace client.search() en Elasticsearch del parsedmarc; el snds_worker hace HTTP scraping; los workers dkim/arc hacen tail -f en files de log con parsing regex.

Despliegue Docker Compose

El stack completo despliega como Docker Compose simple. Listo para runear en cualquier VPS Linux con Docker Engine + Compose plugin instalados.

docker-compose.yml:

version: '3.9'

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=true
      - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
      - "ES_JAVA_OPTS=-Xms2g -Xmx2g"
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
    healthcheck:
      test: ["CMD-SHELL", "curl -k -s -u elastic:${ELASTIC_PASSWORD} https://localhost:9200/_cluster/health | grep -q '\"status\":\"green\\|yellow\"'"]
      interval: 30s
      timeout: 10s
      retries: 5

  collector:
    build: ./collector
    depends_on:
      elasticsearch:
        condition: service_healthy
    environment:
      - ELASTICSEARCH_URL=https://elasticsearch:9200
      - ELASTIC_USER=elastic
      - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
      - POSTMASTER_CREDENTIALS_PATH=/secrets/postmaster.json
      - SNDS_AUTH_KEY=${SNDS_AUTH_KEY}
    volumes:
      - ./config:/config:ro
      - ./secrets:/secrets:ro
      - /var/log/maillog:/host/maillog:ro
    restart: always

  grafana:
    image: grafana/grafana:11.0.0
    depends_on:
      elasticsearch:
        condition: service_healthy
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
      - GF_INSTALL_PLUGINS=
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
    ports:
      - "3000:3000"
    restart: always

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    restart: always

  nginx:
    image: nginx:1.25-alpine
    depends_on:
      - grafana
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/certs:/etc/nginx/certs:ro
    ports:
      - "443:443"
      - "80:80"
    restart: always

volumes:
  elasticsearch_data:
  grafana_data:
  redis_data:

El stack corre cómodamente en un VPS 4 vCPU / 8 GB RAM para organizaciones con bajo 25 dominios remitentes. Despliegues más grandes (50+ dominios, 50M+ mensajes/mes) necesitan 8 vCPU / 16 GB y Elasticsearch tuneado con asignación de heap 4 GB.

El dashboard Grafana unificado

Después del despliegue del stack, el dashboard pre-construido provisiona vía grafana/provisioning/dashboards/email-observability.json. La estructura del dashboard:

Panel 1 — Salud top-line (single-stat):

  • Score de alerta agregado a través de todos los dominios monitoreados (0-10 escala)
  • Verde 0-2, amarillo 3-6, rojo 7-10
  • Refresca cada 5 minutos

Panel 2 — Estado autenticación por dominio remitente (heatmap):

  • Filas: dominios remitentes
  • Columnas: bucket horario últimas 24 horas
  • Color: tasa de alineación DMARC (verde 99-100%, amarillo 95-99%, rojo bajo 95%)
  • Drill-down click pivota al panel detallado

Panel 3 — Reputación dominio Postmaster (gráfico tiempo):

  • Multi-line por dominio
  • Eje Y: bucket reputación dominio (numerizado: bad=1, low=2, medium=3, high=4)
  • Eje X: tiempo, últimos 30 días

Panel 4 — Resultados filtro SNDS por IP (tabla):

  • IPs de envío con su filter result actual SNDS
  • Highlight cualquier IP en Yellow/Red

Panel 5 — Tasa éxito firmado DKIM (single-stat con sparkline):

  • Porcentaje de mensajes outbound DKIM-firmados exitosamente
  • Sparkline mostrando tendencia 24h

Panel 6 — Eventos override ARC (tabla):

  • Mensajes donde DMARC falló pero override ARC aplicó
  • De email_observability-parsedmarc con policy_override_reasons que contiene “arc=pass”

Panel 7 — Score correlación + razones (tabla):

  • Tope dominios por alerting_score
  • Razones expanded mostrando qué streams contribuyen

El dashboard load < 3 segundos para conjuntos de datos típicos (organización con 5-15 dominios, 30 días de data). El panel más caro es el heatmap de autenticación (Panel 2), que se beneficia de un campo correlation_window_id indexado para queries.

Los paneles de correlación son operacionalmente más valiosos. Ejemplo: un panel mostrando “dominios donde Postmaster reporta domain_reputation: low Y DMARC RUA muestra alineación declinante” surgió problemas que dashboards stream-único pierden durante 24-48 horas. Esta es la diferencia entre capturar una degradación entregabilidad en hora 4 vs hora 30 de un incidente multi-día.

Tiempo hasta detectar degradación entregabilidad por enfoque de monitoreo
Tiempo detección para eventos de degradación entregabilidad
Categoría Mediana horas hasta detección
Solo Postmaster 38
Solo SNDS 24
Solo parsedmarc 26
Dos streams correlacionados 12
Cinco streams unificados 4

Muestra de 47 eventos de degradación entregabilidad rastreados a través de engagements operacionales 2024-2026 donde la causa raíz y timing fueron retroactivamente confirmables. Monitoreo stream-único detecta la mayoría de eventos pero con lag significativo (24-38 horas mediana). Correlación de dos streams corta el tiempo de detección aproximadamente a la mitad capturando eventos donde streams divergen antes de que cualquiera cruce su propio threshold. Observabilidad unificada cinco streams reduce detección a 4 horas mediana porque el alert score compuesto cruza warning thresholds basado en movimientos pequeños a través de múltiples streams. El impacto operacional: detección 4 horas habilita remediación same-day; detección 30+ horas significa que un problema se ha compuesto a través de múltiples ciclos de envío antes de ser abordado.

La lógica de correlación cross-stream

La lógica de correlación es donde la observabilidad multi-stream gana valor real sobre dashboards individuales. La función central computa un alerting_score 0-10 por dominio basado en señales convergentes.

collector/correlator.py (lógica correlación):

"""
Lógica correlación cross-stream.
Computa un score compuesto de alerta por dominio.
"""

import logging
from datetime import datetime, timedelta, timezone

from collector.es_client import get_es_client

logger = logging.getLogger(__name__)


def compute_alerting_score(domain_records):
    """
    Computa score de alerta 0-10 basado en señales multi-stream.
    0 = saludable a través de todos los streams
    10 = degradación crítica a través de todos los streams
    """
    score = 0
    reasons = []

    # alineación DMARC parsedmarc cayendo
    pm_records = [r for r in domain_records if r["stream"] == "parsedmarc"]
    if pm_records:
        recent_alignment = sum(1 for r in pm_records if r.get("dmarc_aligned")) / len(pm_records)
        if recent_alignment < 0.95:
            score += 2
            reasons.append(f"alineación DMARC {recent_alignment:.2%}")
        if recent_alignment < 0.80:
            score += 2
            reasons.append("alineación DMARC crítica")

    # degradación reputación Postmaster
    pmt_records = [r for r in domain_records if r["stream"] == "postmaster_v2"]
    if pmt_records:
        latest = max(pmt_records, key=lambda r: r["@timestamp"])
        if latest.get("domain_reputation") in ["low", "bad"]:
            score += 3
            reasons.append(f"reputación Postmaster: {latest['domain_reputation']}")
        if latest.get("spam_rate", 0) > 0.003:
            score += 2
            reasons.append(f"tasa spam Postmaster {latest['spam_rate']:.4f}")

    # degradación filtro SNDS
    snds_records = [r for r in domain_records if r["stream"] == "snds"]
    if snds_records:
        red_count = sum(1 for r in snds_records if r.get("snds_filter_result") == "Red")
        if red_count > 0:
            score += 3
            reasons.append(f"SNDS Rojo: {red_count} IPs")

    # fallas firmado DKIM
    dkim_records = [r for r in domain_records if r["stream"] == "dkim"]
    if dkim_records:
        failed = sum(1 for r in dkim_records if not r.get("sign_success"))
        total = len(dkim_records)
        if total > 0 and (failed / total) > 0.001:
            score += 2
            reasons.append(f"fallas firmado DKIM {failed/total:.4f}")

    return min(score, 10), reasons


async def run_correlation(config):
    """
    Ciclo continuo de correlación: cada hora, computa scores para
    todos los dominios monitoreados, escribe resultados a índice de correlación.
    """
    es_client = get_es_client(config)

    while True:
        for domain in config["monitored_domains"]:
            # Fetch records últimas 24h de todos los streams para este dominio
            query = {
                "query": {
                    "bool": {
                        "must": [
                            {"term": {"sender_domain": domain}},
                            {"range": {"@timestamp": {"gte": "now-24h"}}}
                        ]
                    }
                },
                "size": 10000
            }
            result = await es_client.search(
                index="email_observability-*",
                body=query
            )
            domain_records = [hit["_source"] for hit in result["hits"]["hits"]]

            score, reasons = compute_alerting_score(domain_records)

            correlation_record = {
                "@timestamp": datetime.now(timezone.utc).isoformat(),
                "sender_domain": domain,
                "alerting_score": score,
                "reasons": reasons,
                "input_records_count": len(domain_records),
            }

            await es_client.index(
                index="email_observability-correlation",
                document=correlation_record
            )

            if score >= 5:
                logger.warning(
                    f"Domain {domain} alerting score {score}: {', '.join(reasons)}"
                )

La lógica de correlación arriba es intencionalmente simple — scoring aditivo con thresholds predefinidos. Approaches más sofisticados (machine learning anomaly detection, time-series forecasting) son posibles pero requieren madurez operacional que la mayoría de organizaciones no tienen para los primeros 6-12 meses de vida de una plataforma de observabilidad. Empezar simple; iterar basado en incidentes reales.

Los cinco patrones de correlación más operacionalmente valiosos en producción:

Firma del patrónStreams involucradosIndicador líderIndicadores confirmantes (en orden)MTTD típico multi-streamMTTD típico stream-único
Slide lento de reputaciónDMARC RUA + Postmaster + logs DKIMUptick de desalineación DMARC (1-3%)Caída tier reputación Postmaster (24-48h después); logs firmado DKIM sin cambios6-12 horas36-72 horas
Degradación bloqueo IPSNDS + parsedmarc + PostmasterResultado filtro SNDS Amarillo/Rojo en subset de IPsparsedmarc muestra fallas DMARC concentradas en esas IPs (12-24h); Postmaster muestra caída reputación más amplia (48-72h)4-8 horas48-96 horas
Falla rotación DKIMlogs DKIM + DMARC RUA + ARCLogs locales firmado muestran rotación exitosa pero selector sin cambiosDMARC RUA muestra uptick DKIM=fail en receptores (24-48h)12-24 horas72-120 horas
Cadena ARC no honradaeventos ARC + DMARC RUASeal ARC añadido localmente con cv=passDMARC RUA muestra ningún policy_override_reasons referenciando tu authservid (48-72h)24-48 horasA menudo nunca detectado
Breach umbral bulk senderPostmaster + SNDSTasa spam Postmaster >0.3% sostenido 48hResultado filtro SNDS Amarillo/Rojo; DMARC RUA muestra uptick disposición quarantine8-16 horas96-168 horas

El MTTD 5-10x más rápido en la columna multi-stream impulsa el valor operacional de observabilidad unificada. El monitoreo stream-único es técnicamente funcional pero operacionalmente limitado — los incidentes se componen durante el lag de detección, y el análisis de causa raíz requiere correlacionar manualmente datos de múltiples herramientas después de los hechos.

Caso agencia LATAM: 12 dominios cliente, $89K/año SaaS vs ROI autohospedado

Una agencia mexicana de email marketing con la que trabajamos en Q1 2026 estaba gestionando entregabilidad para 12 clientes enterprise a través de servicios financieros, e-commerce, y SaaS. Estaban considerando suscripción a Postmastery Console ($340/mes por dominio cliente a su tarifa negociada, descuento 10% por 12 dominios), totalizando aproximadamente $89K/año en costos SaaS.

Estado pre-plataforma (enero 2026):

  • 12 dominios cliente, volumen total ~14M mensajes/mes
  • Monitoreo fragmentado: parsedmarc desplegado (de nuestro trabajo turno 17), pero sin integración API Postmaster Tools, sin agregación SNDS, chequeo manual de cada herramienta semanalmente
  • Mean time to detect incidentes de entregabilidad: 36-72 horas (patrón típico: cliente reporta caída en conversión → 24h investigación → identificación de causa)
  • Revenue perdido por incidente: $3K-12K dependiendo de severidad, 4-6 incidentes por trimestre
  • Costo anual de detección lenta: ~$80K en revenue cliente perdido y costo reputacional

Evaluación build vs SaaS:

  • Postmastery Console SaaS: $89K/año, listo en días, incluye horas de consulting deliverability gestionadas
  • Build observabilidad unificada autohospedada: $4K ingeniería Python inicial + $200/mes VPS + infraestructura Elasticsearch ($2.400/año) = $6.400 primer año, $2.400 continuo
  • Factores de decisión: la agencia tenía capacidad Python in-house, valoraba soberanía de datos para confidencialidad cliente, quería habilidad de añadir integraciones custom (sus propios logs MTA, señales internas CRM)

Plan de migración ejecutado:

  • Semana 1: desplegar stack Docker Compose en VPS dedicado, ingestar parsedmarc en Elasticsearch unificado
  • Semana 2: implementar collector API Postmaster v2 con OAuth2 service account, configurar para los 12 dominios cliente
  • Semana 3: implementar scraper XML SNDS para rangos IP cliente, añadir tailing logs DKIM/ARC para clientes self-managed
  • Semana 4: build lógica correlación + dashboards Grafana, desplegar alertas vía integración PagerDuty

Estado post-plataforma (abril 2026, 90 días post-deploy):

  • Mean time to detect incidentes de entregabilidad: 4-8 horas (mejora 5-10x)
  • Incidentes prevenidos de componerse: 3 de los 5 incidentes del trimestre pasado capturados antes que el cliente notara
  • Revenue cliente estimado salvado: ~$22K en Q1 2026
  • Mejoras operacionales: la agencia añadió 3 nuevos clientes sin crecimiento proporcional de overhead de monitoreo (collector maneja nuevos dominios en menos de 10 minutos de cambios de config)
  • ROI: período de payback para inversión inicial $6.400 fue aproximadamente 4 semanas basado en primer incidente prevenido

Consideraciones operacionales LATAM particulares observadas:

  • Pricing SaaS denominado en USD ($89K/año) traduce a equivalente revenue peso que es estructuralmente desafiante para márgenes de agencia; autohospedado cambia costo de USD opex a peso engineering capex
  • Cumplimiento: 2 de 12 clientes requerían residencia de datos para auditoría PCI DSS v4.0; agregación SaaS US-hosted flagged en auditoría, autohospedado en infra regional resolvió
  • Confidencialidad cliente: agregar datos de entregabilidad cliente a un SaaS US surgió preocupaciones de confidencialidad cliente que no aplicaban a infraestructura autohospedada
  • La apertura del framework collector Python a integración custom significó que la agencia podía añadir su señal CRM interna “client revenue impact” como un sexto stream, correlacionando eventos de entregabilidad a outcomes de revenue

El patrón operacional general LATAM: los modelos de pricing SaaS de monitoreo de entregabilidad construidos para mercados US/EU se vuelven estructuralmente insostenibles para agencias LATAM una vez que el conteo de clientes cruza 8-12 dominios, mientras que la capacidad de ingeniería está localmente disponible al 30-50% de tarifas US. El framework de observabilidad unificada autohospedada es operacionalmente un mejor fit que SaaS comercial para LATAM a esta escala.

Estrategia alertas multi-stream

La estrategia de alertas que desplegamos en producción tiene tres tiers, calibrada para reducir fatiga de alertas:

Tier 1 — Informacional (canal Slack solamente):

  • Stream único muestra degradación menor (ej., alineación DMARC 95-98%, reputación Postmaster baja para un dominio)
  • Disparado por alerting_score 2-4
  • Sin wake on-call; chequear durante horas hábiles

Tier 2 — Warning (Slack + email a equipo entregabilidad):

  • Dos streams concuerdan en degradación (ej., alineación DMARC bajo 95% Y reputación Postmaster: baja)
  • Disparado por alerting_score 5-7
  • Acción esperada dentro de 4 horas hábiles

Tier 3 — Crítico (PagerDuty/on-call):

  • Tres o más streams degradados simultáneamente, O cualquier señal crítica única (Postmaster domain_reputation: bad, SNDS Red en múltiples IPs, alineación DMARC bajo 80%)
  • Disparado por alerting_score 8-10
  • Wake inmediato on-call

Reglas de alertas Prometheus correspondientes:

groups:
- name: email_observability
  rules:
  - alert: DeliverabilityCritical
    expr: max_over_time(deliverability_score{}[1h]) >= 8
    for: 10m
    labels:
      severity: critical
      pagerduty: yes
    annotations:
      summary: "Entregabilidad crítica para {{ $labels.sender_domain }}"
      description: "Score {{ $value }} indica degradación 3+ streams"

  - alert: DeliverabilityWarning
    expr: max_over_time(deliverability_score{}[1h]) >= 5
    for: 30m
    labels:
      severity: warning
    annotations:
      summary: "Entregabilidad degradando para {{ $labels.sender_domain }}"

  - alert: DeliverabilityInfo
    expr: max_over_time(deliverability_score{}[1h]) >= 2
    for: 1h
    labels:
      severity: info
    annotations:
      summary: "Entregabilidad señal menor para {{ $labels.sender_domain }}"

KPIs operacionales para la propia plataforma de observabilidad

Cinco métricas que vale la pena trackear sobre la salud de la plataforma (no solo los datos de entregabilidad que monitorea):

KPI 1 — Frescura de datos del collector: objetivo ≤30 minutos para parsedmarc y log streams; ≤24 horas para Postmaster/SNDS (sus cadencias nativas). Por encima del objetivo indica backlog del collector.

KPI 2 — Tasa falsos positivos alertas: objetivo ≤15% para Tier 2 warnings, ≤5% para Tier 3 críticos. Por encima del objetivo indica que los thresholds de correlación necesitan tuning.

KPI 3 — Mean time to detection (MTTD) de incidentes entregabilidad: objetivo ≤6 horas desde inicio de evento hasta alerta disparada. Trackear vía revisión post-incidente.

KPI 4 — Salud índice Elasticsearch: objetivo green status, crecimiento conteo documentos ~5-10% semana sobre semana (steady state) o crecimiento correlacionado con cambios de volumen de envío.

KPI 5 — Latencia query dashboard P95: objetivo ≤3 segundos para load de dashboard unificado. Por encima del objetivo indica que Elasticsearch necesita tuning u optimización de schema.

Estos KPIs de plataforma están ellos mismos visualizados en un meta-dashboard — observabilidad para la plataforma de observabilidad. Lo añadimos después de la segunda vez que el collector silenciosamente dejó de pull data Postmaster por credenciales OAuth2 expiradas y no notamos durante 11 días.

Lo que recomendamos en Blue Spirit

Por transparencia: corremos una versión de esta plataforma internamente para nuestra propia infraestructura de hosting PowerMTA y operaciones de entregabilidad. Nuestro engagement de auditoría de entregabilidad incluye o desplegar la plataforma para clientes o auditar plataformas existentes.

El framework de recomendación para 2026 en forma matriz:

Perfil remitenteVolumen mensualDominios remitentesBuild vs BuyCosto anual (build)Costo anual (SaaS equivalente)
Hobby / micro<100K1Solo parsedmarc$0 (DIY)$0-50/mes
B2B SaaS pequeño<1M1-3No build unificado~$2-3K infra + parsedmarc$2.4-7.2K SaaS
Mid-market1-10M3-10Build si Python in-house$4-6K inicial + $2-3K/año$7.2-24K SaaS
Enterprise grande10-50M10-25Build (autohospedado favorito)$6-10K inicial + $3-5K/año$24-60K SaaS
ESP / agencia50M+25+Build (autohospedado solo viable)$10-15K inicial + $5-8K/año$50-150K+ SaaS
Agencia LATAM (5-15 clientes)varía5-15Build (estructuralmente favorito)~$6.4K (labor local más bajo)$20-50K USD-denominado

Los bullets explicando la matriz:

Para organizaciones enviando bajo 1M mensajes/mes desde 1-3 dominios remitentes: no construir esto. Dashboards parsedmarc plus chequeo manual semanal de UI Postmaster Tools es suficiente. El costo de build ($4-6K) no se paga a esta escala.

Para organizaciones enviando 1M-10M/mes desde 3-10 dominios remitentes: build justificado si tienes capacidad Python in-house. Payback esperado: 6-12 meses basado en prevención de incidentes. Alternativa SaaS (Postmastery Console a ~$200-400/mes por dominio) compite en valor managed-service, no costo.

Para organizaciones enviando 10M+/mes desde 10+ dominios remitentes: autohospedado estructuralmente favorito a menos que tu modelo operacional fuertemente prefiera servicios gestionados. La plataforma unificada se paga en semanas basado solo en prevención de incidentes.

Para ESPs y agencias operando 25+ dominios cliente remitentes: autohospedado es el único path económicamente viable. El pricing SaaS escala linealmente per dominio y rápidamente excede $50K/año; autohospedado escala sub-linealmente.

Para organizaciones LATAM específicamente: el caso estructural para autohospedado es más fuerte que US/EU debido a pricing SaaS denominado en USD compitiendo desfavorablemente vs costos engineering local. El framework de observabilidad unificada es una inversión de alto leverage para agencias LATAM operando 8-15+ dominios cliente.

Si necesitas ayuda desplegando esta plataforma, integrando fuentes de datos adicionales (tus propios logs MTA, sistemas CRM, métricas de negocio), o evaluando monitoreo existente vs el framework de observabilidad unificada — eso es parte de nuestro engagement de auditoría de entregabilidad. El source code del framework se publica como parte de los entregables de auditoría e incluye el JSON del dashboard Grafana, collector Python con los cinco stream pullers, despliegue Docker Compose, y runbooks operacionales.

Resumen honesto

Las operaciones de entregabilidad email en 2026 requieren observabilidad multi-stream. Los cinco streams (DMARC RUA, Postmaster v2, SNDS, logs DKIM, eventos ARC) cada uno responde preguntas operacionales diferentes, y la vista unificada provee detección de incidentes 5-10x más rápida comparado con monitoreo stream-único. El valor operacional no es teórico — incidentes que se componen sobre 30+ horas cuando se monitorean stream-único típicamente resuelven en 4-8 horas cuando se monitorean unificados.

La pregunta build-vs-buy depende de escala y capacidad. Remitentes pequeños (bajo 1M/mes, 1-3 dominios) no deberían construir; la complejidad operacional excede el valor. Remitentes mid-scale (1-10M/mes, 3-10 dominios) con capacidad Python in-house deberían construir; payback en 6-12 meses. Remitentes grandes y ESPs (10M+/mes, 10+ dominios) deberían construir porque los modelos de pricing SaaS se vuelven económicamente prohibitivos a escala.

El framework en este post — collector Python con cinco stream pullers, schema unificado Elasticsearch, dashboards Grafana con correlación cross-stream, alertas multi-tier — es lo que desplegamos en producción para clientes gestionando 12-50 dominios remitentes. El costo de despliegue es aproximadamente $4-6K ingeniería Python inicial plus $2-3K/año infraestructura continua. Comparado con alternativas SaaS (Postmastery, EmailConsul, Mailgun Optimize, Validity Everest) comenzando en $200-400/mes por dominio, autohospedado es económicamente favorable más allá de 5-8 dominios remitentes.

Para organizaciones LATAM específicamente, el caso estructural es más fuerte porque el pricing SaaS USD-denominado escala linealmente per dominio mientras que la capacidad ingeniería local escala al 30-50% de tarifas US. La plataforma de observabilidad unificada es una inversión de alto leverage para agencias LATAM operando 8-15+ dominios cliente. El caso de la agencia cuantificado arriba ($6.400 build cost vs $89K/año SaaS, payback de 4 semanas basado en primer incidente prevenido) es representativo de la economía a esta escala.

El framework técnico está resuelto. La disciplina operacional importa más — confiabilidad del collector, tuning de thresholds de alerta, disciplina de layout del dashboard, revisión post-incidente alimentando feedback a la lógica de correlación. La mayoría de plataformas de observabilidad que auditamos o no tienen esta disciplina (collector corriendo pero nunca revisado, alertas ignoradas como ruido, dashboards construidos una vez y nunca iterados) o la tienen de manera dispar. El framework importa; la disciplina importa más.

Femke van der Berg

Senior Deliverability Engineer · Deliverability & Authentication

¿Algo que deberíamos escribir? Manda tu tema a [email protected].

Escríbenos por WhatsApp