> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bbrands.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Arquitectura de salud

> El motor de veredicto de tres estados, los endpoints de pull por componente, la sincronización de degradado vía Status Reports y los heartbeats de cron — cómo horizon-api convierte el tráfico real en una página de estado.

La página de estado es tan buena como la señal que hay detrás. B Brands no depende
de un ping sintético que dice "el servidor está arriba"; deriva la salud de cada componente
del **tráfico real de producción** y publica tres estados distintos.

## El veredicto de tres estados

Cada componente (una categoría de API o una integración) se clasifica continuamente
en uno de tres estados.

<CardGroup cols={3}>
  <Card title="operational" icon="circle-check">
    Todo está dentro de la banda saludable.
  </Card>

  <Card title="degraded" icon="triangle-exclamation">
    Un cron obsoleto, una única ruta que falla, o una tasa de error de bajo nivel sostenida.
  </Card>

  <Card title="down" icon="circle-xmark">
    Un pico de errores de ventana corta que significa una caída real.
  </Card>
</CardGroup>

### Cómo se computa el veredicto

Funcionalmente: la API cuenta constantemente las solicitudes exitosas y fallidas por
componente, observa dos ventanas de tiempo, y elige el peor estado que aplique.

Técnicamente, la precedencia se evalúa de arriba hacia abajo:

1. **`down`** — tasa de error de ventana corta `>= 50%` sobre `>= 5` solicitudes, o (para
   integraciones) una probe activa fallida.
2. **`degraded`** — un cron folded obsoleto, una ruta individual que falla, o una
   tasa de error de ventana larga sostenida `>= 1%` sobre `>= 5` solicitudes.
3. **`operational`** — todo lo demás.

<Note>
  La **histéresis** previene el flapping. Una vez que un componente entra en `degraded`, solo
  regresa a `operational` cuando la tasa de error de ventana larga cae por debajo del
  umbral de recuperación (`0.5%`). El último veredicto se cachea en Redis
  (`health:verdict:{component}`).
</Note>

### Ventanas de tráfico

Los contadores viven en buckets de Redis de 5 minutos
(`traffic:{scope}:{ok|err}:{windowStart}`, TTL \~70 min). Se leen dos rangos:

| Ventana | Buckets | Rango    | Impulsa    |
| ------- | ------- | -------- | ---------- |
| Corta   | 2       | \~10 min | `down`     |
| Larga   | 12      | \~60 min | `degraded` |

### Umbrales

Todos los umbrales se exportan desde `lib/endpoint/health/integration-traffic.ts`,
así que ajustarlos es un cambio de código de una línea revisado en un PR.

| Constante                      | Valor | Significado                                       |
| ------------------------------ | ----- | ------------------------------------------------- |
| `MAX_TRAFFIC_ERROR_RATE`       | 0.50  | Tasa de ventana corta que significa `down`.       |
| `MIN_TRAFFIC_VOLUME`           | 5     | Solicitudes mínimas antes de confiar en una tasa. |
| `DEGRADED_ERROR_RATE`          | 0.01  | Tasa de ventana larga para entrar en `degraded`.  |
| `DEGRADED_RECOVERY_ERROR_RATE` | 0.005 | Tasa de ventana larga para salir de `degraded`.   |
| `MIN_ROUTE_VOLUME`             | 5     | Solicitudes mínimas para que una ruta cuente.     |
| `ROUTE_DOWN_ERROR_RATE`        | 0.50  | Tasa por ruta que degrada su categoría.           |

## Seguimiento por ruta

`recordApiTraffic` registra tanto un contador de categoría como un contador por ruta. La
ruta se normaliza (`normalizeRoutePath`) para que los segmentos dinámicos — UUIDs, ids
numéricos, tokens opacos largos — colapsen a `[id]`, lo que acota la cardinalidad. Cada
clave de ruta es `{METHOD}:{normalized-path}`.

Esto captura una única ruta que falla con fuerza (por ejemplo, una ruta rota de MaihueGO)
dentro de una categoría por lo demás sana: la señal de la ruta degrada toda la
categoría incluso cuando la tasa de error agregada se mantiene baja y de otro modo ocultaría
el problema.

## Endpoints de pull por componente

Better Stack no lee Redis ni el estado interno. Consulta un endpoint HTTP
por componente:

```text theme={null}
GET /api/v3/health/component/api/<category>
GET /api/v3/health/component/integration/<name>
```

Ambos requieren el header `x-health-token`, cotejado contra `HEALTH_MONITOR_TOKEN`:

* `401` cuando el header no coincide.
* `503` cuando el token no está configurado (**fail closed** — nunca expongas la salud sin
  auth).
* `503` **solo** cuando el veredicto es `down`. Tanto `operational` como `degraded`
  devuelven `200`; la capa amarilla se publica a través de Status Reports, nunca como
  downtime. El veredicto completo está siempre en el cuerpo de la respuesta.

## Sincronización de degradado (Status Reports)

`lib/endpoint/health/degraded-sync.ts` reconcilia cada veredicto con los
Status Reports publicados en Better Stack:

* Entrar en `degraded` → `createDegradedReport`, almacenando el id del report en
  `health:degraded-report:{component}`.
* Salir de `degraded` → `resolveDegradedReport` publica un status update
  `resolved` en el report (nunca lo elimina, de modo que la entrada de la página
  de estado y su espejo en Slack muestran la recuperación) y limpia el marcador.

Lee el mapa componente → id de recurso de la página de estado desde la
variable de entorno JSON `BETTERSTACK_STATUS_PAGE_RESOURCES`. Se ejecuta desde ambos
crons de salud después de computar los veredictos y es totalmente best-effort (nunca
lanza). Cualquier componente que no esté presente en el mapa simplemente se omite.

## Heartbeats de cron

Los heartbeats confirman que el scheduler en sí está vivo — una preocupación separada de
la salud de los componentes.

| Ruta de cron                       | Variable de entorno del heartbeat                | Espera / gracia |
| ---------------------------------- | ------------------------------------------------ | --------------- |
| `/api/v3/cron/health/api`          | `BETTERSTACK_HEARTBEAT_CRON_HEALTH_API`          | 5 min / \~3 min |
| `/api/v3/cron/health/integrations` | `BETTERSTACK_HEARTBEAT_CRON_HEALTH_INTEGRATIONS` | 5 min / \~3 min |
| `/api/v3/cron/metrics/push`        | `BETTERSTACK_HEARTBEAT_CRON_METRICS_PUSH`        | 5 min / \~3 min |

`pingHeartbeat` reintenta una vez con un breve backoff dentro de su timeout, absorbiendo
los baches de red transitorios.

## Métricas de infraestructura

El cron de metrics-push (`/api/v3/cron/metrics/push`) mide la latencia de la base de datos
y de Redis para los gráficos de Infraestructura. Como una única muestra de arranque en frío
producía picos falsos, el colector calienta la conexión con una probe sin cronometrar,
toma tres muestras cronometradas, y reporta la **mediana** — manteniendo la
cadencia de 5 minutos mientras suaviza los outliers puntuales.

## Ruta de la señal de extremo a extremo

```mermaid theme={null}
sequenceDiagram
    participant Client
    participant API as horizon-api
    participant Redis
    participant Monitor as Better Stack Monitor
    participant Reports as Status Reports
    participant Page as Status page

    Client->>API: Normal requests
    API->>Redis: recordApiTraffic (ok/err buckets)
    Monitor->>API: GET /health/component/... (x-health-token)
    API->>Redis: read short + long windows
    API-->>Monitor: 200 (operational/degraded) or 503 (down)
    Monitor->>Page: component red on 503
    Note over API,Reports: Every 5 min (health crons)
    API->>Reports: create/resolve degraded report
    Reports->>Page: component yellow while report is open
```

## Código relacionado

* Motor de veredicto y umbrales: `backend/horizon-api/lib/endpoint/health/`
* Cliente de Status Report: `backend/horizon-api/lib/integration/betterstack/status-report.client.ts`
* Análisis profundo: `docs/initiatives/betterstack/01-architecture.md`
