Skip to content

Alerthub es un servicio escrito en Go que recibe los webhooks de alerta generados por Grafana Alerting, los normaliza y los almacena en una base de datos PostgreSQL.

Notifications You must be signed in to change notification settings

craftech-io/alerthub

Repository files navigation

Alerthub Logo

Alerthub

Alerthub es un servicio escrito en Go que recibe los webhooks de alerta generados por Grafana Alerting, los normaliza y los almacena en una base de datos PostgreSQL.

Su objetivo es convertirse en el hub central de alertas para tu equipo, permitiéndoles consultar, analizar y reportar el estado de las alertas desde:

  • un archivo Excel para reportería,
  • o dashboards interactivos en Grafana.

Cada alerta se procesa bajo un modelo unificado, se le asigna un fingerprint consistente, y se registra junto con su estado (abierta o cerrada), permitiendo trazabilidad completa del ciclo de vida de la alerta.

Arquitectura

Alerthub se ubica entre Grafana Alerting y PostgreSQL para convertir eventos de alerta en un historial consultable. Grafana envía eventos (firing / resolved) mediante un contact point (tipo Alertmanager) hacia Alerthub; el servicio recibe el payload, lo normaliza, genera un fingerprint y persiste el ciclo de vida en PostgreSQL. Luego esa información se consume desde dashboards de Grafana o exportaciones para reportería.

Captura de la arquitectura

🧭 Navegación

🔍 Alerthub UI

Una pequeña interfaz para visualizar las alertas recibidas directamente desde el navegador.

Captura del front


📊 Alerthub + Excel

Exportá los datos desde la base de datos para generar reportería o consolidar métricas en Excel.

Captura del dashboard Tables Inspector


📈 Alerthub + Grafana Dashboard

Un dashboard en Grafana que muestra todo el historial de alertas almacenado en la base de datos de Alerthub, sin aplicar ningún tipo de filtro.

Ideal para:

  • Revisar el estado completo de todas las alertas históricas.
  • Auditar eventos sin importar el namespace, etiquetas o fuente.
  • Construir una vista centralizada de incidentes.

Captura del dashboard Full Alert History


Indice


Características principales

  • Ingesta compatible con Alertmanager: expone /api/v1/alerts y /api/v2/alerts, aceptando tanto arreglos planos como la envoltura estándar (externalURL, alerts).
  • Autenticación opcional: admite Basic Auth y tokens Bearer configurables por variables de entorno.
  • Normalización y enriquecimiento: genera un fingerprint determinístico, completa etiquetas faltantes (por ejemplo cluster o deployment) y clasifica alertas de "no data" para facilitar los filtros.
  • Persistencia en PostgreSQL: almacena cada evento en la tabla alert_events y mantiene el estado actual y los episodios en alert_state y alert_episodes.
  • Healthchecks simples: incluye / con un estado HTML básico y /healthz para comprobaciones automatizadas.

Estructura del proyecto

  • cmd/alerthub: punto de entrada del binario, configura el servidor HTTP.
  • internal/config: carga de variables de entorno.
  • internal/httpapi: manejo de rutas y procesamiento del payload de alertas.
  • internal/ingest: lógica de normalización y generación de fingerprints.
  • internal/store: acceso a la base de datos y operaciones de estado.
  • pkg/models: estructuras compartidas entre los paquetes.
  • migrations/: scripts SQL para crear las tablas necesarias.
  • helm/chart-alerthub/: chart de Helm oficial que empaqueta la aplicación, la base de datos Postgres y los recursos auxiliares.

Requisitos

  • Go 1.18 o superior.
  • PostgreSQL 13+ con la extensión pgcrypto habilitada (usada para gen_random_uuid()).
  • Variables de entorno definidas (ver sección siguiente).

Ejecución local

  1. Exporta las variables de entorno necesarias, en particular DATABASE_URL apuntando a tu instancia de PostgreSQL. | Variable | Descripción | Valor por defecto | | --- | --- | --- | | PORT | Puerto HTTP expuesto por el servicio. | 8080 | | DATABASE_URL | Cadena de conexión a PostgreSQL (obligatoria). | sin valor | | SOURCE | Nombre de la fuente usado para etiquetar los eventos. | alertmanager | | AUTH_BEARER | Token Bearer aceptado para ingesta (opcional). | "" | | AUTH_BASIC_USER | Usuario para autenticación Basic (opcional). | "" | | AUTH_BASIC_PASS | Contraseña asociada al usuario Basic. | "" | | CLUSTER_NAME | Valor por defecto para la etiqueta cluster cuando no llega en el payload. | "" |

Si se definen simultáneamente autenticación Basic y Bearer, la solicitud se acepta cuando cumple con cualquiera de los dos mecanism

  1. Aplica las migraciones descritas arriba o reutiliza los scripts de deploy/local/db-init si deseas recrear el esquema completo. Los scripts de migrations/ crean las tablas alert_events, alert_state y alert_episodes junto con los índices necesarios. Para una instalación local rápida puedes ejecutar:
createdb alerthub
psql "$DATABASE_URL" -f migrations/001_init.sql
psql "$DATABASE_URL" -f migrations/020_state.sql
psql "$DATABASE_URL" -f migrations/030_nodepool.sql
  1. Ejecuta el servidor:

    go run ./cmd/alerthub

El servicio quedará escuchando en http://localhost:8080 (o el puerto configurado) y aceptará peticiones en los endpoints /api/v1/alerts y /api/v2/alerts. No es necesario agregar esos endopint solo la URL base ya que Grafana agrega esos path automaticamente.

Pruebas

Los tests unitarios cubren la normalización de alertas y la persistencia de categorías. Para ejecutarlos:

go test ./...

Panel de administración

En raras ocasiones Grafana no logra notificar el evento Resolved lo que genera un inconveniente en Alerthub ya que seguirá mostrando la alerta como "firing" aunque en realidad ya haya finalizado. Para estos casos existe el endpoint /admin, que muestra todas las alertas activas y permite marcarlas como resueltas manualmente. Ademas al marcarla como resuelta se agrega a la alerta en el annotation "manual_resolution": "eliminacion manual" para poder tener un seguimiento de cuales alertas fueron resueltas manualmente Para cerrar una alerta :

  1. Abrí http://<host>:<port>/admin y localizá el fingerprint de la alerta que quedó abierta.
  2. Presioná Resolve para que Alerthub genere el evento de cierre,agregue el annotation y actualice el estado en la base.

Este flujo desbloquea los tableros cuando el hook de Resolved no llega (un comportamiento común en algunos canales de Grafana) y evita que las métricas sigan reportando un incidente activo.

Despliegue con el chart de Helm

El método soportado para orquestar Alerthub en Kubernetes es el chart helm/chart-alerthub/. Necesitas definir, al menos:

  • global.image.repository y global.image.tag con la imagen de contenedor que quieres desplegar.
  • Un secreto referenciado en deployment.envFrom que exponga DATABASE_URL y las credenciales necesarias. Alternativamente, activa credentials.autoGenerate.enabled para que el chart cree y propague automáticamente las credenciales de PostgreSQL y el servicio HTTP básico.
  • (Opcional) postgres.secretName cuando utilices la base de datos embebida gestionada por el chart.

Puedes tomar helm/example/values.yaml como base y adaptarlo a tu entorno. Una vez que tengas los valores listos, instala el chart con:

helm install alerthub ./helm/chart-alerthub \
  --namespace alerthub-demo \
  --create-namespace \
  -f my-values.yaml

Sustituye my-values.yaml por tu archivo de configuración (puedes comenzar copiando helm/example/values.yaml) y ajusta el namespace según corresponda. Si necesitas ejecutar migraciones durante el despliegue, puedes apoyarte en initSql (habilitado por defecto) o aplicar manualmente los scripts de migrations/ antes de iniciar el servicio.

Bootstrap de base de datos con el chart

El ConfigMap pg-init-sql incluido en el chart monta los archivos 000_extensions.sql, 010_events.sql y 020_events.sql en /docker-entrypoint-initdb.d, y un Job auxiliar los ejecuta automáticamente contra la base declarada (ya sea la desplegada por el propio chart o una instancia externa como RDS). Esta lógica está activa por defecto (initSql.enabled: true), por lo que basta con desplegar el chart para que la base quede inicializada.

Si en tu organización gestionas las migraciones por fuera del chart (por ejemplo con una herramienta dedicada), puedes desactivar el ConfigMap definiendo initSql.enabled: false. Cuando necesites añadir scripts adicionales, sobreescribe el mapa initSql.files en tus valores, asegurándote de listar las claves correspondientes en initSql.items cuando agregues nuevos archivos. Si ya cuentas con un Secret que expone DATABASE_URL, referencia su nombre mediante initSql.databaseUrlSecret para que el Job reutilice esas credenciales al momento de aplicar los scripts.

Configuración de Grafana

Para integrar Grafana con Alerthub necesitas preparar tanto el canal de salida de alertas como la fuente de datos para los dashboards:

  1. Punto de contacto (Contact point) de tipo Alertmanager.

    • Crea un contact point de tipo Alertmanager y apunta la URL al Ingress que expone Alerthub. El README menciona los endpoints /api/v1/alerts y /api/v2/alerts porque son los paths que el servicio realmente expone; al configurar el contact point basta con indicar la URL base (por ejemplo https://alerthub.mi-dominio.com) ya que Grafana agrega automáticamente la ruta de ingesta (/api/v1/alerts por defecto) al enviar el webhook.

    • Si utilizas las credenciales generadas por el chart de Helm, recupéralas desde el secreto <release>-generated-credentials (valor por defecto) con:

      kubectl get secret <nombre-del-secret> -n <namespace> \
        -o jsonpath='{.data.AUTH_BASIC_USER}' | base64 -d
      kubectl get secret <nombre-del-secret> -n <namespace> \
        -o jsonpath='{.data.AUTH_BASIC_PASS}' | base64 -d
    • Configura el contact point en Grafana usando esos valores como usuario/contraseña de la autenticación Basic.

  2. Datasource PostgreSQL para consultas y dashboards.

    • Añade una nueva fuente de datos de tipo PostgreSQL apuntando al Service que expone la base desplegada por el chart (ClusterIP o LoadBalancer).
    • Las credenciales y la base por defecto también se encuentran en el mismo secreto (POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB).
    • Establece la opción de SSL según la configuración de tu clúster (el despliegue de ejemplo deshabilita TLS porque la comunicación ocurre dentro del clúster).

Con estos pasos Grafana podrá enviar alertas a Alerthub y consultar el histórico almacenado para alimentar los tableros incluidos en el repositorio.

Ejemplo de funcionamiento

  1. Payload recibido en /api/v1/alerts

    [
      {
        "status": "",
        "labels": {
          "alertname": "HighErrorRate",
          "namespace": "payments",
          "pod": "payments-api-7c9d7f4c8-abc12",
          "severity": "critical",
          "__alert_rule_uid__": "grafana-rule-42"
        },
        "annotations": {
          "summary": "Error rate above threshold",
          "description": "5xx ratio exceeded"
        },
        "startsAt": "2024-04-20T12:34:56Z",
        "endsAt": "0001-01-01T00:00:00Z",
        "generatorURL": "https://grafana.example.com/alerting/grafana/grafana-rule-42/view"
      }
    ]
  2. Normalización en internal/httpapi/routes.go: se calcula un status a partir de startsAt/endsAt, se robustecen rule_uid (leyendo __alert_rule_uid__, uid o la URL del panel) y rule_name, y se completa la etiqueta cluster cuando llega vacía usando la cabecera CLUSTER_NAME o el label node. 【F:internal/httpapi/routes.go†L85-L147】

  3. Enriquecimiento en internal/ingest/normalizer.go: el normalizador aplica reglas adicionales, como derivar deployment desde el nombre del pod (payments-api-7c9d7f4c8-abc12payments-api), calcular el fingerprint y decidir la category. 【F:internal/ingest/normalizer.go†L21-L53】

  4. Persistencia con internal/store/events.go: la estructura normalizada se almacena en alert_events, guardando todo el mapa de labels y anotaciones como JSONB. 【F:internal/store/events.go†L11-L35】

  5. Consultas posteriores: las columnas generadas definidas en deploy/local/db-init/010_events.sql exponen cluster, namespace, pod, nodepool, deployment, queue, vhost y severity como campos directos para filtros frecuentes. El resto de etiquetas siguen disponibles en alert_events.labels, consultables con labels->>'clave' o filtros GIN (labels @> '{"mi_label":"valor"}'). 【F:deploy/local/db-init/010_events.sql†L1-L58】

Referencia de labels y filtros para dashboards

Esta guía resume las columnas disponibles en alert_events tras el proceso de normalización de Alerthub.

Tabla de labels y filtros disponibles

Campo / filtro Descripción Fuente
cluster Columna generada que proyecta labels->>'cluster'; se rellena automáticamente con valores de cabeceras HTTP si la alerta no lo envía. Permite segmentar eventos por clúster. JSON de labels + normalizador
namespace Exposición directa de labels->>'namespace' para filtrar alertas por espacio de nombres. JSON de labels
pod Copia directa del labels->>'pod', útil para diagnósticos puntuales. JSON de labels
nodepool Columna derivada de labels->>'nodepool' con índice dedicado para dashboards de infraestructura. JSON de labels
deployment Proyecta labels->>'deployment'; cuando falta, se infiere a partir del nombre del pod (Deployment/StatefulSet). JSON de labels + normalizador
queue Expone labels->>'queue' para agrupar incidentes en colas (RabbitMQ, Sidekiq, etc.). JSON de labels
vhost Copia labels->>'vhost', complementando las consultas sobre colas. JSON de labels
severity Mapea labels->>'severity' para clasificar alertas por severidad declarada. JSON de labels
status Estado normalizado del evento (firing o resolved) controlado por un CHECK. Columna nativa
category Clasificación automática (queue, nodepool, pod, other) o nodata si se detecta un patrón de “sin datos”. Normalizador / generador de fingerprint
labels (JSONB) Contiene el resto de pares clave/valor. Se accede mediante operadores JSON (labels->>'mi_label', labels @> ...). Payload crudo de Alertmanager

⚠️ Advertencia sobre etiquetas opcionales

Alerthub solamente persiste las etiquetas (labels) exactamente como llegan en el webhook de Grafana o Alertmanager. No genera automáticamente campos como pod, namespace, cluster u otros si la alerta no los envía, con la única excepción de cluster, que puede rellenarse usando la variable de entorno CLUSTER_NAME o reutilizando el valor de node si está presente en el payload. Asimismo, durante la normalización solo se deriva deployment a partir del pod; el resto de etiquetas se respetan tal cual fueron recibidas. Si una alerta llega sin ciertos labels, Alerthub no podrá exponerlos en sus consultas porque nunca fueron provistos por el origen.

Ejemplos de configuración de alertas en Grafana

Alerthub solo puede indexar y exponer en dashboards los labels que existen en la alerta de Grafana (con la única excepción de cluster, que puede completarse vía CLUSTER_NAME si no llega). Por eso, al definir una alerta, se debe incluir explícitamente en labels los campos que después querés usar para filtrar, agrupar y auditar.

Ejemplo recomendado (labels completos y útiles para dashboards)

Este ejemplo incluye los labels típicos de operación en Kubernetes para que luego puedas filtrar por cluster, namespace, pod y severity desde Grafana/SQL:

apiVersion: 1
groups:
  - name: k8s-workloads
    rules:
      - uid: cpu_pod_prod
        title: "CPU alta en pod"
        condition: A
        labels:
          cluster: prod
          namespace: billing
          pod: billing-api-7f6d9c8bf7-xmzqh
          severity: critical
        annotations:
          summary: "Uso de CPU sobre el umbral"

Resultado: el evento queda almacenado con esas claves y Alerthub puede proyectarlas como columnas y variables de dashboard.

Ejemplo incompleto (labels mínimos: se pierde capacidad de filtro)

Si omitís labels importantes, Grafana no los enviará en el webhook y Alerthub no podrá materializarlos ni usarlos para filtros posteriores:

apiVersion: 1
groups:
  - name: k8s-workloads
    rules:
      - uid: cpu_pod_prod
        title: "CPU alta en pod"
        condition: A
        labels:
          severity: critical
        annotations:
          summary: "Uso de CPU sobre el umbral"

El segundo ejemplo seguirá ingresando a Alerthub, pero al faltar cluster, namespace o pod no será posible filtrarlo por esos campos en los dashboard. Verifica tus reglas de Grafana para garantizar que cada alerta envíe las etiquetas necesarias.

Gestión de labels que aún no existen como columnas

Alerthub almacena todos los labels en alert_events.labels (JSONB). Por defecto, la recomendación es consultar el JSONB directamente y evitar crear nuevas columnas, ya que agregar columnas implica cambios de esquema/migraciones y mayor costo operativo (especialmente en entornos productivos o bases gestionadas como RDS).

Cuando aparece un label nuevo que querés usar en dashboards o consultas, tenés dos opciones:

1) Consultar labels (JSONB) directamente (recomendado)

Usá expresiones como:

  • labels->>'mi_label'
  • labels ? 'mi_label' (verificar existencia)
  • labels @> '{"mi_label":"valor"}'::jsonb (match exacto)

Ventajas:

  • No requiere modificar la base (sin migraciones).
  • Es ideal para labels experimentales, poco frecuentes o específicos de algunos equipos/servicios.
  • Permite iterar rápido sobre dashboards sin tocar el schema.

Consideraciones:

  • Puede ser menos eficiente para filtros muy usados, porque la consulta opera sobre JSONB.
  • Podés mitigar esto con un índice GIN sobre labels (si aplica a tu caso) para acelerar búsquedas @>.
Ejemplos usando labels (JSONB)
  1. Segmentar por etiquetas no materializadas y exigir su presencia

    SELECT rule_name,
           labels->>'environment' AS environment,
           labels->>'team'        AS owning_team,
           count(*)               AS total_eventos
    FROM alert_events
    WHERE status = 'firing'
      AND labels->>'environment' IN ('prod', 'staging')
      AND labels ? 'team'
    GROUP BY rule_name, environment, owning_team
    ORDER BY total_eventos DESC;

    Devuelve un ranking de reglas activas, mostrando el entorno (environment) y el equipo (team) obtenidos directamente del JSON. La cláusula labels ? 'team' garantiza que solo se incluyan filas con esa etiqueta.

  2. Buscar coincidencias exactas dentro del JSON

    SELECT event_id,
           rule_name,
           starts_at,
           labels->>'service' AS service,
           labels
    FROM alert_events
    WHERE labels @> '{"service":"payments","tier":"critical"}'::jsonb
    ORDER BY starts_at DESC
    LIMIT 20;

    Obtiene los 20 eventos más recientes cuya carga de etiquetas contiene exactamente service=payments y tier=critical, devolviendo tanto columnas derivadas como el JSON completo para análisis rápido.

Ejemplos usando category
  1. Estado actual por categoría nodepool

    SELECT nodepool,
           count(*) AS firing_events,
           min(starts_at) AS oldest_open_event
    FROM alert_events
    WHERE status = 'firing'
      AND category = 'nodepool'
    GROUP BY nodepool
    ORDER BY firing_events DESC;

    Resume cuántas alertas de categoría nodepool están abiertas por pool y desde cuándo existe la más antigua.

  2. Detectar episodios marcados como nodata

    SELECT event_id,
           rule_name,
           starts_at,
           ends_at,
           labels->>'datasource' AS datasource_hint
    FROM alert_events
    WHERE category = 'nodata'
    ORDER BY starts_at DESC;

    Lista eventos reclasificados como nodata, junto con una pista del datasource extraída del JSON de labels.

2) Crear una columna generada en alert_events (solo si está justificado)

Materializar un label como columna (por ejemplo team, owner, service) puede ser útil, pero se recomienda hacerlo solo cuando el beneficio operativo o de performance lo justifique, ya que implica cambios de esquema y mantenimiento adicional.

Cuándo conviene (casos típicos)

  • El label se usa en dashboards críticos y con alto tráfico (muchas consultas / refresh frecuente).
  • Necesitás índices B-Tree o índices compuestos (por ejemplo cluster + namespace + owner) para acelerar filtros comunes.
  • Querés simplificar queries repetitivas y evitar depender de expresiones JSONB en cada panel.

Costos / implicancias

  • Requiere modificar el esquema (migración).
  • Debe mantenerse alineado entre entornos (local / Helm / producción) para evitar drift.
  • Aumenta la superficie de mantenimiento (índices, compatibilidad, upgrades, troubleshooting).

Recomendación: antes de crear columnas, evaluar si un índice por expresión sobre labels->>'mi_label' resuelve el problema sin tocar el modelo.


Pasos para exponer un nuevo label como columna generada

  1. Edita deploy/local/db-init/010_events.sql:

    • Dentro del bloque CREATE TABLE ... alert_events, agrega la columna siguiendo este patrón:
      • nombre_label text GENERATED ALWAYS AS ((labels->>'nombre_label')) STORED,
    • En el bloque -- Asegurar columnas GENERADAS..., agrega también:
      • ALTER TABLE alert_events ADD COLUMN IF NOT EXISTS nombre_label text GENERATED ALWAYS AS ((labels->>'nombre_label')) STORED; Esto garantiza compatibilidad cuando la tabla ya existe.
  2. Replica exactamente las mismas líneas en:

    • helm/chart-alerthub/templates/init-sql-configmap.yaml Dentro de la sección 010_events.sql, respetando la indentación YAML.
  3. Si el nuevo label será filtrado con frecuencia, agrega un índice dedicado en ambos scripts:

    • deploy/local/db-init/010_events.sql
    • helm/chart-alerthub/templates/init-sql-configmap.yaml

Nota sobre índices dedicados: cuando se espera filtrar habitualmente por el nuevo label, agrega:

CREATE INDEX IF NOT EXISTS ix_events_<label> ON alert_events (<label>);

Reemplaza <label> por el nombre real de la columna. Esto evita escaneos completos al construir dashboards.

Ejemplo completo: label owner

Sustituye owner y 'plataforma' por el nombre del label y el valor real que quieras documentar.

-- deploy/local/db-init/010_events.sql (bloque CREATE TABLE)
  owner text GENERATED ALWAYS AS ((labels->>'owner')) STORED,

-- deploy/local/db-init/010_events.sql (bloque ALTER TABLE)
ALTER TABLE alert_events
  ADD COLUMN IF NOT EXISTS owner text GENERATED ALWAYS AS ((labels->>'owner')) STORED;

-- Índice opcional si el filtro es frecuente (decláralo también en helm/chart-alerthub/templates/init-sql-configmap.yaml)
CREATE INDEX IF NOT EXISTS ix_events_owner ON alert_events (owner);

-- Consulta ejemplo
SELECT
  received_at,
  rule_name,
  severity
FROM alert_events
WHERE owner = 'plataforma';

Ejemplos de consultas sobre alert_events

Estos fragmentos ilustran cómo combinar los campos derivados y el JSON original de etiquetas. Cada bloque mantiene el formato SQL listo para copiar y ejecutar.

Consultas combinando filtros

  1. Alertas abiertas en las últimas 24 horas por namespace y severidad

    SELECT namespace,
           severity,
           count(*) AS open_alerts
    FROM alert_events
    WHERE status = 'firing'
      AND cluster = 'prod'
      AND starts_at >= now() - interval '24 hours'
    GROUP BY namespace, severity
    ORDER BY open_alerts DESC;

    Devuelve un ranking de alertas activas agrupadas por namespace y severity para el clúster prod durante la última ventana de 24 horas.

  2. Incidencias de colas payments en producción

    SELECT fingerprint,
           rule_name,
           category,
           queue,
           vhost,
           labels->>'environment' AS environment
    FROM alert_events
    WHERE status = 'firing'
      AND category = 'queue'
      AND queue = 'payments'
      AND labels->>'environment' = 'prod';

    Lista cada evento activo relacionado con la cola payments, mostrando la regla que disparó, su categoría y el entorno almacenado en el JSON de labels.

About

Alerthub es un servicio escrito en Go que recibe los webhooks de alerta generados por Grafana Alerting, los normaliza y los almacena en una base de datos PostgreSQL.

Resources

Contributing

Stars

Watchers

Forks

Packages

No packages published