Amb la configuració resolta, Escena Viva ja arrenca en qualsevol entorn amb els valors correctes i mor immediatament si li falta un secret. Però continua havent-hi un problema més gran: quan el Festival de Jazz de Primavera obri la seva venda anticipada i alguna cosa falli a les tres de la matinada, com te n'assabentes? I com esbrines què va passar exactament a la compra de la Lucía, entre les onze mil peticions que hi va haver aquell minut? Aquesta lliçó converteix Escena Viva en una aplicació observable: que produeix registres estructurats que una màquina pot consultar, mètriques que algú pot mirar en un tauler, sondes de salut que l'orquestrador pot interrogar i alertes que sonen quan val la pena. Al mòdul 10 vam aprendre a produir mètriques —el vigilant del bucle d'esdeveniments, l'ús de memòria— però es quedaven dins del procés. Avui les enviem a algun lloc.
Contingut
- Per què
console.logdeixa de servir - pino: registre estructurat en JSON
- El registrador fill i la traçabilitat per petició
- Què es registra i què NO es registra MAI
- Errors: la traça completa només del costat del servidor
- Els tres pilars de l'observabilitat
- Mètriques amb
prom-client - Traces distribuïdes: quan compensen
- Sondes de salut: viu enfront de preparat
- Alertes que serveixen
- Retenció, cost i on s'envien els registres
- Per què
console.log deixa de servir
console.log deixa de servirconsole.log és perfecte mentre desenvolupes. En producció té quatre defectes greus. No té nivells, així que no pots abaixar el soroll en producció i apujar-lo quan investigues un incident: o ho imprimeixes tot o res. No té estructura: console.log('Compra de ' + correu) produeix una frase, i per respondre «quantes compres va fallar la Lucía?» cal escriure una expressió regular fràgil sobre gigabytes de text. No té context: no saps de quina petició, de quin usuari ni de quin treballador ve aquella línia, i amb quatre treballadors en clúster les línies de quatre peticions simultànies s'entrellacen. I pot ser síncron: quan la sortida estàndard és un fitxer o un TTY, process.stdout.write és blocador a Linux, així que cada console.log atura el bucle d'esdeveniments — en una ruta calenta això es mesura en el p99 que tant vam cuidar al mòdul 10.
Un registre estructurat és, en canvi, una línia de JSON per esdeveniment:
{"level":30,"time":"2026-08-15T21:14:02.415Z","pid":4821,"idPeticio":"a3f1c2d4","ruta":"/api/compres","estat":201,"duracioMs":83,"esdeveniment":"compra.completada","idEsdeveniment":"evt-003","entrades":2,"totalCentims":7000,"msg":"Compra completada"}La diferència no és estètica: amb això pots preguntar «dóna'm totes les compres d'evt-003 que van trigar més de 500 ms i van retornar 5xx, agrupades per treballador». Amb una frase en prosa, no.
- pino: registre estructurat en JSON
pino és el registrador estàndard de facto a Node per una raó concreta: és ràpid. Serialitza a JSON amb un serialitzador propi i escriu de manera asíncrona a través d'un transport en un fil a part, així que registrar deixa de competir amb atendre peticions. S'instal·la amb npm install pino i npm install --save-dev pino-pretty. pino-pretty va a devDependencies expressament: formatar és un luxe de desenvolupament; en producció escrivim JSON cru i l'entorn s'encarrega de la resta.
// src/registre/logger.js
'use strict';
const pino = require('pino');
const { configuracio } = require('../config/index.js');
const { censurar } = require('./censura.js');
const opcions = {
level: configuracio.nivellRegistre,
// Camps fixos a TOTES les linies.
base: { servei: 'escena-viva', entorn: configuracio.entorn, pid: process.pid },
timestamp: pino.stdTimeFunctions.isoTime, // ISO: llegible en qualsevol visor.
serializers: {
err: pino.stdSerializers.err,
peticio: (req) => ({ metode: req.method, ruta: req.path, ip: req.ip }),
// Reutilitza la censura recursiva del M8: res sensible no surt d'aqui.
dades: (valor) => censurar(valor),
},
// Segona capa de proteccio, per si algu oblida el serialitzador.
redact: {
paths: ['req.headers.authorization', 'req.headers.cookie', 'contrasenya',
'*.contrasenya', 'token', '*.token'],
censor: '[CENSURAT]',
},
};
// pino-pretty NOMES en desenvolupament: en produccio, JSON cru a stdout.
const transport =
configuracio.entorn === 'development'
? pino.transport({ target: 'pino-pretty', options: { colorize: true } })
: undefined;
module.exports = { logger: pino(opcions, transport) };Tres decisions mereixen explicació. base afegeix camps fixos a totes les línies: servei és imprescindible quan diversos serveis escriuen al mateix agregador, i pid distingeix els treballadors del clúster del mòdul 10. serializers s'apliquen per nom de camp, de manera que en escriure logger.info({ dades: cos }) l'objecte passa per censurar abans de sortir, reutilitzant src/registre/censura.js del mòdul 8 sense duplicar lògica. I redact és la xarxa de seguretat: encara que algú oblidi el serialitzador, aquelles rutes se substitueixen per [CENSURAT]. Dues capes, perquè una de sola falla.
| Nivell | Valor | Ús a Escena Viva | En producció? |
|---|---|---|---|
trace |
10 | Cada consulta SQL, cada encert de memòria cau | No, només depurant |
debug |
20 | Decisions internes: quina política d'aforament s'ha aplicat | No per defecte |
info |
30 | Esdeveniments de negoci: compra completada, entrada emesa | Sí |
warn |
40 | Alguna cosa estranya però recuperable: reintent de Redis, límit assolit | Sí |
error |
50 | Petició fallida, tasca de cua exhaurida després de reintents | Sí |
fatal |
60 | El procés no pot continuar: la BD no respon en arrencar | Sí |
La regla pràctica: info respon «què està fent el sistema?», error respon «què s'ha trencat?», i la resta és per quan ja saps que hi ha un problema. NIVELL_REGISTRE de la lliçó anterior permet pujar a debug en una instància durant un incident, sense desplegar.
Substituir morgan i els console.error solts
El mòdul 6 va deixar morgan a src/middleware/registre-http.js. Morgan escriu text de servidor web clàssic, que ara és l'excepció en un flux JSON, així que se substitueix per un middleware propi que tanca el cicle de cada petició:
// src/middleware/registre-peticions.js (reescrit sobre pino)
'use strict';
function crearRegistrePeticions() {
return function registrePeticions(req, res, seguent) {
const inici = process.hrtime.bigint();
// 'finish' s'emet quan la resposta s'ha enviat del tot.
res.on('finish', () => {
const duracioMs = Number(process.hrtime.bigint() - inici) / 1e6;
const nivell = res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'info';
// El PATRO de ruta, no l'URL concreta: si no, no pots agrupar.
const ruta = req.route ? req.baseUrl + req.route.path : req.path;
req.log[nivell](
{ metode: req.method, ruta, estat: res.statusCode, duracioMs },
'peticio completada'
);
});
seguent();
};
}
module.exports = { crearRegistrePeticions };Detalls que importen: la ruta es registra com a patró (/api/esdeveniments/:idEsdeveniment), perquè registrar l'URL amb l'identificador a dins impedeix agrupar; el nivell depèn del codi d'estat, perquè un 500 aparegui en filtrar per error; i process.hrtime.bigint() dóna precisió de nanosegons, a diferència de Date.now(). Pel que fa als console.error solts pel codi, s'eliminen tots: un grep -rn "console\." src/ a la canonada de CI (lliçó 11-06) evita que tornin.
- El registrador fill i la traçabilitat per petició
Aquí arriba la peça que anem prometent des del mòdul 6, quan vam escriure src/middleware/id-peticio.js per assignar a cada petició un crypto.randomUUID(). Fins ara servia de poc; ara es converteix en el fil que cus tots els registres d'una petició, gràcies als registradors fill de pino: logger.child({ camp: valor }) retorna un registrador que afegeix aquests camps a totes les seves línies sense cost de serialització repetida.
// src/middleware/id-peticio.js (ampliat)
'use strict';
const crypto = require('node:crypto');
function crearIdPeticio({ logger }) {
return function idPeticio(req, res, seguent) {
// Respectem l'identificador de la vora si el proxy ja n'ha posat un.
const entrant = req.get('x-request-id');
req.idPeticio = entrant && entrant.length <= 64 ? entrant : crypto.randomUUID();
res.setHeader('X-Request-Id', req.idPeticio);
// Registrador fill: TOT el que es registri des d'aqui porta l'identificador.
req.log = logger.child({ idPeticio: req.idPeticio });
seguent();
};
}
module.exports = { crearIdPeticio };A src/app.js l'ordre importa: idPeticio va abans que qualsevol middleware que vulgui registrar alguna cosa, i després de trust proxy perquè req.ip sigui correcte. Els controladors i serveis reben req.log i el propaguen cap avall; al consumidor de la cua BullMQ del mòdul 10 l'idPeticio viatja com a camp de la tasca, així que la feina asíncrona continua correlacionada amb la petició que la va originar. Seguir una compra fallida. En Marc intenta comprar dues entrades per al Festival de Jazz i veu un error 500 amb X-Request-Id: 9c4a.... Escriu al suport amb aquest codi i n'hi ha prou amb una consulta — grep '"idPeticio":"9c4a' registres.jsonl | jq -c '{time, level, esdeveniment, msg}':
{"time":"...02.101Z","level":30,"esdeveniment":"peticio.rebuda","msg":"POST /api/compres"}
{"time":"...02.118Z","level":30,"esdeveniment":"aforament.comprovat","msg":"Disponible: 1189"}
{"time":"...02.140Z","level":30,"esdeveniment":"reserva.creada","msg":"Reserva provisional"}
{"time":"...02.402Z","level":50,"esdeveniment":"pagament.fallit","msg":"La passarella va retornar 502"}
{"time":"...02.410Z","level":30,"esdeveniment":"reserva.alliberada","msg":"Reserva alliberada"}
{"time":"...02.415Z","level":50,"esdeveniment":"peticio.completada","msg":"peticio completada"}Sis línies i la història completa: l'aforament estava bé, la reserva es va crear, la passarel·la de pagament va fallar amb un 502 i —molt important— la reserva es va alliberar correctament, així que aquelles dues butaques no van quedar bloquejades. Sense l'idPeticio aquelles sis línies estarien barrejades amb les d'unes altres quatre-centes peticions simultànies. Aquest és el valor de la traçabilitat, i és el que justifica tota la feina d'aquesta lliçó.
- Què es registra i què NO es registra MAI
| Capa | Què registrar | Nivell |
|---|---|---|
| Middleware HTTP | Mètode, patró de ruta, estat, durada | info / warn / error |
| Autenticació | Inici de sessió correcte o fallit, identificador i rol | info / warn |
| Controlador | Esdeveniment de negoci i els seus identificadors (evt-003, codi d'entrada) |
info |
| Domini | Res. El domini és pur: retorna resultats, no registra | — |
| Repositori | Consultes lentes (per sobre d'un llindar), errors de connexió | warn / error |
| Cua | Tasca iniciada, completada, fallida, número d'intent | info / error |
| Arrencada i aturada | Port, entorn, versió, senyal rebuda, tancament de connexions | info |
I la llista que no es negocia. Mai no es registren contrasenyes, ni tan sols fallides (revelen patrons i errades de contrasenyes reals); tokens JWT complets, tokens de refresc, galetes de sessió ni capçaleres Authorization; números de targeta, CVV o IBAN, ni complets ni «només els quatre últims, que no passa res»; dades personals innecessàries com el correu complet, l'adreça o el telèfon, en comptes de l'identificador de l'usuari; ni cossos de petició sencers «per si de cas», sinó només els camps que t'importen. El mòdul 8 ja ens va donar src/registre/censura.js amb censurar i CAMPS_CENSURATS, que recorre objectes recursivament substituint camps sensibles. En connectar-lo com a serialitzador de pino, aquella protecció s'aplica automàticament a tot el que passi pel camp dades, i la combinació de serialitzador més redact cobreix tant el que anotes expressament com el que se't cola. Recorda a més el marc legal: a la Unió Europea, un registre amb dades personals és tractament de dades personals, amb les seves obligacions de retenció i de dret a l'esborrat. Registrar menys no és només higiene tècnica, és menys risc jurídic.
- Errors: la traça completa només del costat del servidor
El gestor d'errors del mòdul 6 ja distingeix errors operatius (previstos: aforament insuficient, entrada no trobada) de defectes (no previstos). Aquesta distinció governa també el registre:
// src/middleware/errors.js (fragment del gestor final)
function gestorDErrors(error, req, res, seguent) {
const estat = ESTAT_PER_CODI[error.codi] ?? 500;
if (estat >= 500) {
req.log.error({ err: error, estat }, 'error no controlat'); // Traca completa.
} else {
req.log.warn({ codi: error.codi, estat }, error.message); // Sense traca: es soroll.
}
// Al client, MAI la traca. Nomes l'identificador per correlacionar.
const codi = error.codi ?? 'ERROR_INTERN';
const missatge = estat >= 500 ? 'Error intern del servidor' : error.message;
res.status(estat).json({ error: { codi, missatge, idPeticio: req.idPeticio } });
}Les traces revelen rutes del sistema de fitxers, noms de mòduls interns i de vegades valors. El client rep només l'idPeticio; amb ell, el suport troba la traça completa a l'agregador, i l'usuari obté alguna cosa accionable sense que es filtri res. Falta tancar els dos casos que escapen a Express, a src/servidor.js:
for (const succes of ['uncaughtException', 'unhandledRejection']) {
process.on(succes, (motiu) => {
logger.fatal({ err: motiu, succes }, 'fallada no gestionada; acabant');
process.exitCode = 1;
tancarOrdenadament();
});
}Sí, s'acaba el procés. Un uncaughtException el deixa en estat indefinit: registrem, aturem ordenadament (mòdul 6) i deixem que el supervisor —PM2 a 11-03, Docker a 11-04, la plataforma a 11-05— arrenqui una instància sana.
- Els tres pilars de l'observabilitat
| Pilar | Què és | A quina pregunta respon | Cost |
|---|---|---|---|
| Registres | Esdeveniments discrets amb context | «Què li va passar exactament a aquesta petició?» | Alt: creix amb el trànsit |
| Mètriques | Valors numèrics agregats en el temps | «Com va el sistema en conjunt?» | Baix: mida constant |
| Traces | El recorregut d'una petició per diversos serveis | «On se n'ha anat el temps?» | Mitjà: sol mostrejar-se |
La seqüència real d'un incident ho explica millor que qualsevol definició: una alerta salta perquè una mètrica (p99 de latència) s'ha disparat; el tauler mostra que només afecta /api/compres; una traça revela que el 80 % del temps és en una consulta a PostgreSQL; i els registres d'aquella petició donen la consulta concreta i l'esdeveniment (evt-003, com no) que la provoca. Mètriques per detectar, traces per localitzar, registres per entendre. Els tres, no un.
- Mètriques amb
prom-client
prom-clientPrometheus és l'estàndard de facto: un servidor que consulta periòdicament un punt d'entrada HTTP de la teva aplicació i desa sèries temporals; Grafana les dibuixa, i prom-client és la llibreria que exposa aquell punt d'entrada des de Node.
// src/observabilitat/metriques.js
'use strict';
const clientProm = require('prom-client');
const { crearVigilantDelBucle } = require('./bucle.js');
const registre = new clientProm.Registry();
const etiquetesHttp = ['metode', 'ruta', 'estat'];
// Metriques per defecte del proces: CPU, heap, descriptors, recollidor... de franc.
clientProm.collectDefaultMetrics({ register: registre, prefix: 'escenaviva_' });
const peticionsTotals = new clientProm.Counter({
name: 'escenaviva_peticions_total', help: 'Peticions HTTP ateses',
labelNames: etiquetesHttp, registers: [registre],
});
const duracioPeticions = new clientProm.Histogram({
name: 'escenaviva_duracio_peticio_segons', help: 'Durada de les peticions',
labelNames: etiquetesHttp, registers: [registre],
// Cubells triats amb les dades de carrega del M10, no a l'atzar.
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});
const entradesVenudes = new clientProm.Counter({
name: 'escenaviva_entrades_venudes_total', help: 'Entrades venudes',
labelNames: ['idEsdeveniment', 'sala'], registers: [registre],
});
const midaCua = new clientProm.Gauge({
name: 'escenaviva_cua_pendents', help: 'Tasques pendents a BullMQ',
labelNames: ['cua'], registers: [registre],
});
const retardBucle = new clientProm.Gauge({
name: 'escenaviva_bucle_retard_ms', help: "Retard del bucle d'esdeveniments",
registers: [registre],
});
// Reutilitzem el vigilant del M10: la metrica ja es produia, ara es publica.
const vigilant = crearVigilantDelBucle({
intervalMs: 1000, alMesurar: (retardMs) => retardBucle.set(retardMs),
});
module.exports = {
registre, peticionsTotals, duracioPeticions, entradesVenudes, midaCua, vigilant,
};Fem servir els tres tipus de mètrica: el Counter només puja i es reinicia en reiniciar el procés (peticions totals, entrades venudes); el Gauge puja i baixa (tasques a la cua, retard del bucle); i l'Histogram reparteix observacions en cubells, d'on surten els percentils (durada de peticions). Sobre l'histograma hi ha un detall crucial: els percentils no es poden promitjar, així que si cada treballador calculés el seu propi p99, la mitjana d'aquells p99 no seria el p99 del sistema. Per això s'exporten els cubells crus i Prometheus calcula el percentil amb histogram_quantile sobre tots ells. Avís crític sobre les etiquetes: cada combinació de valors crea una sèrie temporal. Etiquetar per idEsdeveniment està bé (tres esdeveniments); etiquetar per idUsuari, per URL completa o per idPeticio és una explosió de cardinalitat que tomba Prometheus. És l'error número u amb mètriques.
// src/rutes/metriques.js — 404 en comptes de 401: ni confirmem que existeix.
enrutador.get('/metriques', async (req, res) => {
const esperat = configuracio.observabilitat.tokenMetriques;
if (!esperat || req.get('authorization') !== `Bearer ${esperat}`) return res.status(404).end();
res.set('Content-Type', registre.contentType);
res.end(await registre.metrics());
});Es protegeix perquè /metriques revela topologia interna, volum de negoci i versions, i s'exclou de la limitació de peticions perquè Prometheus consulta cada 15 segons. Amb el clúster del mòdul 10 hi ha un matís: cada treballador té el seu registre en memòria i Prometheus consulta un sol port, així que o bé es fa servir AggregatorRegistry de prom-client al primari, o —més simple— s'exposa un port de mètriques per treballador i es deixen descobrir tots. Cap opció és màgica; l'important és saber que el problema existeix. Prometheus desa les sèries i les consulta amb PromQL; la seva configuració mínima és una llista d'objectius amb el seu interval i les seves credencials. Grafana es connecta a Prometheus i dibuixa. El tauler mínim d'Escena Viva té quatre gràfiques: peticions per segon per estat, latència p50/p95/p99, retard del bucle d'esdeveniments i tasques pendents a la cua. Si només pots mirar una pantalla durant l'estrena del Festival de Jazz, que sigui aquesta.
- Traces distribuïdes: quan compensen
Una traça segueix una petició a través de diversos serveis, encadenant spans (trams amb inici, fi i pare). OpenTelemetry és l'estàndard obert: instrumenta automàticament Express, PostgreSQL, Redis i HTTP sortint, i exporta a Jaeger, Tempo o al servei que facis servir. La instrumentació es carrega abans que qualsevol altre mòdul (node --require ./src/observabilitat/traces.js src/servidor.js) perquè necessita embolcallar-los en carregar-se. Ara l'honestedat: Escena Viva encara no les necessita. És una API, una base de dades relacional, una de documental, Redis i un consumidor de cua. Amb l'idPeticio propagat als registres i a les tasques de la cua tens el 90 % del benefici a cost gairebé zero. Les traces compensen quan hi ha molts serveis, quan cada petició creua cinc salts o més, o quan el problema és «triga molt» i ningú sap en quin salt. Quan Escena Viva es parteixi en serveis de catàleg, vendes i notificacions, s'instrumenta; abans, és complexitat sense retorn.
- Sondes de salut: viu enfront de preparat
Tot supervisor (PM2, Docker, la PaaS, Kubernetes) necessita preguntar a l'aplicació com està. I hi ha dues preguntes diferents que es confonen constantment:
| Sonda | Pregunta | Si falla | Comprova |
|---|---|---|---|
/salut/viu |
El procés és viu i respon? | Es reinicia el procés | Només que el bucle d'esdeveniments respon |
/salut/preparat |
Pot atendre trànsit ara mateix? | Se li retira trànsit, sense reiniciar | Dependències: BD, Redis |
L'error clàssic —i és un clàssic perquè tomba sistemes sencers— és fer que la sonda de vivacitat consulti la base de dades. Escenari: PostgreSQL se satura durant l'estrena del Festival de Jazz, la sonda falla a les quatre instàncies, el supervisor les reinicia totes alhora i, en arrencar, les quatre obren els seus pools de cop contra una base de dades ja saturada. Falla un altre cop: reinici en cascada, i una incidència de latència es converteix en una caiguda total. La regla: la vivacitat només mira el procés; la disponibilitat mira les dependències.
// src/rutes/salut.js
'use strict';
function crearRutesSalut({ comprovarPostgres, comprovarMongo, comprovarRedis, logger }) {
const enrutador = express.Router();
const arrencatEl = Date.now();
// L'aturada ordenada el posa a false ABANS de comencar a tancar.
const estat = { acceptantTrafic: true };
// Temps limit: una sonda penjada es pitjor que una que falla.
const ambLimit = (promesa, ms) =>
Promise.race([promesa, new Promise((_, r) => setTimeout(() => r(new Error('exhaurit')), ms))]);
// VIVACITAT: si aixo respon, el bucle d'esdeveniments funciona. Res mes.
enrutador.get('/salut/viu', (req, res) => {
res.json({ estat: 'viu', actiuSegons: Math.floor((Date.now() - arrencatEl) / 1000) });
});
// DISPONIBILITAT: comprova dependencies. allSettled informa de TOTES,
// no nomes de la primera que falla.
enrutador.get('/salut/preparat', async (req, res) => {
if (!estat.acceptantTrafic) return res.status(503).json({ estat: 'aturant' });
const noms = ['postgres', 'mongo', 'redis'];
const resultats = await Promise.allSettled([
ambLimit(comprovarPostgres(), 1000),
ambLimit(comprovarMongo(), 1000),
ambLimit(comprovarRedis(), 500),
]);
const detall = Object.fromEntries(
resultats.map((r, i) => [noms[i], r.status === 'fulfilled'])
);
const preparat = Object.values(detall).every(Boolean);
if (!preparat) logger.warn({ detall }, 'sonda de disponibilitat fallida');
res.status(preparat ? 200 : 503).json({ estat: preparat ? 'preparat' : 'no preparat', detall });
});
return { enrutador, estat };
}
module.exports = { crearRutesSalut };Tres decisions deliberades: les comprovacions porten temps límit, perquè una sonda que es penja és pitjor que una que falla; Promise.allSettled informa de totes les dependències; i estat.acceptantTrafic es posa a false en rebre SIGTERM, abans de començar a tancar, perquè el balancejador deixi d'enviar trànsit durant els segons de drenatge. Aquest detall és el que converteix l'aturada ordenada del mòdul 6 en un desplegament sense errors. Totes dues rutes s'exclouen de la limitació de peticions i no exigeixen autenticació, però tampoc no exposen dades internes: res de versions de la base de dades ni cadenes de connexió.
- Alertes que serveixen
Regla d'or de l'enginyeria de fiabilitat: alerta sobre símptomes, no sobre causes. Un símptoma és alguna cosa que l'usuari nota; una causa és una hipòtesi sobre per què. Si alertes sobre «CPU al 90 %», et despertaràs nits en què la CPU era alta i tot anava perfecte, i no et despertaràs la nit en què tot va caure per una altra raó.
| Alerta | Símptoma o causa | Val? |
|---|---|---|
p99 de /api/compres > 2 s durant 5 min |
Símptoma | Sí |
| Taxa de 5xx > 1 % durant 5 min | Símptoma | Sí |
| Cua d'entrades > 500 tasques i creixent 10 min | Símptoma | Sí |
| Cap entrada venuda en 30 min en horari de venda | Símptoma | Sí |
| CPU > 80 % o memòria > 70 % | Causa | No com a alerta; sí com a gràfica |
| Retard del bucle > 200 ms durant 5 min | Frontera | Sí: correlaciona molt bé amb el dolor real |
Dos matisos marquen la diferència entre un sistema d'alertes útil i un que s'ignora. Tota alerta porta durada: «més de 2 s» salta amb un pic irrellevant, «més de 2 s sostingut 5 minuts» salta quan hi ha un problema real. I tota alerta que es dispara ha de requerir una acció humana ara; si no la requereix, no és una alerta sinó una gràfica. Una alerta que ningú atén és pitjor que cap, perquè ensenya l'equip a ignorar les notificacions, i el dia que en salti una de veritat també s'ignorarà. Això té nom —fatiga d'alertes— i ha causat més incidències greus que qualsevol defecte. Cada alerta hauria de portar un enllaç a un procediment: què mirar, què comprovar, a qui escalar; si no saps escriure'l, probablement l'alerta no serveix.
- Retenció, cost i on s'envien els registres
Els registres costen diners: a nivell info, una API amb 500 peticions per segon genera de l'ordre de 100 GB al mes només amb la línia de fi de petició, i els serveis gestionats cobren per gigabyte ingerit i per dia retingut.
| Tipus | Retenció raonable | Per què |
|---|---|---|
Registres d'aplicació (info+) |
14-30 dies | Cobreix la investigació d'un incident recent |
| Registres d'error | 90 dies | Per detectar patrons que es repeteixen |
| Auditoria (mòdul 8) | 1-7 anys | Requisit legal; van a BD, no a l'agregador |
| Mètriques | 13 mesos | Comparar el Festival de Jazz amb el de l'any passat |
I arribem a l'últim punt, que tanca el cercle amb la lliçó 11-01. Els dotze factors, factor XI: tracta els registres com a fluxos d'esdeveniments. L'aplicació no escriu fitxers, no els rota i no sap on van: escriu a stdout i allà s'acaba la seva responsabilitat. Qui recull aquella sortida depèn de l'entorn —PM2 la redirigeix a fitxers (11-03), Docker la captura amb el seu controlador de registre (11-04), la PaaS l'envia al seu agregador (11-05), Kubernetes la recull amb un agent—, i escriure fitxers des de l'aplicació t'obliga a resoldre rotació, permisos i espai en disc, a més de perdre els registres quan un contenidor efímer mor. En codi: pino(opcions) escriu a stdout i és el correcte; pino(pino.destination('/var/log/escena-viva.log')) fa que l'aplicació decideixi el destí, i és justament el que no volem. Una sola línia de diferència, i una muntanya d'operacions que t'estalvies.
Errors Comuns i Consells
- Deixar
pino-prettyactiu en producció. És lent i produeix text que l'agregador no pot indexar. - Registrar l'URL completa com a camp agrupable.
/api/esdeveniments/evt-003en comptes de/api/esdeveniments/:idEsdevenimentimpedeix agrupar i, en mètriques, dispara la cardinalitat. - Sonda de vivacitat que consulta la base de dades. Reinici en cascada garantit sota càrrega.
- Retornar la traça al client. Filtra rutes i estructura interna. Retorna només l'
idPeticio. - Registrar el cos complet de les peticions. Volum enorme i risc de dades personals i secrets.
- Consell: afegeix l'
idPeticioals missatges d'error que veu l'usuari. Que el suport pugui demanar «digue'm el codi que apareix a la pantalla» converteix una investigació d'una hora en una consulta de deu segons. - Consell: registra en arrencar un esdeveniment amb la versió, el commit i l'entorn. Saber quina versió s'estava executant durant un incident és la primera pregunta que et faràs.
Exercicis
Exercici 1 — Registre de consultes lentes
Afegeix a src/repositoris/ un embolcall que mesuri la durada de cada consulta i registri en warn les que superin 200 ms, amb el nom del repositori, el mètode i la durada, fent servir req.log quan existeixi per conservar l'idPeticio.
Exercici 2 — Mètrica de negoci i prova de les sondes
Instrumenta el cas d'ús de compra per incrementar entradesVenudes amb les etiquetes idEsdeveniment i sala, i escriu la consulta PromQL que respongui: entrades venudes per minut al Festival de Jazz de Primavera. Després, escriu una prova d'integració amb supertest que verifiqui que /salut/viu retorna 200 encara que les comprovacions de dependències fallin, que /salut/preparat retorna 503 quan comprovarRedis rebutja i que també retorna 503 quan estat.acceptantTrafic és false.
Solucions
Exercici 1. Un embolcall genèric evita tocar cada mètode:
// src/repositoris/instrumentar.js
'use strict';
const { logger } = require('../registre/logger.js');
const LLINDAR_MS = 200;
function instrumentarRepositori(nom, repositori) {
return new Proxy(repositori, {
get(desti, metode) {
const valor = desti[metode];
if (typeof valor !== 'function') return valor;
return async function (...parametres) {
const inici = process.hrtime.bigint();
try {
return await valor.apply(desti, parametres);
} finally {
const duracioMs = Number(process.hrtime.bigint() - inici) / 1e6;
if (duracioMs > LLINDAR_MS) {
(this?.log ?? logger).warn(
{ repositori: nom, metode: String(metode), duracioMs }, 'consulta lenta');
}
}
};
},
});
}
module.exports = { instrumentarRepositori };Exercici 2. Al cas d'ús, després de confirmar la compra dins de la transacció, entradesVenudes.inc({ idEsdeveniment: compra.idEsdeveniment, sala: compra.sala }, compra.entrades.length). La consulta fa servir rate sobre cinc minuts i multiplica per 60 per passar de segons a minuts: sum by (sala) (rate(escenaviva_entrades_venudes_total{idEsdeveniment="evt-003"}[5m])) * 60. rate gestiona correctament els reinicis del comptador quan un treballador es recicla, que és justament per què no es fa servir increase a pèl ni la diferència crua. I per a les sondes, injectant comprovadors falsos a la factoria de l'aplicació:
const request = require('supertest');
const { expect } = require('chai');
const { crearAplicacio } = require('../../src/app.js');
describe('sondes de salut', () => {
const ok = () => Promise.resolve(true);
const cau = () => Promise.reject(new Error('caiguda'));
const crear = (redis, resta = ok) =>
crearAplicacio({ comprovarPostgres: resta, comprovarMongo: resta, comprovarRedis: redis });
it('viu respon 200 encara que les dependencies fallin', async () => {
await request(crear(cau, cau).app).get('/salut/viu').expect(200);
});
it('preparat respon 503 si Redis falla', async () => {
const resposta = await request(crear(cau).app).get('/salut/preparat').expect(503);
expect(resposta.body.detall).to.deep.include({ redis: false, postgres: true });
});
it("preparat respon 503 durant l'aturada", async () => {
const { app, estatSalut } = crear(ok);
estatSalut.acceptantTrafic = false;
await request(app).get('/salut/preparat').expect(503);
});
});Conclusió
Escena Viva ja no parla sola. Escriu registres estructurats en JSON amb pino, cada línia etiquetada amb l'idPeticio que arrosseguem des del mòdul 6 —de manera que una compra fallida es reconstrueix sencera amb una consulta—, passats per la censura del mòdul 8 perquè ni una contrasenya ni un token acabin a l'agregador. Publica mètriques a /metriques protegit: peticions, latència amb percentils reals, entrades venudes, mida de cua i el retard del bucle d'esdeveniments que el mòdul 10 ens va ensenyar a mesurar. Exposa /salut/viu i /salut/preparat, diferents expressament, llestes perquè un supervisor les interrogui. I tot surt per stdout, perquè decidir on van els registres no és assumpte de l'aplicació.
Precisament això —que algú extern reculli la sortida, vigili el procés i el torni a aixecar si cau— és el que encara no tenim. Ara mateix Escena Viva s'arrenca a mà amb npm start en una terminal, i si tanques la sessió SSH es mor amb ella. A la propera lliçó, Usant PM2 per a la Gestió de Processos, posem un supervisor al davant: fitxer d'ecosistema amb les dues aplicacions del projecte, mode clúster sense mantenir el nostre propi src/cluster.js, recàrrega sense talls recolzada en l'aturada ordenada, protecció contra bucles de reinici i arrencada automàtica en engegar la màquina.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
