Aurora Libros està construïda, optimitzada i endurida. Falta el que separa una plataforma que funciona d'una plataforma que es pot operar: saber què està passant ara mateix, què va passar ahir a la nit a les 3:14 i per què el contenidor de l'API es va reiniciar quatre vegades dimarts.
Contingut
- La regla d'stdout/stderr, revisitada
- Els drivers de logging
- Configuració global i per servei
- Rotació obligatòria: el càlcul del disc ple
- Logs estructurats en JSON
- Agregació centralitzada: el patró
- Loki, Promtail i Grafana al perfil
observabilitat - Consultes LogQL útils
- Mètriques:
docker statsi els seus límits - L'API de mètriques del daemon
- cAdvisor, node-exporter i Prometheus
- Les mètriques que de debò es vigilen
- Alertes
- L'endpoint de salut ric
- Traces distribuïdes: el pas següent
- La regla d'stdout/stderr, revisitada
Una aplicació en contenidor no escriu fitxers de log: escriu a la sortida estàndard i a la d'error, i deixa que la plataforma faci la resta.
El motiu és concret: el contenidor és efímer. Un fitxer a /var/log/app.log desapareix en recrear-lo, obliga a muntar un volum només per a això, exigeix rotació pròpia i no es pot consultar amb docker logs. Escrivint a stdout, el procés només produeix els esdeveniments; recollir-los, rotar-los i enviar-los on toqui és responsabilitat del driver de logging.
docker compose exec aurora-api sh -c 'ls -la /proc/1/fd/1 /proc/1/fd/2'
# /proc/1/fd/1 -> pipe:[482913]
# /proc/1/fd/2 -> pipe:[482914]Les sortides del procés són canonades cap al daemon, no fitxers. A l'altre extrem, el driver decideix la seva destinació.
- Els drivers de logging
| Driver | Destinació | docker logs |
Quan fer-lo servir |
|---|---|---|---|
json-file |
/var/lib/docker/containers/<id>/*-json.log |
Sí | Per defecte. Vàlid amb rotació configurada |
local |
Format binari propi, comprimit | Sí | Millor rendiment i rotació activada de sèrie |
journald |
systemd-journald del host |
Sí | Hosts amb systemd; s'integra amb journalctl |
syslog |
Servidor syslog local o remot | No | Infraestructura syslog ja existent |
fluentd |
Dimoni Fluentd/Fluent Bit | No | Encaminament flexible cap a diverses destinacions |
gelf |
Graylog / Logstash (UDP) | No | Piles Graylog o ELK |
awslogs, gcplogs |
CloudWatch, Cloud Logging | No | Càrregues natives d'aquell núvol |
none |
Es descarten | No | Contenidors sorollosos sense valor de log |
La limitació marcada com a "No" és important i sorprèn en el pitjor moment: amb syslog, fluentd, gelf o awslogs, docker logs deixa de funcionar. Si el sistema central està caigut, et quedes sense cap via per veure què passa.
L'estratègia habitual evita aquell carreró: deixar json-file o local amb rotació i recollir els fitxers amb un agent extern (Promtail, Fluent Bit, Vector). Així conserves docker logs per al diagnòstic immediat i tens agregació central per a l'històric.
- Configuració global i per servei
El valor per defecte per a tot el host va a /etc/docker/daemon.json, i el que és específic a Compose:
Després de sudo systemctl restart docker, verifica-ho amb docker info --format '{{.LoggingDriver}}'. Compte: canviar el driver només afecta els contenidors creats després; els existents conserven el seu fins que es recreïn.
services:
aurora-api:
logging:
driver: json-file
options: { max-size: "50m", max-file: "5", tag: "{{.Name}}/{{.ID}}" }
aurora-cache:
logging:
driver: none # Redis amb --loglevel notice no aporta res aquí
- Rotació obligatòria: el càlcul del disc ple
Sense rotació, json-file creix sense límit. Fem el càlcul amb Aurora Libros: l'API registra una línia per petició, uns 200 bytes en JSON, amb 50 peticions per segon en hora punta i una mitjana de 15/s al llarg del dia.
Gairebé 8 GB al mes d'un sol servei, en un fitxer que no es trunca mai. Amb quatre serveis i un disc de 40 GB, la plataforma cau per disc ple en menys de dos mesos. I la fallada per disc ple és especialment desagradable: PostgreSQL deixa d'acceptar escriptures, Docker no pot crear contenidors i el mateix sistema de logs no pot registrar el problema.
sudo du -sh /var/lib/docker/containers/*/ | sort -rh | head -2
docker inspect aurora-libros-aurora-api-1 --format '{{.HostConfig.LogConfig}}'
# 2.1G /var/lib/docker/containers/a91f3c.../
# {json-file map[max-file:5 max-size:50m]}Amb max-size: 50m i max-file: 5, el sostre per contenidor són 250 MB, i sempre tens els últims dies. Dos avisos: max-file sense max-size no fa res, i el driver local porta la rotació activada per defecte (100 MB en 5 fitxers), cosa que el converteix en l'opció més segura si te n'oblides de configurar-la.
- Logs estructurats en JSON
Un log en text lliure obliga a escriure expressions regulars per a tot. Un log en JSON es consulta per camps.
// api/src/log.js — registre estructurat mínim, sense dependències
const NIVELLS = { error: 0, warn: 1, info: 2, debug: 3 };
const nivellActual = NIVELLS[process.env.LOG_NIVELL ?? 'info'] ?? 2;
function emetre(nivell, missatge, extra = {}) {
if (NIVELLS[nivell] > nivellActual) return;
const linia = { ts: new Date().toISOString(), nivell, servei: 'aurora-api',
version: process.env.APP_VERSION ?? '1.3.0', missatge, ...extra };
process[nivell === 'error' ? 'stderr' : 'stdout'].write(JSON.stringify(linia) + '\n');
}
module.exports = Object.fromEntries(
Object.keys(NIVELLS).map(n => [n, (m, e) => emetre(n, m, e)])
);// api/src/server.js — identificador de petició i registre d'accés
const { randomUUID } = require('node:crypto');
const log = require('./log');
app.use((req, res, next) => {
req.id = req.get('x-request-id') ?? randomUUID(); // reutilitza el del frontal si arriba
res.set('x-request-id', req.id); // i el retorna al client
const inici = process.hrtime.bigint();
res.on('finish', () => {
const ms = Number(process.hrtime.bigint() - inici) / 1e6;
log.info('peticio', { req_id: req.id, metode: req.method,
ruta: req.route?.path ?? req.path, estat: res.statusCode, ms: Number(ms.toFixed(1)) });
});
next();
});
app.get('/llibres', async (req, res) => {
const enCache = await cache.get('llibres:tots');
log.debug(enCache ? 'cache hit' : 'cache miss', { req_id: req.id, clau: 'llibres:tots' });
if (enCache) return res.json({ origen: 'cache', ...JSON.parse(enCache) });
// ... consulta a PostgreSQL, log.debug('consulta bd', {...}) i escriptura a la memòria cau
});curl -s -o /dev/null http://localhost:8080/api/llibres
docker compose logs aurora-api --tail 2 --no-log-prefix | jq -c{"ts":"2026-08-05T10:14:02.881Z","nivell":"debug","servei":"aurora-api","version":"1.3.0","missatge":"cache miss","req_id":"7f3a...","clau":"llibres:tots"}
{"ts":"2026-08-05T10:14:02.914Z","nivell":"info","servei":"aurora-api","version":"1.3.0","missatge":"peticio","req_id":"7f3a...","metode":"GET","ruta":"/llibres","estat":200,"ms":33.2}El req_id és la peça clau: es propaga per la capçalera x-request-id, el retorna la resposta i apareix a totes les línies d'aquella petició. Quan un usuari reporta un error a les 10:14 amb aquell identificador, recuperes la traça completa de la seva petició amb una sola consulta, en lloc de llegir deu mil línies.
- Agregació centralitzada: el patró
docker logs serveix per a un contenidor d'una màquina. Amb diversos serveis, diversos nodes i contenidors que es recreen, cal que els esdeveniments surtin del host i visquin més que el contenidor que els va produir.
flowchart LR A["aurora-api<br/>stdout JSON"] --> D["Driver json-file<br/>/var/lib/docker/containers"] B["aurora-db"] --> D C["aurora-web"] --> D D --> P["Promtail<br/>(llegeix i etiqueta)"] P --> L["Loki<br/>(índex per etiquetes)"] L --> G["Grafana<br/>consultes LogQL"] M["cAdvisor + node-exporter"] --> PR["Prometheus"] PR --> G PR --> AL["Alertmanager"]
Loki indexa només les etiquetes (servei, nivell, host) i comprimeix la resta, cosa que el fa molt més barat que una pila que indexa cada paraula. Les alternatives: ELK/OpenSearch (Elasticsearch + Logstash + Kibana), més potent en cerca de text complet i força més exigent en recursos; i els serveis gestionats dels núvols.
- Loki, Promtail i Grafana al perfil
observabilitat
observabilitat# compose.observabilitat.yaml — perfil "observabilitat" d'Aurora Libros
services:
loki:
image: grafana/loki:3.3.0
profiles: [observabilitat]
volumes: [loki-dades:/loki]
networks: [observacio]
promtail:
image: grafana/promtail:3.3.0
profiles: [observabilitat]
command: ["-config.file=/etc/promtail/config.yaml"]
volumes:
- ./observabilitat/promtail.yaml:/etc/promtail/config.yaml:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro # només per llegir etiquetes
depends_on: [loki]
networks: [observacio]
prometheus:
image: prom/prometheus:v3.1.0
profiles: [observabilitat]
volumes:
- ./observabilitat/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./observabilitat/alertes.yml:/etc/prometheus/alertes.yml:ro
- prom-dades:/prometheus
networks: [observacio, posterior]
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.52.0
profiles: [observabilitat]
volumes: ["/:/rootfs:ro", "/var/run:/var/run:ro", "/sys:/sys:ro", "/var/lib/docker/:/var/lib/docker:ro"]
devices: ["/dev/kmsg"]
networks: [observacio]
node-exporter:
image: prom/node-exporter:v1.8.2
profiles: [observabilitat]
command: ["--path.rootfs=/host"]
pid: host
volumes: ["/:/host:ro,rslave"]
networks: [observacio]
grafana:
image: grafana/grafana:11.5.0
profiles: [observabilitat]
environment:
GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin
secrets: [grafana_admin]
volumes:
- ./observabilitat/fonts.yaml:/etc/grafana/provisioning/datasources/fonts.yaml:ro
- grafana-dades:/var/lib/grafana
ports: ["3001:3000"]
depends_on: [loki, prometheus]
networks: [observacio]
volumes: { loki-dades: {}, prom-dades: {}, grafana-dades: {} }
networks: { observacio: { driver: bridge } }# observabilitat/prometheus.yml
global: { scrape_interval: 15s }
rule_files: ["/etc/prometheus/alertes.yml"]
scrape_configs:
- { job_name: cadvisor, static_configs: [{ targets: ["cadvisor:8080"] }] }
- { job_name: node, static_configs: [{ targets: ["node-exporter:9100"] }] }
- { job_name: docker-daemon, static_configs: [{ targets: ["172.17.0.1:9323"] }] }
- { job_name: aurora-api, metrics_path: /metriques,
static_configs: [{ targets: ["aurora-api:3000"] }] }docker compose -f compose.yaml -f compose.observabilitat.yaml --profile observabilitat up -d
docker compose --profile observabilitat ps --format "{{.Service}}" | tr '\n' ' '
# grafana prometheus cadvisor loki promtail node-exporter aurora-api aurora-db ...Que tot això visqui en un perfil (lliçó 04-06) és deliberat: docker compose up -d continua aixecant només la plataforma, i l'observabilitat s'afegeix quan cal, sense consumir recursos al portàtil de ningú.
Avís de seguretat. Promtail munta
docker.socken només lectura per llegir etiquetes, i cAdvisor té accés ampli al host. Són concessions reals (lliçó 05-03): en producció, fes servir un proxy de socket filtrat i valida aquesta configuració amb el teu responsable de seguretat.
- Consultes LogQL útils
# Errors de l'API a l'última hora
{compose_service="aurora-api"} | json | nivell="error"
# La traça completa d'una petició concreta
{compose_service="aurora-api"} | json | req_id="7f3a1c9d-4e28-4a1b-9c33-1f0e7b2d5a64"
# Peticions lentes: més de 500 ms
{compose_service="aurora-api"} | json | ms > 500 | line_format "{{.ruta}} {{.ms}}ms"
# Taxa de respostes 5xx per minut
sum(rate({compose_service="aurora-api"} | json | estat >= 500 [1m]))
# Proporció d'encerts de memòria cau
sum(count_over_time({compose_service="aurora-api"} | json | missatge="cache hit" [5m]))
/ sum(count_over_time({compose_service="aurora-api"} | json | missatge=~"cache (hit|miss)" [5m]))La segona consulta és la que justifica tota la feina de l'apartat 5: amb un identificador que et dona l'usuari, recuperes la seva petició exacta entre milions de línies en menys d'un segon. L'última converteix una decisió d'arquitectura —el patró cache-aside— en una mètrica observable: si la proporció d'encerts cau, alguna cosa passa amb Redis o amb la invalidació.
- Mètriques:
docker stats i els seus límits
docker stats i els seus límitsNAME CPU % MEM USAGE / LIMIT MEM %
aurora-libros-aurora-api-1 2.14% 84.2MiB / 512MiB 16.45%
aurora-libros-aurora-db-1 0.88% 412.7MiB / 2GiB 20.15%
aurora-libros-aurora-cache-1 0.31% 9.8MiB / 256MiB 3.83%Els seus límits, i per això no n'hi ha prou: és una instantània sense històric, no té alertes, no agrega diversos hosts i cal estar-hi mirant. Serveix per a "què està passant ara"; no per a "què va passar ahir a la nit".
- L'API de mètriques del daemon
El daemon mateix exposa mètriques en format Prometheus si l'habilites a daemon.json:
sudo systemctl restart docker
curl -s http://localhost:9323/metrics | grep '^engine_daemon_container_states' | head -3engine_daemon_container_states_containers{state="running"} 6
engine_daemon_container_states_containers{state="paused"} 0
engine_daemon_container_states_containers{state="stopped"} 2Són mètriques del daemon, no de cada contenidor: quants contenidors hi ha en cada estat, temps de les operacions, versió del motor. Útils per vigilar la salut del Docker mateix. I exposa aquell port només a la xarxa de gestió: no porta autenticació.
- cAdvisor, node-exporter i Prometheus
cAdvisor llegeix els cgroups (lliçó 05-07) i publica mètriques per contenidor; node-exporter fa el mateix amb el host.
curl -s http://localhost:9090/api/v1/query --data-urlencode \
'query=rate(container_cpu_usage_seconds_total{name=~"aurora.*"}[5m])' | jq -r '.data.result[].metric.name'Grafana consulta totes dues fonts, i els panells no cal inventar-los: els dashboards 193 (Docker/cAdvisor) i 1860 (Node Exporter Full) del catàleg públic cobreixen el 90 % del que cal.
- Les mètriques que de debò es vigilen
| Mètrica | Expressió | Per què importa |
|---|---|---|
| Memòria respecte al límit | container_memory_usage_bytes / container_spec_memory_limit_bytes |
A prop del 100 % → OOM kill (codi 137) |
| CPU respecte al límit | rate(container_cpu_usage_seconds_total[5m]) |
Estrangulament i latència alta |
| Reinicis | changes(container_start_time_seconds[1h]) |
Un contenidor que es reinicia sol està fallant |
| Estat de salut | /salut, docker inspect .State.Health |
Distingeix "arrencat" de "preparat" |
| Latència p95 | histogram_quantile(0.95, ...) |
La mitjana menteix; el percentil 95 no |
| Taxa d'errors | rate(peticions{estat=~"5.."}[5m]) |
El senyal més directe que alguna cosa s'ha trencat |
| Disc i connexions a la BD | node_filesystem_avail_bytes, pg_stat_activity |
Logs que omplen el disc; pool exhaurit que tomba l'API |
Fixa't en el patró: el que importa gairebé mai no és el valor absolut, sinó la relació amb el límit que vas posar a la lliçó 03-07. 400 MB de memòria no diuen res; 400 MB sobre un límit de 512 MB diuen que falta poc per a un OOM.
- Alertes
# observabilitat/alertes.yml
groups:
- name: aurora
rules:
- alert: ContenidorCaigut
expr: absent(container_last_seen{name="aurora-libros-aurora-api-1"}) == 1
for: 2m
labels: { severitat: critica, avis: guardia }
annotations: { resum: "aurora-api no respon des de fa 2 minuts" }
- alert: MemoriaAPropDelLimit
expr: container_memory_usage_bytes{name=~"aurora.*"}
/ container_spec_memory_limit_bytes{name=~"aurora.*"} > 0.9
for: 5m
labels: { severitat: alta, avis: equip }
annotations: { resum: "{{ $labels.name }} al {{ $value | humanizePercentage }} del límit" }
- alert: TaxaDErrorsAlta
expr: sum(rate(aurora_peticions_total{estat=~"5.."}[5m]))
/ sum(rate(aurora_peticions_total[5m])) > 0.05
for: 3m
labels: { severitat: critica, avis: guardia }
annotations: { resum: "Més del 5 % de les peticions retornen 5xx" }
- alert: ReinicisRepetits
expr: changes(container_start_time_seconds{name=~"aurora.*"}[15m]) > 3
for: 1m
labels: { severitat: alta, avis: equip }Les dues claus d'una alerta que no acaba ignorada. La primera és for: sense ell, un pic de dos segons desperta algú de matinada. La segona és a qui notifica: guardia per al que exigeix acció immediata (servei caigut, errors massius) i equip per al que pot esperar a demà (memòria alta, reinicis). Una alerta que no porta a cap acció concreta s'ha d'esborrar; el pitjor estat possible d'un sistema d'alertes és que l'equip les ignori per costum.
- L'endpoint de salut ric
/salut va començar retornant {"estat":"ok"}. Això només diu que el procés arrenca. Un endpoint útil comprova les seves dependències:
app.get('/salut', async (req, res) => {
const inici = Date.now();
const comprovar = async (nom, fn) => {
const t = Date.now();
try { await fn(); return { nom, estat: 'ok', ms: Date.now() - t }; }
catch (e) { return { nom, estat: 'error', ms: Date.now() - t, detall: e.message }; }
};
const parts = await Promise.all([
comprovar('bd', () => db.query('SELECT 1')),
comprovar('cache', () => cache.ping())
]);
const sa = parts.every(p => p.estat === 'ok');
if (!sa) log.error('salut degradada', { parts });
res.status(sa ? 200 : 503).json({
estat: sa ? 'ok' : 'degradat', version: process.env.APP_VERSION ?? '1.3.0',
uptime_s: Math.round(process.uptime()), ms: Date.now() - inici, dependencies: parts
});
});curl -s http://localhost:8080/api/salut | jq -c
docker compose stop aurora-cache && sleep 2
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/api/salut
docker compose start aurora-cache{"estat":"ok","version":"1.3.0","uptime_s":1842,"ms":4,"dependencies":[{"nom":"bd","estat":"ok","ms":2},{"nom":"cache","estat":"ok","ms":1}]}
503El 503 és el que importa: el HEALTHCHECK del Dockerfile el detecta, el contenidor passa a unhealthy, depends_on: service_healthy no arrenca el que en depèn i el balancejador deixa d'enviar-li trànsit. Amb un /salut que només retornava ok, res d'això no passaria.
Un matís de disseny: distingeix liveness (s'ha de reiniciar aquest procés?) de readiness (pot rebre trànsit?). Si Redis cau, l'API no s'ha de reiniciar en bucle, només deixar d'anunciar-se com a preparada; la distinció explícita arriba amb Kubernetes (lliçó 06-05).
- Traces distribuïdes: el pas següent
Logs i mètriques responen a "què va passar" i "quant"; les traces responen a "on se'n va anar el temps". Una petició a /llibres travessa nginx, Express, Redis i PostgreSQL, i una traça mesura cada tram per separat. OpenTelemetry és l'estàndard: amb la instrumentació automàtica de Node n'hi ha prou amb un paquet i unes variables (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4318, OTEL_TRACES_SAMPLER_ARG: "0.1" per mostrejar el 10 %).
El req_id de l'apartat 5 és, de fet, un precursor artesanal del trace_id. Si adoptes OpenTelemetry, substitueix-lo per l'identificador de traça i tindràs logs, mètriques i traces correlacionats pel mateix camp.
Errors Habituals i Consells
Escriure logs a fitxers dins del contenidor. Es perden en recrear-lo i docker logs no els veu. Sempre a stdout/stderr.
No configurar rotació. 8 GB al mes per servei i una caiguda per disc ple que a més impedeix registrar-ne la causa.
Canviar el driver a fluentd o syslog i perdre docker logs. Si el sistema central falla, et quedes cec. Mantén json-file/local i recull amb un agent.
Registrar dades personals o secrets. Un log amb contrasenyes o dades de clients és un incident de protecció de dades. Filtra a l'emissor i consulta-ho amb el teu responsable de compliance.
Alertar sobre valors absoluts, o alertar sense for. 400 MB no signifiquen res sense el límit al costat, i un pic de dos segons que desperta algú de matinada ensenya l'equip a ignorar les alertes.
Un /salut que només diu ok. No distingeix "el procés viu" de "el servei funciona". Comprova les dependències.
Consell: munta l'observabilitat abans de necessitar-la. El dia de l'incident no hi ha temps per instal·lar Prometheus, i les dades que més falta fan són les de les hores anteriors, que ja no pot recuperar ningú.
Exercicis
Exercici 1. Calcula el creixement de logs d'aurora-api amb el teu propi trànsit: mesura la mida del fitxer de log, genera 500 peticions, torna a mesurar i extrapola a un mes. Després configura la rotació i demostra que el sostre es respecta.
Exercici 2. Afegeix el req_id i el registre estructurat a aurora-api, genera trànsit i demostra que pots reconstruir la traça completa d'una petició concreta a partir de l'identificador que retorna la capçalera de resposta.
Exercici 3. Aixeca el perfil observabilitat, provoca un consum de memòria a prop del límit a aurora-api i comprova a Prometheus que l'expressió de l'alerta es dispara. Explica per què l'alerta fa servir una proporció i no un valor absolut.
Solucions
Solució 1.
id=$(docker compose ps -q aurora-api); f=/var/lib/docker/containers/$id/$id-json.log
abans=$(sudo stat -c %s "$f")
for i in $(seq 1 500); do curl -s -o /dev/null http://localhost:8080/api/llibres; done
p=$(( ($(sudo stat -c %s "$f") - abans) / 500 ))
echo "bytes/petició: $p"
echo "a 15 pet/s: $(( p * 15 * 86400 / 1048576 )) MB/dia"
echo "al mes: $(( p * 15 * 86400 * 30 / 1073741824 )) GB"Set gigabytes al mes d'un sol servei, mesurats i no estimats. Ara el límit:
docker compose up -d --force-recreate aurora-api
for i in $(seq 1 60000); do curl -s -o /dev/null http://localhost:8080/api/salut; done
id=$(docker compose ps -q aurora-api)
sudo du -ch /var/lib/docker/containers/$id/*json.log* | tail -4El sostre es respecta: tres fitxers, cap per sobre de 10 MB, 25 MB en total passi el que passi. S'han perdut les línies més antigues, i això és exactament el que es busca: els logs recents són els que serveixen per diagnosticar, i l'històric és responsabilitat de Loki, no del disc del host. El detall que cal recordar és que max-size sense max-file limita un sol fitxer i max-file sense max-size no limita res: calen tots dos.
Solució 2.
docker compose up -d --build aurora-api
rid=$(curl -s -D - -o /dev/null http://localhost:8080/api/llibres | grep -i '^x-request-id' | tr -d '\r' | awk '{print $2}')
echo "petició: $rid"
docker compose logs aurora-api --no-log-prefix --tail 200 | jq -c "select(.req_id==\"$rid\")"petició: 7f3a1c9d-4e28-4a1b-9c33-1f0e7b2d5a64
{"ts":"...T10:14:02.874Z","nivell":"debug","missatge":"cache miss","req_id":"7f3a1c9d-...","clau":"llibres:tots"}
{"ts":"...T10:14:02.901Z","nivell":"debug","missatge":"consulta bd","req_id":"7f3a1c9d-...","files":9,"ms":24.8}
{"ts":"...T10:14:02.914Z","nivell":"info","missatge":"peticio","req_id":"7f3a1c9d-...","metode":"GET","ruta":"/llibres","estat":200,"ms":33.2}Tres línies que expliquen la història completa d'aquella petició: la memòria cau no tenia la clau, es va consultar PostgreSQL amb 9 files en 24,8 ms, i la resposta va sortir amb un 200 en 33,2 ms totals. Amb aquestes dades pots afirmar que el 75 % del temps se'n va anar a la base de dades, sense endevinar res.
El que fa això operable de debò és que l'identificador surt a l'exterior a la capçalera x-request-id. L'usuari que reporta un problema et pot donar aquell codi —o el frontal el pot incloure al seu missatge d'error—, i tu recuperes la seva petició exacta entre milions. I com que la capçalera també s'accepta a l'entrada, si nginx o el frontal ja en generen un, l'API el reutilitza i la traça queda encadenada d'extrem a extrem.
Solució 3.
docker compose -f compose.yaml -f compose.observabilitat.yaml --profile observabilitat up -d
Q='container_memory_usage_bytes{name=~"aurora.*"} / container_spec_memory_limit_bytes{name=~"aurora.*"}'
curl -s http://localhost:9090/api/v1/query --data-urlencode "query=$Q" \
| jq -r '.data.result[] | "\(.metric.name) \(.value[1])"' | head -1
# Es força el consum amb un endpoint de prova que reté memòria
for i in $(seq 1 40); do curl -s -o /dev/null "http://localhost:8080/api/proves/memoria?mb=12"; done
sleep 70
curl -s http://localhost:9090/api/v1/query --data-urlencode "query=$Q > 0.9" \
| jq -r '.data.result[] | "\(.metric.name) \(.value[1])"'
curl -s http://localhost:9090/api/v1/alerts | jq -r '.data.alerts[] | "\(.labels.alertname) \(.state)"'L'expressió retorna resultat (94 % del límit) i l'alerta entra en pending. Després de cinc minuts complint la condició —el for: 5m de la regla— passaria a firing i arribaria a Alertmanager. Aquell retard és deliberat: un pic de memòria durant una importació puntual no ha de generar avís; una tendència sostinguda al 94 %, sí.
I la raó de fer servir una proporció i no un valor absolut és doble. La primera, de significat: 480 MB és catastròfic per a aurora-api (límit de 512 MB, a un pas de l'OOM killer i del codi 137) i absolutament normal per a aurora-db (límit de 2 GB). Una alerta amb llindar fix o bé inunda de falsos positius o bé no detecta res. La segona, de manteniment: el dia que apugis el límit de l'API a 1 GB, la regla en proporció continua sent correcta sense tocar-la, mentre que un llindar absolut caldria recordar de canviar-lo. I aquell "recordar" és justament el que no passa mai.
Conclusió
Aurora Libros ja no és una caixa opaca en marxa. Saps per què una aplicació en contenidor escriu a stdout/stderr i no a fitxers, coneixes els vuit drivers de logging i el parany que n'amaguen la meitat: amb syslog, fluentd o gelf, docker logs deixa de funcionar, així que l'estratègia sòlida és json-file o local amb rotació més un agent que reculli. I has fet el càlcul que convenç qualsevol: 214 bytes per petició són 7 GB al mes d'un sol servei, i sense max-size i max-file això acaba en una caiguda per disc ple que a més impedeix registrar-ne la causa.
La teva API emet ara logs estructurats en JSON amb nivell, servei, versió i un req_id que viatja per la capçalera x-request-id i permet reconstruir la traça completa d'una petició concreta entre milions de línies. Damunt d'això muntes l'agregació amb Loki, Promtail i Grafana en un perfil observabilitat que no molesta en desenvolupament, amb consultes LogQL que van des d'"errors de l'última hora" fins a la proporció d'encerts de memòria cau, que converteix una decisió d'arquitectura en una mètrica observable.
Del costat de les mètriques coneixes els límits de docker stats, l'endpoint Prometheus del daemon, i cAdvisor i node-exporter alimentant Prometheus i Grafana. I saps què vigilar: memòria i CPU respecte al límit —no en absolut—, reinicis, salut, latència p95, taxa d'errors i disc; amb alertes que porten for i que distingeixen qui desperten. Tanques amb un /salut que comprova PostgreSQL i Redis i retorna 503 quan alguna cosa falla, fent que el HEALTHCHECK, depends_on i el balancejador reaccionin sols, i amb OpenTelemetry apuntat com a pas següent.
A la lliçó 05-07, l'última del mòdul, es tanca el cercle: baixaràs al kernel de Linux per veure que un contenidor no existeix. És un procés normal amb tres mecanismes a sobre —namespaces, cgroups i un sistema de fitxers de capes—, i els tocaràs un a un: els set namespaces amb lsns i nsenter, els fitxers de cgroups v2 d'aurora-db comprovant que coincideixen exactament amb els límits que vas posar a 03-07, el muntatge OverlayFS real amb el seu copy-on-write, les capacitats descodificades amb capsh, i què fa exactament runc quan arrenca un contenidor.
Docker: De Principiant a Avançat
Mòdul 1: Introducció a Docker
- Què és Docker?
- Instal·lant Docker
- Arquitectura de Docker
- Comandes Bàsiques de Docker
- Entenent les Imatges de Docker
- Creant el teu Primer Contenidor Docker
- El Projecte del Curs: la Plataforma Aurora Libros
Mòdul 2: Treballant amb Imatges Docker
- Docker Hub i Repositoris
- Construint Imatges Docker
- Conceptes Bàsics de Dockerfile
- Instruccions Avançades del Dockerfile
- Gestionant Imatges Docker
- Etiquetatge i Publicació d'Imatges
Mòdul 3: Contenidors Docker
- Executant Contenidors
- Cicle de Vida del Contenidor
- Gestionant Contenidors
- Inspecció i Depuració de Contenidors
- Xarxes a Docker
- Persistència de Dades amb Volums
- Límits de Recursos i Polítiques de Reinici
Mòdul 4: Docker Compose
- Introducció a Docker Compose
- Definint Serveis a Docker Compose
- Comandes de Docker Compose
- Aplicacions Multi-Contenidor
- Variables d'Entorn a Docker Compose
- Perfils, Overrides i Múltiples Entorns
- Desenvolupament Local amb Docker Compose
Mòdul 5: Conceptes Avançats de Docker
- Aprofundiment en Xarxes Docker
- Opcions d'Emmagatzematge Docker
- Millors Pràctiques de Seguretat a Docker
- Optimitzant Imatges Docker
- Builds Avançades amb BuildKit i Buildx
- Registre i Monitoratge a Docker
- El Runtime per Dins: Namespaces, Cgroups i Capes
Mòdul 6: Docker en Producció
- Preparar una Imatge per a Producció
- CI/CD amb Docker
- Orquestrant Contenidors amb Docker Swarm
- Introducció a Kubernetes
- Desplegant Contenidors Docker a Kubernetes
- Escalat i Balanceig de Càrrega
- Estratègies de Desplegament i Rollback
