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

  1. Per què console.log deixa de servir
  2. pino: registre estructurat en JSON
  3. El registrador fill i la traçabilitat per petició
  4. Què es registra i què NO es registra MAI
  5. Errors: la traça completa només del costat del servidor
  6. Els tres pilars de l'observabilitat
  7. Mètriques amb prom-client
  8. Traces distribuïdes: quan compensen
  9. Sondes de salut: viu enfront de preparat
  10. Alertes que serveixen
  11. Retenció, cost i on s'envien els registres

  1. Per què console.log deixa de servir

console.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.

  1. 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.

  1. 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çó.

  1. 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.

  1. 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.

  1. 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.

  1. Mètriques amb prom-client

Prometheus é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.

  1. 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.

  1. 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ó.

  1. 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.

  1. 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-pretty actiu en producció. És lent i produeix text que l'agregador no pot indexar.
  • Registrar l'URL completa com a camp agrupable. /api/esdeveniments/evt-003 en comptes de /api/esdeveniments/:idEsdeveniment impedeix 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'idPeticio als 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

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats