La lección anterior terminó con un problema que no admite aplazamiento: todas las métricas de CicloUrbana viven en memoria y mueren con el proceso. Cada despliegue automático de 08-05 las borra; /actuator/metrics devuelve el acumulado de ahora mismo y no tiene historia; no hay una gráfica, no hay comparación con la semana pasada y, sobre todo, no hay una sola alerta. Los SLO que definimos con RED y USE están escritos y no los vigila nadie.
Esta lección cierra ese hueco con las dos herramientas que se han convertido en el estándar de facto: Prometheus, que recoge y almacena series temporales, y Grafana, que las representa. Veremos por qué Prometheus va a buscar los datos en lugar de esperarlos, cómo exponer las métricas de Micrometer en su formato, cómo leer ese formato de texto —incluidas las cubetas de histograma que configuramos en 09-03—, PromQL de verdad hasta poder calcular el p95 real de la red de Ribalta agregando las tres instancias, el cuadro de mando de CicloUrbana panel a panel y, lo que de verdad cambia la vida del equipo, las reglas de alerta que convierten un número en una llamada de teléfono cuando —y solo cuando— hace falta.
Contenido
- Por qué hace falta un sistema externo
- El modelo pull frente al push
- La arquitectura de monitorización de CicloUrbana
- Exponer
/actuator/prometheus - Leer el formato de texto
- El modelo de datos de Prometheus
prometheus.ymlpara CicloUrbana- Descubrimiento de objetivos en Kubernetes
- Retención y almacenamiento
- PromQL: lo que hay que saber
- Las consultas de CicloUrbana, una a una
- Levantar la pila con
docker-compose - Grafana: origen de datos y cuadros de mando importados
- El cuadro de mando de CicloUrbana, panel a panel
- Reglas de alerta
- Alertmanager: rutas, agrupación y silencios
- Buenas prácticas de alertado
- Alternativas gestionadas
- Errores Comunes y Consejos
- Ejercicios
- Por qué hace falta un sistema externo
El MeterRegistry de 09-03 es un conjunto de contadores en memoria. Eso implica cuatro carencias que ninguna configuración de Actuator puede resolver:
| Carencia | Consecuencia práctica |
|---|---|
| Sin persistencia | Cada reinicio o despliegue pone los contadores a cero |
| Sin historia | No se puede responder «¿esto pasaba la semana pasada?» |
| Sin agregación | Tres instancias dan tres respuestas distintas y nadie las suma |
| Sin evaluación continua | Nadie mira /actuator/metrics a las cuatro de la mañana |
Un sistema de monitorización externo resuelve las cuatro: recoge periódicamente, almacena con retención, agrega entre instancias y evalúa reglas para alertar. La aplicación solo tiene que exponer sus números en un formato que ese sistema entienda, y con Micrometer eso cuesta una dependencia.
- El modelo pull frente al push
Prometheus va a buscar los datos: cada 15 segundos hace una petición HTTP a cada instancia y guarda lo que le devuelve. Es la decisión de diseño que más lo distingue de sistemas como Datadog o StatsD, donde es la aplicación la que empuja sus métricas hacia un colector.
| Pull (Prometheus) | Push (StatsD, Datadog) | |
|---|---|---|
| Quién inicia | El servidor de monitorización | La aplicación |
| Configuración de destinos | Centralizada, en un fichero | En cada aplicación |
| ¿La instancia está viva? | Se sabe gratis: up == 0 si no responde |
Hay que deducirlo de la ausencia de datos |
| Sobrecarga en la aplicación | Mínima: responder un texto | Un hilo emisor y una cola |
| Procesos efímeros (lotes) | Problema: pueden morir antes de la recogida | Natural |
| Redes con NAT o cortafuegos | El servidor debe alcanzar la aplicación | La aplicación solo necesita salida |
| Depuración | Abres la URL en el navegador y lo ves | Hay que mirar el destino |
La ventaja decisiva en la práctica es la tercera: con pull, Prometheus genera automáticamente una métrica up por objetivo, de modo que «la aplicación está caída» es una consulta, no una inferencia. Y para el caso que el modelo no cubre —tareas efímeras como la importación nocturna de 07-03, que puede terminar entre dos recogidas— existe el Pushgateway, un intermediario donde el proceso deposita sus métricas y del que Prometheus tira después. Es la excepción, y conviene tratarla como tal: usarlo para servicios normales anula la detección de caídas.
- La arquitectura de monitorización de CicloUrbana
flowchart LR
subgraph K8s["Kubernetes · 08-04"]
A1[ciclourbana-1<br/>:8081/actuator/prometheus]
A2[ciclourbana-2]
A3[ciclourbana-3]
end
P[(Prometheus<br/>TSDB local)]
A1 -->|scrape 15s| P
A2 -->|scrape 15s| P
A3 -->|scrape 15s| P
PG[PostgreSQL<br/>postgres_exporter] --> P
P --> G[Grafana<br/>cuadros de mando]
P -->|reglas disparadas| AM[Alertmanager]
AM --> S[Slack del equipo]
AM --> O[Guardia · PagerDuty]
G -.consulta PromQL.-> P
Cinco piezas y una responsabilidad cada una. La aplicación solo expone un endpoint de texto. Prometheus recoge, almacena y evalúa las reglas: fíjate en que las alertas se disparan en Prometheus, no en Grafana. Alertmanager recibe las alertas disparadas y decide a quién avisar, agrupando, silenciando y evitando repeticiones. Grafana solo consulta y dibuja: no almacena nada. Y los exporters —como postgres_exporter— publican en el mismo formato las métricas de sistemas que no hablan Prometheus, algo que interesa porque en 09-01 quedó claro que buena parte de los problemas de CicloUrbana están en la base de datos.
- Exponer
/actuator/prometheus
/actuator/prometheus<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>Con solo esa dependencia, Micrometer registra un PrometheusMeterRegistry y Actuator añade el endpoint. Falta exponerlo:
management:
endpoints.web.exposure.include: health,info,loggers,metrics,caches,prometheus
server.port: 8081 # el puerto de gestion de 07-01
metrics.tags:
application: ciclourbana
entorno: ${SPRING_PROFILES_ACTIVE:local}
prometheus.metrics.export.enabled: trueSeguridad: la cadena EndpointRequest.toAnyEndpoint() de 07-01 ya cubre este endpoint y exige ADMIN, así que Prometheus necesitará credenciales (apartado 7). Es lo correcto: /actuator/prometheus revela nombres de endpoints, versiones y el volumen de negocio de Ribalta. Y sigue viviendo en el puerto 8081, que no sale de la red interna.
- Leer el formato de texto
# HELP ciclourbana_alquileres_iniciados_total Alquileres iniciados
# TYPE ciclourbana_alquileres_iniciados_total counter
ciclourbana_alquileres_iniciados_total{application="ciclourbana",entorno="prod",estacion="1",tarifa="ESTANDAR",} 18422.0
ciclourbana_alquileres_iniciados_total{application="ciclourbana",entorno="prod",estacion="1",tarifa="ESTUDIANTE",} 4310.0
# HELP ciclourbana_bicicletas_disponibles Bicicletas disponibles por estacion
# TYPE ciclourbana_bicicletas_disponibles gauge
ciclourbana_bicicletas_disponibles{application="ciclourbana",estacion="Plaza Mayor",} 12.0
# HELP http_server_requests_seconds Duration of HTTP server request handling
# TYPE http_server_requests_seconds histogram
http_server_requests_seconds_bucket{uri="/api/v1/alquileres",status="201",le="0.1",} 8241.0
http_server_requests_seconds_bucket{uri="/api/v1/alquileres",status="201",le="0.3",} 11903.0
http_server_requests_seconds_bucket{uri="/api/v1/alquileres",status="201",le="0.8",} 12744.0
http_server_requests_seconds_bucket{uri="/api/v1/alquileres",status="201",le="+Inf",} 12801.0
http_server_requests_seconds_count{uri="/api/v1/alquileres",status="201",} 12801.0
http_server_requests_seconds_sum{uri="/api/v1/alquileres",status="201",} 1284.31Cómo se lee, elemento por elemento. # HELP es la descripción, que viene literalmente del .description(...) que escribimos en MetricasCicloUrbana. # TYPE declara el tipo —counter, gauge, histogram, summary—, y a partir de él Grafana y PromQL saben qué operaciones tienen sentido. Cada línea siguiente es una serie temporal: nombre, llaves con las etiquetas y valor actual; la combinación nombre + etiquetas es su identidad, y aquí se ve por qué la cardinalidad de 09-03 importa tanto —cada línea es una serie que Prometheus indexará y guardará para siempre—.
Los sufijos merecen atención especial, porque son la traducción de lo que configuramos en 09-03. Un contador se convierte en _total por convención de Prometheus. Un histograma produce tres familias: _count (cuántas observaciones), _sum (la suma de todas, en segundos) y _bucket con la etiqueta le (less or equal), una serie por umbral. Fíjate en que las cubetas son acumulativas: le="0.3" vale 11.903 e incluye las 8.241 de le="0.1". La última, le="+Inf", coincide siempre con _count. Y esos umbrales 0,1 / 0,3 / 0,8 salen exactamente del slo: que configuramos en 09-03.
Con _count y _sum se obtiene la media (_sum / _count = 1284,31 / 12801 = 100 ms), y con las cubetas se obtiene el percentil real: eso es el apartado 10.
- El modelo de datos de Prometheus
Una muestra es un valor float64 con una marca de tiempo, y pertenece a una serie temporal identificada por un nombre de métrica y un conjunto de etiquetas. Nada más: no hay tablas, ni esquemas, ni tipos complejos.
ciclourbana_alquileres_iniciados_total{entorno="prod", tarifa="ESTANDAR", estacion="1"}
└────────────── nombre ──────────────┘ └────────────── etiquetas ──────────────┘Los cuatro tipos: counter (monótono creciente; se consulta con rate, nunca por su valor absoluto), gauge (sube y baja; su valor sí significa algo), histogram (cubetas acumulativas, agregable) y summary (percentiles precalculados; el que produce percentiles de 09-03, no agregable).
Prometheus añade además unas etiquetas propias a cada muestra: job (el nombre del grupo de objetivos), instance (host:puerto) y las que se definan en la configuración. Por eso, aunque las tres réplicas publiquen el mismo contador, sus series son distinguibles.
prometheus.yml para CicloUrbana
prometheus.yml para CicloUrbanaglobal:
scrape_interval: 15s # cada cuanto se recogen los objetivos
evaluation_interval: 15s # cada cuanto se evaluan las reglas de alerta
external_labels:
cluster: ribalta-prod # identifica el origen en alertas y federacion
rule_files:
- /etc/prometheus/reglas/*.yml
alerting:
alertmanagers:
- static_configs:
- targets: ["alertmanager:9093"]
scrape_configs:
- job_name: ciclourbana
metrics_path: /actuator/prometheus # NO es el /metrics por defecto
scheme: http
basic_auth: # la cadena de seguridad de 07-01
username: prometheus
password_file: /etc/prometheus/secretos/clave
scrape_interval: 15s
scrape_timeout: 10s # debe ser menor que el intervalo
static_configs:
- targets: ["ciclourbana-1:8081", "ciclourbana-2:8081", "ciclourbana-3:8081"]
labels:
entorno: prod
- job_name: postgres
static_configs: [{ targets: ["postgres-exporter:9187"] }]
- job_name: prometheus # Prometheus se vigila a si mismo
static_configs: [{ targets: ["localhost:9090"] }]Las decisiones que importan. metrics_path es obligatorio porque Prometheus busca /metrics por defecto y Actuator publica en /actuator/prometheus: es el error de configuración número uno. El intervalo de 15 segundos es un compromiso —más frecuente da más resolución y más almacenamiento; por debajo de 10 s rara vez compensa, y por encima de 60 s se pierden los picos cortos—. scrape_timeout menor que scrape_interval, o las recogidas se solapan. La autenticación es la contrapartida de haber asegurado Actuator, con la contraseña en un fichero y no en el YAML. Y el puerto 8081, que es el de gestión: Prometheus nunca toca el puerto del tráfico de los ciudadanos.
- Descubrimiento de objetivos en Kubernetes
La lista estática deja de servir en cuanto el HPA de 08-04 escala de tres a siete réplicas: los pods nuevos no aparecerían. La solución es el descubrimiento dinámico:
- job_name: ciclourbana-k8s
kubernetes_sd_configs:
- role: pod
relabel_configs:
# 1. Solo los pods que se anuncian con la anotacion
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: "true"
# 2. La ruta viene de la anotacion del pod
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
# 3. El puerto tambien
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
# 4. Etiquetas utiles para consultar despues
- source_labels: [__meta_kubernetes_pod_name]
target_label: podY en el Deployment de 08-04, la plantilla del pod se anota:
template:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: "/actuator/prometheus"
prometheus.io/port: "8081"El mecanismo es elegante: Prometheus pregunta a la API de Kubernetes qué pods existen y el relabel_configs filtra y transforma esa lista. Cada pod nuevo que nazca con esas anotaciones empieza a ser recogido en el siguiente ciclo, y cada pod que muera deja de serlo, sin tocar la configuración. Ese es el motivo real por el que Prometheus encaja tan bien con Kubernetes.
- Retención y almacenamiento
Prometheus guarda los datos en su propia base de series temporales, en disco local, con bloques de dos horas comprimidos. Los parámetros de arranque:
--storage.tsdb.retention.time=30d --storage.tsdb.retention.size=50GB --storage.tsdb.path=/prometheus
Una estimación útil: cada muestra ocupa aproximadamente 1-2 bytes una vez comprimida. Con 15.000 series y una recogida cada 15 segundos, salen unos 86 millones de muestras al día, es decir, del orden de 100-170 MB diarios y unos 3-5 GB al mes. La conclusión operativa: el consumo depende del número de series, no del tráfico, lo que devuelve otra vez a la cardinalidad de 09-03 —duplicar las etiquetas duplica el disco y la memoria, aunque el número de peticiones no cambie—.
Treinta días bastan para operar. Para informes anuales al ayuntamiento hace falta almacenamiento de larga duración —Thanos, Cortex o Mimir—, que se sitúan detrás de Prometheus y guardan los bloques en almacenamiento de objetos. Y un aviso importante: el volumen de Prometheus debe ser persistente (un PersistentVolumeClaim en Kubernetes); si vive en el sistema de ficheros del contenedor, cada reinicio borra la historia y volvemos al problema del apartado 1.
- PromQL: lo que hay que saber
Selectores y comparadores. La forma básica es el nombre de la métrica con un filtro de etiquetas entre llaves:
http_server_requests_seconds_count{entorno="prod", uri="/api/v1/alquileres"}
http_server_requests_seconds_count{status!="200"} # distinto de
http_server_requests_seconds_count{status=~"5.."} # coincide con la expresion
http_server_requests_seconds_count{uri!~"/actuator.*"} # no coincideLos cuatro comparadores son =, !=, =~ y !~; los dos últimos son expresiones regulares ancladas a la cadena completa, así que status=~"5.." capta exactamente los 5xx.
rate(): la función más importante. Un contador solo crece, así que su valor absoluto no dice nada útil —«hemos servido 18.422 alquileres desde que arrancó el proceso» no se puede comparar con otra instancia ni con ayer—. Lo que interesa es a qué ritmo crece:
rate calcula el incremento por segundo dentro de la ventana y, crucialmente, maneja los reinicios: si el contador se pone a cero porque la aplicación se reinició, rate lo detecta y no produce un valor negativo absurdo. Es exactamente lo que hace usable el modelo de contadores monótonos.
irate() usa solo los dos últimos puntos: reacciona muy rápido y es muy ruidoso. La regla práctica: rate para gráficas y alertas, irate solo para depurar un pico en directo. Y una condición que se olvida: la ventana debe contener al menos cuatro muestras, así que con recogida cada 15 s, nunca menos de [1m].
Agregación. Los operadores (sum, avg, max, min, count, topk) colapsan series:
sum(rate(http_server_requests_seconds_count[5m])) # total del servicio
sum by (uri) (rate(http_server_requests_seconds_count[5m])) # desglosado por endpoint
sum without (instance) (rate(...)) # suma las 3 replicas
topk(5, sum by (uri) (rate(http_server_requests_seconds_count[5m]))) # los 5 mas usadossum by (...) conserva solo las etiquetas nombradas; sum without (...) conserva todas menos esas. La segunda es a menudo más práctica: without (instance) agrega las réplicas manteniendo el resto del contexto.
histogram_quantile(): el p95 real. Aquí se cobra la decisión de 09-03 de usar histogramas:
Se lee de dentro afuera: rate sobre las cubetas da el ritmo de observaciones por cubeta; sum by (le, uri) suma las tres instancias conservando el umbral y el endpoint; histogram_quantile interpola el percentil. El le en el by es obligatorio: sin él se destruye la estructura del histograma y el resultado no significa nada. Y este es el p95 agregado que era imposible con percentiles calculados en la JVM.
Otras funciones útiles. increase(x[1h]) da el incremento total en la ventana —increase es rate × segundos, y sirve para «cuántos alquileres en la última hora»—. avg_over_time(x[10m]) promedia un gauge en el tiempo. changes(x[1h]) cuenta cambios de valor, útil para detectar reinicios. Y absent(x) vale 1 cuando la serie no existe, que es la forma de alertar de que algo dejó de publicarse.
Operaciones entre series. Se pueden dividir dos consultas si sus etiquetas casan, que es como se calcula una proporción:
sum(rate(http_server_requests_seconds_count{outcome="SERVER_ERROR"}[5m]))
/
sum(rate(http_server_requests_seconds_count[5m]))Con sum(...) en ambos lados el resultado es un escalar y no hay problema de emparejamiento; sin agregar, hay que usar on(...) o ignoring(...) para indicar por qué etiquetas casan las series.
- Las consultas de CicloUrbana, una a una
| Qué responde | Consulta PromQL |
|---|---|
| Peticiones por segundo, por endpoint | sum by (uri) (rate(http_server_requests_seconds_count{entorno="prod"}[5m])) |
| Tasa de error 5xx (proporción) | sum(rate(http_server_requests_seconds_count{outcome="SERVER_ERROR"}[5m])) / sum(rate(http_server_requests_seconds_count[5m])) |
| Latencia p95 por endpoint | histogram_quantile(0.95, sum by (le, uri) (rate(http_server_requests_seconds_bucket[5m]))) |
| Latencia p99 del inicio de alquiler | histogram_quantile(0.99, sum by (le) (rate(http_server_requests_seconds_bucket{uri="/api/v1/alquileres"}[5m]))) |
| Alquileres por minuto y tarifa | sum by (tarifa) (rate(ciclourbana_alquileres_iniciados_total[5m])) * 60 |
| Alquileres en la última hora | sum(increase(ciclourbana_alquileres_iniciados_total[1h])) |
| Bicicletas disponibles por estación | ciclourbana_bicicletas_disponibles{entorno="prod"} |
| Estaciones vacías ahora | count(ciclourbana_bicicletas_disponibles == 0) |
| Uso del pool de HikariCP | hikaricp_connections_active / hikaricp_connections_max |
| Hilos esperando conexión | hikaricp_connections_pending |
| Pausas de GC (tiempo en pausa por segundo) | rate(jvm_gc_pause_seconds_sum[5m]) |
| Memoria de heap usada | sum by (instance) (jvm_memory_used_bytes{area="heap"}) |
| Tasa de aciertos de caché (09-02) | sum by (cache) (rate(cache_gets_total{result="hit"}[5m])) / sum by (cache) (rate(cache_gets_total[5m])) |
| Duración mediana de un trayecto | histogram_quantile(0.5, sum by (le) (rate(ciclourbana_alquiler_duracion_bucket[30m]))) |
| Instancias caídas | up{job="ciclourbana"} == 0 |
| Estado del cortacircuitos (07-06) | resilience4j_circuitbreaker_state{state="open"} == 1 |
Cinco de ellas merecen comentario porque encierran una idea:
La tasa de error se expresa como proporción, no como número absoluto: 20 errores por minuto es una catástrofe con 100 peticiones y ruido con 100.000. Un detalle práctico: en el numerador conviene sumar outcome="SERVER_ERROR" y no status=~"5..", porque outcome ya distingue los errores nuestros de los 4xx del cliente —un 409 por intentar un segundo alquiler es la regla de negocio funcionando, no un fallo—.
El p95 por endpoint es la consulta que justifica todo el módulo: agrega las tres instancias, conserva el desglose por endpoint y da el número que se prometió como SLO en 09-03.
Los alquileres por minuto y tarifa son el pulso del negocio, y el * 60 es solo para leerlos en unidades humanas: rate siempre devuelve por segundo.
El uso del pool es una utilización (USE), y basta comparar con 0,8. hikaricp_connections_pending es la saturación, y su umbral es cero.
La tasa de aciertos de caché cierra 09-02: la regla del by (cache) en numerador y denominador hace que la división case serie con serie y dé un valor por caché.
- Levantar la pila con
docker-compose
docker-composeJunto al docker-compose.yml de 07-04, un fichero de observabilidad:
# docker-compose.observabilidad.yml
services:
prometheus:
image: prom/prometheus:v2.53.0
container_name: ciclourbana-prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=30d
- --web.enable-lifecycle # recarga la config con POST /-/reload
volumes:
- ./observabilidad/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./observabilidad/reglas:/etc/prometheus/reglas:ro
- prometheus-datos:/prometheus
ports: ["9090:9090"]
networks: [red-ciclourbana]
alertmanager:
image: prom/alertmanager:v0.27.0
volumes: ["./observabilidad/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro"]
ports: ["9093:9093"]
networks: [red-ciclourbana]
grafana:
image: grafana/grafana:11.1.0
container_name: ciclourbana-grafana
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:?falta GRAFANA_PASSWORD}
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- ./observabilidad/grafana/provisioning:/etc/grafana/provisioning:ro
- grafana-datos:/var/lib/grafana
ports: ["3000:3000"]
depends_on: [prometheus]
networks: [red-ciclourbana]
postgres-exporter:
image: prometheuscommunity/postgres-exporter:v0.15.0
environment:
DATA_SOURCE_NAME: "postgresql://ciclourbana:${POSTGRES_PASSWORD}@postgres:5432/ciclourbana?sslmode=disable"
networks: [red-ciclourbana]
volumes:
prometheus-datos:
grafana-datos:Se arranca junto al principal con docker compose -f docker-compose.yml -f docker-compose.observabilidad.yml up -d. Cuatro detalles: los volúmenes con nombre conservan la historia y los cuadros de mando entre reinicios; --web.enable-lifecycle permite recargar reglas sin reiniciar; la contraseña de Grafana es obligatoria —dejar admin/admin expuesto es un clásico—; y el postgres_exporter trae las métricas de la base de datos que en 09-01 había que consultar a mano con pg_stat_statements.
- Grafana: origen de datos y cuadros de mando importados
Grafana se configura por interfaz, pero la configuración que sobrevive es la aprovisionada por fichero, versionada en el repositorio:
# observabilidad/grafana/provisioning/datasources/prometheus.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
jsonData:
timeInterval: 15s # debe coincidir con el scrape_intervalImportar antes que construir. Antes de dibujar un solo panel, conviene importar dos cuadros de mando de la comunidad, indicando su identificador en Dashboards → Import:
| Id | Cuadro de mando | Qué aporta |
|---|---|---|
| 4701 | JVM (Micrometer) | Memoria por área, GC, hilos, clases, CPU. El más útil de todos |
| 12900 | Spring Boot 2.1+ Statistics | Peticiones HTTP, latencias, estado general |
| 11378 | Spring Boot APM | Vista con HikariCP y endpoints |
| 9628 | PostgreSQL Database | Métricas del postgres_exporter |
La comparación honesta: importar da el 80 % del valor en cinco minutos, cubre todo lo genérico —JVM, GC, pool, HTTP— y está mucho mejor construido de lo que uno haría a mano. Lo que no puede darte es lo específico de Ribalta: alquileres por tarifa, bicicletas por estación, aciertos de caché. La estrategia sensata es importar lo genérico y construir un único cuadro de mando propio con lo de negocio, en lugar de reinventar la gráfica de memoria de la JVM.
Un aviso: los cuadros importados suponen nombres de etiquetas concretos, casi siempre application y instance. Si tus etiquetas comunes de 09-03 no incluyen application, los paneles saldrán vacíos y parecerá que algo está roto.
- El cuadro de mando de CicloUrbana, panel a panel
Un buen cuadro de mando responde en cinco segundos a la pregunta «¿está bien el servicio?» y permite profundizar después. Se organiza en filas, de lo que ve el ciudadano a lo que consume la máquina:
Fila 1 — Estado del servicio (RED). Cuatro paneles: Tasa (sum(rate(http_server_requests_seconds_count[5m])), gráfica de líneas), Errores (la proporción del apartado 11, en stat con umbrales de color: verde < 0,5 %, ámbar < 1 %, rojo por encima), Latencia p50/p95/p99 (tres consultas superpuestas en una gráfica, con el SLO de 800 ms dibujado como línea de umbral) y Disponibilidad (avg(up{job="ciclourbana"}), en stat).
Fila 2 — Por endpoint. Una tabla con topk(10, sum by (uri) (rate(...))) y una gráfica del p95 por uri. Esta fila responde «¿qué endpoint es el culpable?» inmediatamente después de que la fila 1 diga que algo va mal.
Fila 3 — JVM y recursos (USE). Memoria de heap tras GC, rate(jvm_gc_pause_seconds_sum[5m]), hilos vivos, hikaricp_connections_active frente a max en la misma gráfica, y hikaricp_connections_pending como stat con umbral en 1. Es la fila de las causas.
Fila 4 — Negocio. Alquileres por minuto y tarifa (barras apiladas), bicicletas disponibles por estación (mapa de calor o tabla ordenada ascendente, para ver primero las estaciones vacías), duración mediana del trayecto y tasa de aciertos por caché. Es la fila que el ayuntamiento entiende sin traducción, y la que detecta un despliegue que rompió el alquiler.
Variables de plantilla. En lugar de duplicar el cuadro por entorno, se definen variables: entorno como query label_values(up, entorno), e instancia como label_values(up{entorno="$entorno"}, instance) con la opción All activada. Después, todas las consultas usan {entorno="$entorno", instance=~"$instancia"}. Un solo cuadro sirve para pre y prod, y permite aislar una réplica sospechosa con un desplegable.
Buenas prácticas de visualización, que separan un cuadro útil de uno decorativo: unidades siempre (segundos, peticiones/s, bytes, por ciento) porque un número sin unidad induce a error; los percentiles en la misma gráfica que la mediana, para ver la separación entre la experiencia típica y la mala; umbrales dibujados como línea, de modo que el SLO sea visible y no un dato de la documentación; escala logarítmica en latencias, que suelen abarcar órdenes de magnitud; nada de gráficas de tarta para series temporales; y como máximo unos 12 paneles, porque un cuadro de 40 no se mira nunca.
- Reglas de alerta
Las reglas se evalúan en Prometheus, cada evaluation_interval, y viven en los ficheros de rule_files:
# observabilidad/reglas/ciclourbana.yml
groups:
- name: ciclourbana-servicio
interval: 30s
rules:
- alert: AplicacionCaida
expr: up{job="ciclourbana"} == 0
for: 2m
labels: { severity: critica, equipo: plataforma }
annotations:
summary: "Instancia {{ $labels.instance }} no responde"
description: "Prometheus no consigue recoger metricas desde hace 2 minutos."
runbook_url: "https://wiki.ribalta.es/runbooks/ciclourbana-caida"
- alert: TasaDeErrorAlta
expr: |
sum(rate(http_server_requests_seconds_count{outcome="SERVER_ERROR"}[5m]))
/ sum(rate(http_server_requests_seconds_count[5m])) > 0.01
for: 5m
labels: { severity: critica, equipo: plataforma }
annotations: { summary: "Tasa de error del {{ $value | humanizePercentage }}" }
- alert: LatenciaP99Degradada
expr: |
histogram_quantile(0.99,
sum by (le) (rate(http_server_requests_seconds_bucket{uri="/api/v1/alquileres"}[5m]))
) > 1
for: 10m
labels: { severity: aviso, equipo: plataforma }
annotations: { summary: "p99 de inicio de alquiler por encima de 1 s" }
- alert: PoolDeConexionesSaturado
expr: hikaricp_connections_pending > 5
for: 5m
labels: { severity: critica, equipo: plataforma }
annotations: { summary: "{{ $value }} hilos esperando conexion en {{ $labels.instance }}" }
- alert: SinAlquileresEnHoraPunta
expr: |
sum(rate(ciclourbana_alquileres_iniciados_total[10m])) == 0
and on() (hour() >= 6 < 9) # 7-10 h en horario de Ribalta (UTC+1)
for: 10m
labels: { severity: critica, equipo: producto }
annotations: { summary: "Ningun alquiler iniciado en 10 min en hora punta" }Los cuatro campos de una regla y su papel. expr es una consulta PromQL: la alerta está activa mientras devuelva alguna serie. for es el elemento más subestimado: exige que la condición se mantenga durante ese tiempo antes de disparar, y es lo que elimina el 90 % del ruido —un pico de dos segundos no despierta a nadie—. labels clasifica la alerta y es lo que Alertmanager usará para enrutar. Y annotations es el mensaje para el humano, con plantillas Go ({{ $value }}, {{ $labels.instance }}) y, muy recomendable, un runbook_url: a las cuatro de la mañana, un enlace a los pasos de diagnóstico vale más que cualquier gráfica.
La última regla merece detenimiento porque es la más valiosa y la más difícil de escribir: alerta de que no está pasando algo que debería pasar. Requiere acotar el horario —hour() devuelve la hora UTC, de ahí el desfase— para que no dispare a las cuatro de la mañana, cuando cero alquileres es lo normal en Ribalta.
- Alertmanager: rutas, agrupación y silencios
Prometheus dispara; Alertmanager decide qué hacer:
route:
group_by: [alertname, entorno] # agrupa las 3 instancias en un solo mensaje
group_wait: 30s # espera por si llegan companeras
group_interval: 5m # cada cuanto informa de novedades del grupo
repeat_interval: 4h # cada cuanto insiste con lo ya notificado
receiver: slack-plataforma
routes:
- matchers: [severity="critica"]
receiver: guardia
continue: true # ademas, sigue al receptor por defecto
- matchers: [equipo="producto"]
receiver: slack-producto
inhibit_rules:
- source_matchers: [alertname="AplicacionCaida"]
target_matchers: [severity="aviso"]
equal: [instance] # si esta caida, callar sus avisos derivados
receivers:
- name: slack-plataforma
slack_configs:
- api_url_file: /etc/alertmanager/secretos/slack
channel: "#ciclourbana-alertas"
title: "{{ .CommonAnnotations.summary }}"
- name: guardia
pagerduty_configs: [{ routing_key_file: /etc/alertmanager/secretos/pagerduty }]
- name: slack-producto
slack_configs:
- api_url_file: /etc/alertmanager/secretos/slack
channel: "#ciclourbana-producto"Tres mecanismos que evitan el infierno de notificaciones. La agrupación convierte tres alertas idénticas de las tres réplicas en un solo mensaje. Las reglas de inhibición callan las alertas derivadas: si la aplicación está caída, no tiene sentido avisar además de que su latencia es rara. Y los silencios, que se crean desde la interfaz de Alertmanager con una duración y un motivo, sirven para una ventana de mantenimiento conocida —una migración de Flyway pesada, por ejemplo—; conviene que sean siempre temporales, porque un silencio permanente es una alerta borrada sin decirlo.
- Buenas prácticas de alertado
Alertar sobre síntomas, no sobre causas. «La latencia p99 supera 1 s» es un síntoma que el ciudadano nota; «la CPU está al 85 %» es una causa que puede ser perfectamente normal. Si se alerta de causas, el equipo recibe avisos de cosas que no afectan a nadie y deja de mirarlos, que es exactamente cómo se pierde una alerta importante.
Cada alerta debe exigir una acción humana e inmediata. La pregunta que decide es: si esto llega a las cuatro de la mañana, ¿hay algo que hacer ahora? Si la respuesta es «mirarlo mañana», no es una alerta: es un panel del cuadro de mando o, como mucho, un aviso en horario laboral. La distinción severity: critica frente a severity: aviso del apartado 15 es exactamente eso, con destinos distintos.
Combatir la fatiga de alertas. Un equipo que recibe treinta notificaciones al día deja de leerlas en dos semanas, y a partir de ahí el sistema de alertas no existe aunque siga funcionando. Las herramientas: for generoso, agrupación, inhibición y borrar sin piedad las alertas que nunca han llevado a una acción.
Poner siempre un runbook_url. Quien recibe la alerta a las cuatro de la mañana puede no ser quien escribió el código. Tres líneas de «comprueba esto, mira aquello, si es esto haz lo otro» cambian por completo el resultado.
Alertar también sobre la ausencia. Los sistemas rotos a veces dejan de emitir en lugar de emitir errores. absent() y la regla de «sin alquileres en hora punta» cubren ese hueco.
Probar las alertas. Prometheus trae promtool test rules, que permite escribir pruebas unitarias de las reglas con series simuladas. Una alerta que nunca se ha visto dispararse es una hipótesis, no una alerta.
- Alternativas gestionadas
| Solución | Modelo | Ventaja principal | Inconveniente | Cuándo elegirla |
|---|---|---|---|---|
| Prometheus + Grafana propios | Autogestionado | Control total, sin coste de licencia, estándar | Hay que operarlo y dimensionarlo | CicloUrbana: volumen moderado y equipo capaz |
| Grafana Cloud | Gestionado | Mismo PromQL y mismos cuadros; sin operar nada | Coste por serie ingerida | Equipo pequeño que quiere lo mismo sin mantenerlo |
| Datadog | Gestionado, push con agente | Métricas, logs y APM integrados y muy pulidos | El más caro, con diferencia; efecto de bloqueo | Empresa que prioriza integración sobre coste |
| New Relic | Gestionado | APM muy fuerte, instrumentación automática | Coste; modelo de datos propio | Diagnóstico profundo de aplicaciones |
| Elastic APM | Ambos | Métricas, logs y trazas en la misma pila que 09-05 | Operar Elasticsearch no es barato | Ya se usa ELK para logs |
| CloudWatch (08-03) | Gestionado en AWS | Integrado con ECS, RDS y ALB; sin infraestructura | PromQL no; consultas y paneles pobres | Todo en AWS y necesidades sencillas |
El criterio. La pregunta no es cuál es mejor, sino quién va a operar esto y con qué presupuesto. Prometheus autogestionado es gratuito en licencias y caro en atención; los gestionados invierten los términos. Para CicloUrbana —tres instancias, unas 15.000 series, un equipo que ya opera Kubernetes— la pila propia es la elección correcta, con una salida natural hacia Grafana Cloud si el mantenimiento empieza a pesar, porque el lenguaje de consulta y los cuadros de mando se conservan. Ese es, de hecho, el argumento estratégico más fuerte a favor del ecosistema Prometheus: no encierra.
Y una nota que anticipa el final del módulo: OpenTelemetry se está convirtiendo en el estándar común para emitir métricas, logs y trazas, con un protocolo (OTLP) que casi todos los destinos aceptan. Prometheus ya sabe ingerir OTLP, Micrometer tiene puente hacia él y los backends comerciales lo soportan. Es la dirección hacia la que converge todo, y es la que tomaremos en 09-06 para la trazabilidad distribuida.
Errores Comunes y Consejos
Olvidar metrics_path: /actuator/prometheus. Prometheus busca /metrics, el objetivo aparece en rojo y todo lo demás parece roto. Es el fallo número uno: compruébalo en Status → Targets.
Usar el valor absoluto de un contador. ciclourbana_alquileres_iniciados_total sin rate() da un número que solo crece y que se pone a cero en cada despliegue. Sobre un contador, siempre rate o increase.
Olvidar le en el sum by de histogram_quantile. El resultado es un número sin significado que además parece plausible. Siempre sum by (le, ...).
Ventana de rate demasiado corta. Con recogida cada 15 s, rate(x[30s]) no tiene muestras suficientes y produce huecos. Regla: la ventana, al menos cuatro veces el intervalo de recogida.
Alertar sin for. Un pico de dos segundos despierta a alguien. Casi toda alerta necesita for de entre 2 y 15 minutos.
Alertar sobre causas. CPU al 85 %, memoria al 70 %, GC frecuente: son diagnósticos, no problemas. Alerta sobre lo que sufre el usuario y usa esas otras métricas para investigar.
No persistir el volumen de Prometheus ni el de Grafana. Un reinicio borra la historia y los cuadros de mando construidos a mano. Volúmenes con nombre y, mejor aún, cuadros aprovisionados por fichero en el repositorio.
Dejar Grafana con admin/admin accesible. Los cuadros de mando revelan volúmenes de negocio, endpoints y versiones. Contraseña obligatoria y registro deshabilitado.
Consejo: aprovisiona por fichero los orígenes de datos, los cuadros y las reglas, y versiónalos en el repositorio junto al código. Un cuadro de mando construido a mano en la interfaz es infraestructura sin control de versiones.
Consejo: empieza importando el 4701 y el 12900 y construye un único cuadro propio con lo de negocio de Ribalta. Reinventar la gráfica de memoria de la JVM no aporta nada.
Consejo: escribe la alerta a la vez que la métrica. Una métrica de negocio sin alerta se convierte en un panel que nadie mira.
Ejercicios
Ejercicio 1: escribir las consultas
Escribe la consulta PromQL para cada pregunta del ayuntamiento y explica cada función que uses: (a) cuántos alquileres se han iniciado hoy en la estación «Universidad»; (b) qué porcentaje de las peticiones a /api/v1/estaciones se responde en menos de 300 ms —el SLO de 09-03—; (c) qué instancia consume más memoria de heap; (d) la tasa de aciertos de la caché estaciones en la última hora; (e) si alguna réplica lleva más de cinco minutos con el cortacircuitos de pagos abierto.
Ejercicio 2: la alerta que no dispara
El equipo definió esta regla y, durante una caída real de 20 minutos en la que la API devolvió 500 a todo, no llegó ninguna notificación. Encuentra los tres motivos posibles y explica cómo verificarías cada uno.
- alert: ErroresEnLaApi
expr: http_server_requests_seconds_count{status="500"} > 100
for: 30m
labels: { severity: info }
annotations: { summary: "Errores en la API" }Ejercicio 3: diseñar el cuadro de mando de guardia
El ayuntamiento pide una pantalla que se proyecte en la sala de operaciones y que permita a alguien sin conocimientos técnicos saber si la red de Ribalta funciona. Diseña ese cuadro de mando: elige seis paneles como máximo, indica la consulta PromQL de cada uno, el tipo de visualización, las unidades y los umbrales de color, y justifica qué has dejado fuera y por qué.
Soluciones
Solución 1.
(a) Alquileres de hoy en «Universidad»:
increase da el incremento total del contador en la ventana —a diferencia de rate, que da el ritmo por segundo—, y sum colapsa las tres réplicas y los tres tipos de tarifa en un único número. Advertencia honesta: [24h] es «las últimas 24 horas», no «desde medianoche»; para eso hay que ajustar el rango del panel o usar una regla de registro diaria.
(b) Porcentaje bajo 300 ms:
sum(rate(http_server_requests_seconds_bucket{uri="/api/v1/estaciones", le="0.3"}[5m]))
/
sum(rate(http_server_requests_seconds_count{uri="/api/v1/estaciones"}[5m]))Aquí se cobra la configuración slo: 300ms de 09-03: como las cubetas son acumulativas, la de le="0.3" contiene exactamente las peticiones que cumplieron el objetivo, y dividirla por el total da la proporción de cumplimiento. Es la forma correcta de medir un SLO, mejor que histogram_quantile, porque responde a la pregunta contractual: «qué porcentaje cumplió», no «cuál es el percentil». Requisito imprescindible: que exista una cubeta exactamente en 0.3.
(c) Instancia con más heap:
sum by (instance) agrega los distintos pools de memoria (Eden, Survivor, Old) de cada JVM, y topk(1, ...) devuelve la mayor conservando la etiqueta instance, que es la respuesta buscada.
(d) Tasa de aciertos de la caché:
sum(rate(cache_gets_total{cache="estaciones", result="hit"}[1h]))
/
sum(rate(cache_gets_total{cache="estaciones"}[1h]))El denominador incluye aciertos y fallos porque no filtra por result. Si el resultado está por debajo de 0,2, procede revisar la clave o la autoinvocación de 09-02; en un panel conviene mostrarlo como porcentaje con umbrales.
(e) Cortacircuitos abierto más de cinco minutos:
min_over_time sobre la ventana vale 1 solo si la serie ha valido 1 durante toda la ventana, lo que expresa exactamente «lleva cinco minutos abierto» y no «se abrió en algún momento». La alternativa idiomática es expr: ... == 1 con for: 5m en una regla de alerta, que es como se escribiría en producción.
Solución 2.
Motivo 1 — la expresión usa un contador sin rate. http_server_requests_seconds_count es acumulativo desde el arranque, así que > 100 se cumple siempre en cualquier aplicación con tráfico... y por eso, paradójicamente, no distingue una caída de un día normal. Además, si la caída provocó reinicios, el contador se puso a cero y bajó de 100. Lo correcto es una proporción con rate. Cómo verificarlo: ejecutar la expresión en la interfaz de Prometheus y ver que devuelve un valor grande incluso en un momento sano.
Motivo 2 — for: 30m es más largo que la incidencia. La caída duró 20 minutos, así que la alerta estuvo en estado pending todo ese tiempo y se resolvió antes de pasar a firing: nunca llegó a Alertmanager. Cómo verificarlo: mirar el estado histórico en Alerts o consultar la métrica ALERTS{alertstate="pending"}. Regla práctica: el for debe ser bastante menor que la duración del incidente que se quiere detectar; 5 minutos es un valor razonable.
Motivo 3 — severity: info probablemente no se enruta a ningún receptor. Las rutas del apartado 16 encaminan critica a la guardia y equipo=producto a su canal; una etiqueta que no casa con ninguna ruta específica cae en el receptor por defecto, y si el equipo lo tiene apuntado a un canal que nadie lee, el efecto práctico es el mismo que no alertar. Cómo verificarlo: usar amtool config routes test severity=info o la herramienta de enrutado de la interfaz de Alertmanager.
Además, la anotación «Errores en la API» no dice cuántos, ni dónde, ni qué hacer, y no hay runbook_url. Versión corregida:
- alert: TasaDeErrorAlta
expr: |
sum(rate(http_server_requests_seconds_count{outcome="SERVER_ERROR"}[5m]))
/ sum(rate(http_server_requests_seconds_count[5m])) > 0.01
for: 5m
labels: { severity: critica, equipo: plataforma }
annotations:
summary: "Tasa de error {{ $value | humanizePercentage }} en {{ $labels.entorno }}"
runbook_url: "https://wiki.ribalta.es/runbooks/tasa-error"Solución 3.
Principio de diseño: la audiencia no es técnica, así que cada panel debe responder una pregunta en lenguaje de servicio, con color como información principal y el número como detalle. Seis paneles:
| Panel | Consulta | Visualización | Unidad y umbrales |
|---|---|---|---|
| ¿Está en marcha? | avg(up{job="ciclourbana"}) * 100 |
Stat grande | %; rojo < 100, verde = 100 |
| Alquileres por minuto | sum(rate(ciclourbana_alquileres_iniciados_total[5m])) * 60 |
Gráfica de líneas | alq/min; el pulso del servicio |
| Peticiones con error | proporción SERVER_ERROR × 100 |
Stat con fondo de color | %; verde < 0,5, ámbar < 1, rojo ≥ 1 |
| Tiempo de respuesta (p95) | histogram_quantile(0.95, sum by (le) (rate(http_server_requests_seconds_bucket[5m]))) |
Gráfica con línea de umbral en 0,3 s | segundos; log |
| Bicicletas disponibles en la red | sum(ciclourbana_bicicletas_disponibles) y count(... == 0) |
Stat doble | unidades; rojo si hay > 5 estaciones vacías |
| Estaciones con menos bicicletas | bottomk(5, ciclourbana_bicicletas_disponibles) |
Tabla ordenada | unidades |
Qué se deja fuera y por qué. Memoria de heap, pausas de GC, hilos de Tomcat, uso del pool, tasa de aciertos de caché y latencia por endpoint: todo eso son causas, no síntomas, y su público es el equipo técnico. Un operario del ayuntamiento no puede hacer nada con «GC pause 180 ms», y su presencia solo consigue que los paneles que sí importan se pierdan entre ruido. Viven en el cuadro de mando técnico del apartado 14 y en el 4701 importado, a un clic de distancia.
Dos decisiones adicionales. La segunda fila del cuarto panel dibuja el SLO como línea de umbral, para que «va lento» sea visible sin interpretar números. Y el panel de alquileres por minuto es deliberadamente el más grande: es el único que detecta un servicio funcionalmente roto con todos los indicadores técnicos en verde, el caso que en 09-03 identificamos como el que las métricas técnicas no ven nunca. Conviene además fijar el rango temporal por defecto en 6 horas y activar el refresco automático cada 30 segundos, coherente con el intervalo de recogida.
Conclusión
Las métricas de CicloUrbana han salido del proceso. Entiendes por qué hacía falta un sistema externo —persistencia, historia, agregación entre instancias y evaluación continua— y por qué Prometheus va a buscar los datos en lugar de esperarlos, con la ventaja decisiva de que «la instancia está caída» se convierte en una consulta (up == 0) en lugar de una inferencia, y con el Pushgateway como excepción acotada para procesos efímeros. Sabes exponer las métricas con micrometer-registry-prometheus en el puerto de gestión de 07-01, y leer el formato de texto: # HELP, # TYPE, las series con sus etiquetas y los sufijos _count, _sum y _bucket con la etiqueta le acumulativa, que son la traducción literal de los histogramas que configuraste en 09-03.
Tienes un prometheus.yml completo para Ribalta —con el metrics_path que casi todo el mundo olvida, la autenticación que exige la cadena de seguridad de Actuator y los intervalos elegidos con criterio— y sabes sustituir la lista estática por descubrimiento en Kubernetes con anotaciones y relabel_configs, de modo que cada réplica que cree el HPA de 08-04 empiece a medirse sola. Conoces el coste real del almacenamiento y su dependencia del número de series, no del tráfico, que devuelve una vez más a la cardinalidad.
Y sabes PromQL de verdad: selectores con sus cuatro comparadores, rate siempre sobre un contador y por qué irate solo sirve para depurar, sum by y sum without para agregar las tres instancias, histogram_quantile con el le obligatorio para obtener el p95 real que era imposible con percentiles calculados en la JVM, más increase, avg_over_time, absent y las operaciones entre series que producen proporciones. Con esas piezas has escrito las consultas concretas de CicloUrbana: tráfico y latencia por endpoint, tasa de error como proporción, alquileres por minuto y tarifa, bicicletas por estación, pool de HikariCP, pausas de GC y aciertos de caché.
Del lado operativo, tienes la pila levantada con docker-compose junto a la de 07-04, con volúmenes persistentes y el postgres_exporter; Grafana aprovisionado por fichero, con la estrategia de importar lo genérico (4701, 12900) y construir solo lo de negocio; el cuadro de mando de Ribalta organizado en cuatro filas de lo que ve el ciudadano a lo que consume la máquina, con variables de plantilla por entorno e instancia; y las reglas de alerta con expr, for, labels y annotations, incluida la más valiosa y la más difícil de escribir —la que avisa de que no está pasando algo que debería pasar—. Alertmanager agrupa, inhibe y silencia, y tienes las reglas de oro del alertado: síntomas y no causas, cada alerta con una acción posible, runbook_url siempre, y borrar sin piedad lo que solo produce fatiga.
Con esto, dos de los tres pilares están en pie. Las métricas dicen que algo va mal y desde cuándo; los cuadros de mando lo muestran y las alertas lo avisan. Pero cuando la alerta llega a las cuatro de la mañana diciendo que la tasa de error del inicio de alquiler es del 3 %, la siguiente pregunta —qué ha fallado exactamente, en qué petición, con qué usuario, con qué mensaje— no la responde ninguna gráfica. La responde el pilar que llevamos usando desde la primera lección sin haberlo tratado nunca en serio: el registro de eventos. La siguiente lección, Gestión de Registros y Logs, lo convierte en una herramienta de verdad: niveles con criterio, logback-spring.xml por perfil, logs estructurados en JSON, el trazaId del FiltroTraza de 03-06 propagado y consultable, lo que jamás debe registrarse sobre los ciudadanos de Ribalta, y la agregación centralizada que permite buscar en los logs de las tres instancias como si fueran uno.
Curso de Spring Boot
Módulo 1: Introducción a Spring Boot
- ¿Qué es Spring Boot?
- Configuración de tu Entorno de Desarrollo
- Creando tu Primera Aplicación Spring Boot
- Entendiendo la Estructura del Proyecto
- El Arranque y el Ciclo de Vida de la Aplicación
Módulo 2: Conceptos Básicos de Spring Boot
- Anotaciones de Spring Boot
- Inyección de Dependencias en Spring Boot
- Ámbito y Ciclo de Vida de los Beans
- Configuración de Spring Boot
- Propiedades de Spring Boot
- Autoconfiguración y Starters por Dentro
Módulo 3: Construyendo Servicios Web RESTful
- Introducción a los Servicios Web RESTful
- Creando Controladores REST
- Manejo de Métodos HTTP
- Validación de Datos de Entrada
- DTOs y Mapeo entre Capas
- Manejo de Excepciones en REST
- Documentar la API con OpenAPI
Módulo 4: Acceso a Datos con Spring Boot
- Introducción a Spring Data JPA
- Configuración de Fuentes de Datos
- Creación de Entidades JPA
- Relaciones entre Entidades
- Uso de Repositorios de Spring Data
- Métodos de Consulta en Spring Data JPA
- Transacciones y Gestión de la Persistencia
- Migraciones de Esquema con Flyway
Módulo 5: Seguridad en Spring Boot
- Introducción a Spring Security
- Configuración de Spring Security
- Autenticación y Autorización de Usuarios
- Implementación de Autenticación JWT
- Seguridad a Nivel de Método y Endurecimiento de la API
Módulo 6: Pruebas en Spring Boot
- Introducción a las Pruebas
- Pruebas Unitarias con JUnit
- Simulación con Mockito
- Pruebas de Integración
- Pruebas con Testcontainers
Módulo 7: Funciones Avanzadas de Spring Boot
- Spring Boot Actuator
- Perfiles de Spring Boot
- Tareas Programadas y Ejecución Asíncrona
- Spring Boot con Docker
- Spring Boot y Microservicios
- Comunicación entre Servicios y Tolerancia a Fallos
Módulo 8: Despliegue de Aplicaciones Spring Boot
- Introducción al Despliegue
- Desplegando en Heroku
- Desplegando en AWS
- Desplegando en Kubernetes
- Integración y Entrega Continua
Módulo 9: Rendimiento y Monitoreo
- Ajuste de Rendimiento
- Caché con Spring Cache
- Monitoreo con Spring Boot Actuator
- Uso de Prometheus y Grafana
- Gestión de Registros y Logs
- Trazabilidad Distribuida
