Skip to content

Arquitectura — MiniObserv ​


1. Visión general ​

MiniObserv es una plataforma de observabilidad de infraestructura minimalista y autoalojada. Su objetivo es responder a una pregunta concreta: ¿qué está haciendo este servidor ahora y en los últimos N minutos?

Lo que es:

  • Un sistema de recolección y almacenamiento de métricas de sistema (CPU, memoria, disco, red).
  • Una API de consulta con agregaciones temporales basada en TimescaleDB.
  • Una solución operativa lista para ejecutarse con docker compose up.

Lo que no es:

  • No es una plataforma de trazabilidad distribuida ni de logs estructurados.
  • No está diseñado para escalado horizontal multi-instancia de servidor en esta versión.

2. Diagrama de componentes ​

┌────────────────────────────────────────────────────────────────┐
│  Host monitorizado                                             │
│                                                                │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  Agente (cmd/agent)                                      │  │
│  │                                                          │  │
│  │  ┌──────────────┐    ┌────────────────────────────────┐  │  │
│  │  │  Collectors  │───▶│  Agent (goroutines)            │  │  │
│  │  │  cpu         │    │  collectLoop → batches chan    │  │  │
│  │  │  memory      │    │  senderLoop  ← batches chan    │  │  │
│  │  │  disk        │    └──────────────┬─────────────────┘  │  │
│  │  │  network     │                   │                     │  │
│  │  └──────────────┘                   │ HTTP POST           │  │
│  └──────────────────────────────────── │ ────────────────────┘  │
│                                        │ JWT Bearer             │
└───────────────────────────────────────────────────────────────-┘
                                         │
                                         ▼
┌────────────────────────────────────────────────────────────────┐
│  Servidor (cmd/server)                                         │
│                                                                │
│  ┌──────────────┐   ┌────────────────┐   ┌────────────────┐   │
│  │  JWTMiddle-  │──▶│  api.Handle-   │──▶│  storage.      │   │
│  │  ware        │   │  Ingest /      │   │  pgxMetric-    │   │
│  │              │   │  HandleQuery   │   │  Repository    │   │
│  └──────────────┘   └───────┬────────┘   └───────┬────────┘   │
│                             │                     │            │
│                             │ Heartbeat           │ pgx.Batch  │
│                             ▼                     │ pgxpool    │
│                    ┌────────────────┐             │            │
│                    │  HostTracker   │──────────── │ ──────┐    │
│                    │  (en memoria)  │             │       │    │
│                    └───────┬────────┘             │       │    │
│                            │ host.down            │       │    │
│                            ▼                      │       │    │
│                    ┌────────────────┐             │       │    │
│                    │  Webhook-      │             │       │    │
│                    │  Notifier      │             │       │    │
│                    │  (goroutine)   │             │       │    │
│                    └────────────────┘             │       │    │
│                                                   │       │    │
│  GET /healthz  ──▶  HandleHealthz (sin auth)      │       │    │
│  GET /readyz   ──▶  HandleReadyz  (sin auth)      │       │    │
│  GET /api/v1/hosts ──▶ HandleHosts (sin auth) ◀───────────┘    │
└───────────────────────────────────────────────────────────────┘
                                                    │
                                                    ▼
                              ┌─────────────────────────────────┐
                              │  TimescaleDB (PostgreSQL 16)     │
                              │                                  │
                              │  tabla: metrics                  │
                              │  hypertable por time             │
                              │  chunk: 1 día                    │
                              │  índice: (host, name, time DESC) │
                              └─────────────────────────────────┘

3. Flujo de datos ​

1. Ticker (COLLECT_INTERVAL)
        │
        ▼
2. collector.Registry.CollectAll()
   ├── CPUCollector     → cpu.usage_pct (por núcleo y total)
   ├── MemoryCollector  → mem.used_pct, mem.used_bytes, mem.total_bytes
   ├── DiskCollector    → disk.used_pct, disk.used_bytes, disk.total_bytes
   └── NetworkCollector → net.bytes_in, net.bytes_out (delta desde tick anterior)
        │
        ▼
3. []model.Metric → model.MetricBatch{Host, Metrics}
        │
        ▼
4. batches chan (buffer 10) — desacopla recolección de envío
        │
        ▼
5. HTTPSender.Send() — POST /api/v1/metrics con Bearer JWT
   └── backoff exponencial en errores transitorios (1 s → 60 s, ±25 % jitter)
        │
        ▼
6. JWTMiddleware valida el token HS256
        │
        ▼
7. api.HandleIngest() — decodifica JSON, valida, límite 1000 métricas
        │
        ▼
8. storage.Insert() — pgx.Batch, un round-trip por batch
        │
        ▼
9. TimescaleDB — INSERT INTO metrics (time, host, name, value, labels)
        │
        ▼
10. GET /api/v1/metrics/query → time_bucket() + avg/max/min → []QueryPoint

4. Estructura de paquetes ​

github.com/kamerrezz/theminidog/
├── cmd/
│   ├── agent/          — punto de entrada del agente: carga config, construye
│   │                     el grafo de dependencias, arranca Agent.Run()
│   └── server/         — punto de entrada del servidor: carga config, aplica
│                         migraciones, arranca el servidor HTTP
├── internal/
│   ├── agent/
│   │   ├── agent.go    — coordinación: collectLoop + senderLoop con goroutines
│   │   ├── collector/
│   │   │   ├── collector.go  — interfaz Collector y Registry
│   │   │   ├── cpu.go        — uso de CPU por núcleo (gopsutil)
│   │   │   ├── memory.go     — uso de RAM
│   │   │   ├── disk.go       — uso de disco por punto de montaje
│   │   │   └── network.go    — bytes in/out por interfaz (delta semántico)
│   │   └── sender/
│   │       └── sender.go     — HTTPSender con backoff exponencial y JWT
│   ├── config/
│   │   ├── agent.go    — LoadAgent(): variables de entorno del agente
│   │   └── server.go   — LoadServerConfig(): variables de entorno del servidor
│   ├── model/
│   │   └── metric.go   — Metric, MetricBatch, validación y lista blanca de nombres
│   └── server/
│       ├── server.go   — ciclo de vida HTTP: arranque, graceful shutdown
│       ├── api/
│       │   ├── router.go     — registro de rutas
│       │   ├── metrics.go    — HandleIngest, HandleQuery
│       │   ├── hosts.go      — HandleHosts: estado de salud de hosts (GET /api/v1/hosts)
│       │   ├── health.go     — HandleHealthz, HandleReadyz
│       │   ├── middleware.go — JWTMiddleware HS256
│       │   └── errors.go     — writeError: formato JSON estándar
│       ├── alerting/
│       │   └── notifications.go — WebhookNotifier: entrega fire-and-forget con timeout 5 s
│       └── storage/
│           ├── metrics.go    — pgxMetricRepository: Insert (batch) y Query
│           └── hosts.go      — HostTracker en memoria: Heartbeat, HostStatuses, detección de down
├── migrations/
│   ├── 001_create_metrics.up.sql   — extensión TimescaleDB, tabla metrics, hypertable, índice
│   ├── 002_create_alerts.up.sql    — tabla de reglas de alerta y alertas activas
│   └── 003_create_logs.up.sql      — tabla logs, PK compuesta (id, time), hypertable
└── deployments/
    └── docker-compose.yml          — stack completo: TimescaleDB + servidor + agente

5. Diseño del almacenamiento ​

Por qué TimescaleDB ​

TimescaleDB extiende PostgreSQL con hypertables: tablas particionadas automáticamente por tiempo. Esto permite:

  • Consultas eficientes por rango temporal sin necesidad de particionamiento manual.
  • time_bucket(): agrupación temporal nativa con granularidades arbitrarias.
  • Retención de datos configurable mediante políticas (no implementada en esta versión, pero disponible).
  • Compatibilidad total con el ecosistema PostgreSQL (pgx, migraciones, JSON, índices).

Modelo estrecho ​

La tabla tiene exactamente cinco columnas:

sql
CREATE TABLE metrics (
    time   TIMESTAMPTZ      NOT NULL,
    host   TEXT             NOT NULL,
    name   TEXT             NOT NULL,
    value  DOUBLE PRECISION NOT NULL,
    labels JSONB
);

Este diseño "estrecho" tiene ventajas deliberadas:

  • Esquema fijo: no se requieren migraciones al añadir nuevas métricas; solo se cambia el código del agente.
  • Labels JSONB: permite metadatos variables por tipo de métrica (core=0, mount=/, iface=eth0) sin columnas adicionales.
  • Una fila por medición: facilita el razonamiento sobre los datos y simplifica las consultas.

Hypertable ​

sql
SELECT create_hypertable('metrics', 'time',
    chunk_time_interval => INTERVAL '1 day',
    if_not_exists => TRUE
);

Cada día de datos se almacena en un chunk independiente. Las consultas por rango temporal solo tocan los chunks relevantes, lo que reduce drásticamente el I/O.

Índice compuesto ​

sql
CREATE INDEX idx_metrics_host_name_time ON metrics (host, name, time DESC);

El patrón de consulta habitual es WHERE host = $1 AND name = $2 AND time BETWEEN $3 AND $4. El índice cubre este acceso en orden descendente, ideal para consultas de "los últimos N minutos".

Inserción en batch ​

El repositorio usa pgx.Batch para enviar todas las métricas de un tick en un único round-trip de red. Es obligatorio llamar a defer br.Close() para liberar la conexión al pool.


6. Seguimiento de hosts y notificaciones (Semana 5) ​

HostTracker ​

storage.HostTracker mantiene en memoria el timestamp de la última actividad de cada host. El handler HandleIngest llama a Heartbeat(host) en cada ingesta exitosa.

Un goroutine de vigilancia (watchLoop) evalúa periódicamente el estado de cada host según dos umbrales:

UmbralVariablePredeterminadoEstado resultante
Tiempo sin reporte < HOST_STALE_AFTERHOST_STALE_AFTER20sok
Tiempo sin reporte entre HOST_STALE_AFTER y HOST_DOWN_AFTER——stale
Tiempo sin reporte > HOST_DOWN_AFTERHOST_DOWN_AFTER50sdown

Cuando un host pasa a down, HostTracker llama directamente a los notificadores registrados con un evento host.down. Esto es independiente del sistema de alertas por umbral de métricas.

WebhookNotifier ​

alerting.WebhookNotifier implementa la interfaz Notifier. Recibe un evento, serializa el payload JSON y lo envía mediante POST HTTP con un timeout de 5 segundos. La entrega es "fire-and-forget": no hay reintentos en v1.

Cada notificación se ejecuta en una goroutine separada con context.WithoutCancel para desacoplar su ciclo de vida de la petición HTTP que la originó.

Endpoint GET /api/v1/hosts ​

api.HandleHosts consulta HostTracker.HostStatuses() y serializa el resultado. No requiere autenticación y es adecuado para sondas externas de monitoreo del estado de la flota.


8. Flujo de autenticación ​

AGENT_TOKEN (secreto compartido, ≥16 chars)
        │
        ├── Agente: genera JWT HS256 con exp=now+24h
        │         Header: {"alg":"HS256","typ":"JWT"}
        │         Payload: {"sub":"agent","exp":<unix>}
        │         Firma: HMAC-SHA256(header.payload, AGENT_TOKEN)
        │
        └── Servidor: JWTMiddleware valida en cada petición autenticada
                      ├── Verifica firma con AGENT_TOKEN
                      ├── Verifica algoritmo == HS256 (bloquea alg=none y RS256)
                      └── Verifica expiración automáticamente

Las rutas /healthz y /readyz no requieren autenticación (sondas de infraestructura).


9. Convención de nombres de métricas ​

MiniObserv usa un conjunto cerrado de nueve nombres canónicos. El servidor rechaza cualquier nombre fuera de esta lista con HTTP 400.

NombreTipoLabelsDescripción
cpu.usage_pctporcentaje (0–100)core=total|0|1|…Uso de CPU por núcleo y total
mem.used_pctporcentaje (0–100)—Porcentaje de RAM usada
mem.used_bytesbytes—Bytes de RAM en uso
mem.total_bytesbytes—Bytes de RAM total
disk.used_pctporcentaje (0–100)mount=/Uso de disco por punto de montaje
disk.used_bytesbytesmount=/Bytes de disco usados
disk.total_bytesbytesmount=/Bytes de disco total
net.bytes_inbytes (delta)iface=eth0Bytes recibidos desde el tick anterior
net.bytes_outbytes (delta)iface=eth0Bytes enviados desde el tick anterior

10. Semántica de deltas en red ​

Los contadores del sistema operativo para net.bytes_in y net.bytes_out son acumulativos: siempre crecen desde el arranque. MiniObserv convierte estos contadores en deltas por intervalo:

tick N:   lee BytesRecv=1000 → guarda como prev; devuelve nil
tick N+1: lee BytesRecv=1150 → delta = 1150 - 1000 = 150 bytes → emite net.bytes_in{iface=eth0}=150

Consecuencias:

  • El primer tick del agente devuelve cero métricas de red. Esto es correcto.
  • Si una interfaz aparece por primera vez en el tick N+1 (no estaba en tick N), ese ciclo también se omite.
  • Si el contador del sistema operativo retrocede (reinicio, overflow de 32 bits), el delta se clampea a cero.
  • El loopback (lo) siempre se excluye.

11. Referencia de configuración ​

Agente ​

VariableObligatoriaPredeterminadoValidación
SERVER_URLsí—URL válida http:// o https://
AGENT_TOKENnovacíoSin validación; vacío = sin autenticación JWT
AGENT_HOSTnoos.Hostname()Cualquier string no vacío
COLLECT_INTERVALno10sDuración Go válida entre 1 s y 300 s
LOG_LEVELnoinfodebug, info, warn, error
LOG_PATHSnovacíoRutas separadas por coma

Servidor ​

VariableObligatoriaPredeterminadoValidación
DATABASE_URLsí—DSN postgres:// o postgresql:// válido
AGENT_TOKENsí—Mínimo 16 caracteres
LISTEN_ADDRno:8080Dirección de escucha válida
MIGRATIONS_PATHno./migrationsRuta al directorio de migraciones
LOG_LEVELnoinfodebug, info, warn, error
REQUEST_TIMEOUTno10sEntre 1 s y 120 s
SHUTDOWN_TIMEOUTno5sEntre 1 s y 30 s
ALERT_NOTIFICATIONSno—Array JSON de objetos webhook para alertas y eventos host.down
HOST_STALE_AFTERno20sTiempo tras el cual un host silencioso pasa a estado stale
HOST_DOWN_AFTERno50sTiempo tras el cual un host silencioso pasa a estado down y se dispara el notificador

Released under the MIT License.