Proyecto didáctico que demuestra los tres pilares de la observabilidad en una aplicación Spring Boot.
| # | Tema | Archivo |
|---|---|---|
| 00 | ¿Qué es la observabilidad? Monitoring vs. Observability. Los 3 pilares. Pull vs. Push. SLI/SLO/SLA. | docs/00-observabilidad.md |
| 01 | Métricas con Prometheus y Micrometer. Counter, Gauge, Histogram. PromQL. Cardinalidad. | docs/01-metricas.md |
| 02 | Trazas distribuidas con OpenTelemetry y Tempo. Spans, TraceContext, Sampling, TraceQL. | docs/02-trazas.md |
| 03 | Logs con Loki. Logs estructurados. LogQL. Loki vs. Elasticsearch. Correlación log→traza. | docs/03-logs.md |
| 04 | Alertas con Prometheus y AlertManager. Reglas PromQL. Routing. Silencing. Inhibition. | docs/04-alertas.md |
| 05 | Arquitectura de producción. Mismo servidor vs. servidor separado. Seguridad. Checklist. | docs/05-produccion.md |
Proyecto didáctico — implementación:
| Señal | Herramienta | Modelo | Protocolo |
|---|---|---|---|
| Métricas | Prometheus + Grafana | Pull | HTTP scraping |
| Trazas | Tempo + Grafana | Push | OTLP (gRPC/HTTP) |
| Logs | Loki + Grafana | Push | OTLP (HTTP) |
┌──────────────────────────────────────────────────────────────────┐
│ MISMO SERVIDOR (modo didáctico) │
│ │
│ ┌────────────────────┐ PULL ┌──────────────────────┐ │
│ │ Spring Boot :8080 │ ◄─── scrape ── │ Prometheus :9090 │ │
│ │ /actuator/ │ │ (métricas) │ │
│ │ prometheus │ └──────────┬───────────┘ │
│ │ │ │ remote_write │
│ │ │ PUSH (OTLP) ┌──────────▼───────────┐ │
│ │ │ ──────────► │ Alloy :4317/:4318 │ │
│ └────────────────────┘ │ (colector) │ │
│ └──────┬───────────────┘ │
│ │ │
│ ┌─────────────┼──────────────┐ │
│ │ │ │ │
│ traces logs metrics │
│ │ │ │ │
│ ┌──────▼──┐ ┌───────▼──┐ │ │
│ │ Tempo │ │ Loki │ │ │
│ │ :3200 │ │ :3100 │ │ │
│ └──────┬──┘ └───────┬──┘ │ │
│ │ │ │ │
│ ┌──────▼─────────────▼─────────────▼─┐ │
│ │ Grafana :3000 │ │
│ │ (métricas + trazas + logs) │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────┐ │
│ │ AlertManager :9093 │ ◄── alertas de Prometheus │
│ └───────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
docker compose up -d./mvnw spring-boot:runLa app corre en
http://localhost:8080
La variableOTEL_EXPORTER_HOST=localhost(default) apunta a Alloy local.
# Crear pedidos (genera métricas + trazas + logs)
curl -X POST "http://localhost:8080/api/orders?productId=PROD-001"
# Operación lenta (dispara alerta de latencia en ~5 min)
curl http://localhost:8080/api/orders/slow
# Error controlado (dispara alerta de error rate en ~2 min)
curl http://localhost:8080/api/orders/error
# Evento de negocio con duración custom
curl -X POST "http://localhost:8080/api/orders/events?type=shipment&durationMs=200"
# Ver métricas crudas de Prometheus
curl http://localhost:8080/actuator/prometheus | grep demo_Abrir http://localhost:3000 (admin/admin)
| Dashboard | Qué muestra |
|---|---|
| Spring Boot Starter | Métricas JVM, HTTP, threads |
| Monitoring Service Health | Error rate, latencia P95/P99, disponibilidad |
| Monitoring Logs Deep Dive | Logs con filtro por nivel, mensaje, correlación |
| Monitoring Traces Deep Dive | Trazas distribuidas, gantt chart por petición |
| Monitoring Service Unified | Vista combinada métricas + trazas + logs |
| Monitoring Operations | Resumen operacional general |
| Observability Platform Health | Estado de Prometheus, Loki, Tempo, Alloy |
monitoring/
├── src/main/java/edu/pucmm/monitoring/
│ ├── MonitoringApplication.java # Entrada + @ConfigurationPropertiesScan
│ ├── config/
│ │ ├── TelemetryConfig.java # MeterRegistryCustomizer (tags globales)
│ │ └── TelemetryProperties.java # @ConfigurationProperties("monitoring.telemetry")
│ ├── controller/
│ │ └── OrderController.java # Endpoints de demostración
│ └── service/
│ └── OrderService.java # Métricas custom: Counter, Timer, @Timed
│
├── telemetry/
│ ├── prometheus/
│ │ ├── prometheus.yml # Scrape config + alerting
│ │ └── rules/app-alerts.yml # Reglas de alerta PromQL
│ ├── loki/loki.yaml # Config de Loki (almacenamiento de logs)
│ ├── tempo/tempo.yaml # Config de Tempo (almacenamiento de trazas)
│ ├── alloy/
│ │ ├── config.alloy # Alloy modo mismo servidor (all-in-one)
│ │ └── agent.alloy # Alloy modo agente remoto (servidor separado)
│ ├── alertmanager/alertmanager.yml # Enrutamiento de alertas
│ └── grafana/
│ ├── provisioning/
│ │ ├── datasources/datasources.yml # Prometheus + Tempo + Loki
│ │ └── dashboards/dashboards.yaml # Carpeta de dashboards
│ └── dashboards/ # Archivos JSON de dashboards
│
├── docker-compose.yml # Stack completo de observabilidad
└── src/main/resources/application.yml # Config de la app con telemetría
Expone endpoints de gestión. El más importante para observabilidad:
GET /actuator/prometheus— métricas en formato Prometheus text
Modelo Pull: Prometheus va a buscar las métricas cada 15 segundos.
Tres formas de crear métricas custom:
// 1. Anotación declarativa (más simple)
@Timed(value = "demo_create_seconds", description = "Tiempo de creación")
public void create() { ... }
// 2. Counter (cuenta eventos)
Counter.builder("demo_orders_total").register(registry).increment();
// 3. Timer con histograma (mide duración y calcula percentiles)
Timer.builder("demo_processing_seconds").register(registry).record(() -> {
// lógica de negocio
});Modelo Push: la app envía activamente sus trazas y logs a Alloy.
spring-boot-starter-opentelemetryinstrumenta automáticamente HTTP, JDBC, etc.OTEL_EXPORTER_HOSTcontrola hacia dónde se envían las señales.- Sampling en
application.yml: 1.0 = 100% (didáctico), 0.1 = 10% (producción).
Colector de telemetría que:
- Recibe OTLP de la app (puerto 4317/4318)
- Enriquece los logs con labels para Loki
- Reenvía trazas a Tempo
- Reenvía logs a Loki
- Descubre y recolecta logs de contenedores Docker
La observabilidad real viene de poder cruzar las tres señales:
- Métrica anómala → buscar trazas de ese período → revisar logs de esa traza
- Error en log → ir a la traza completa → ver en qué servicio falló
- Traza lenta → correlacionar con métricas de CPU/memoria del momento
En producción, el stack de telemetría corre en un servidor diferente al de la app.
Solo una variable de entorno:
# Antes (mismo servidor):
OTEL_EXPORTER_HOST=localhost
# Después (servidor separado):
OTEL_EXPORTER_HOST=192.168.1.100 # IP del servidor de telemetríaUsar telemetry/alloy/agent.alloy en lugar de config.alloy:
docker run \
-v ./telemetry/alloy/agent.alloy:/etc/alloy/config.alloy \
-p 4317:4317 -p 4318:4318 \
-e CENTRAL_ALLOY_OTLP_GRPC_ENDPOINT=192.168.1.100:4317 \
-e CENTRAL_LOKI_PUSH_URL=http://192.168.1.100:3100/loki/api/v1/push \
-e DEPLOYMENT_ENVIRONMENT=prod \
-v /var/run/docker.sock:/var/run/docker.sock \
grafana/alloy:latest run /etc/alloy/config.alloyEl prometheus.yml necesita apuntar a la IP del servidor de la app:
scrape_configs:
- job_name: app
static_configs:
- targets: ["192.168.1.50:8080"] # IP del servidor de la app[Servidor App :IP_A] [Servidor Telemetría :IP_T]
┌──────────────────────┐ ┌────────────────────────────┐
│ Spring Boot :8080 │ │ Alloy (central) :4317 │
│ Alloy (agente) │─────►│ Tempo :3200 │
│ :4317/:4318 │ WAN │ Loki :3100 │
└──────────────────────┘ │ Prometheus :9090 │
▲ │ Grafana :3000 │
│ scraping │ AlertManager :9093 │
└────────────────────┤ │
└────────────────────────────┘
| Puerto | Servicio | Propósito |
|---|---|---|
| 8080 | Spring Boot App | API REST + /actuator/prometheus |
| 3000 | Grafana | Dashboards |
| 9090 | Prometheus | UI + PromQL |
| 3200 | Tempo | API de trazas |
| 3100 | Loki | API de logs |
| 4317 | Alloy | OTLP gRPC receiver |
| 4318 | Alloy | OTLP HTTP receiver |
| 12345 | Alloy | UI de diagnóstico |
| 9093 | AlertManager | UI + API |
# Tasa de peticiones HTTP por segundo
rate(http_server_requests_seconds_count{application="monitoring-demo"}[5m])
# Latencia P95
histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri))
# Tasa de errores
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_count[5m]))
# Pedidos creados por segundo
rate(demo_orders_created_total[5m])
# Todos los logs de la app
{service_name="monitoring-demo"}
# Solo errores
{service_name="monitoring-demo"} |= "ERROR"
# Logs con traceId específico
{service_name="monitoring-demo"} | json | traceId="abc123..."
# Tasa de errores en logs
sum(rate({service_name="monitoring-demo"} |= "ERROR" [5m]))
# Trazas del servicio de demostración
{resource.service.name="monitoring-demo"}
# Trazas lentas (más de 500ms)
{resource.service.name="monitoring-demo" && duration>500ms}
# Trazas con errores
{resource.service.name="monitoring-demo" && status=error}