A 06-01 vam deixar TechCorp amb logs consultables per requestId i un p95 de POST /v1/comandes de 250 ms. El que aquests senyals no diuen és on se'n van aquests 250 ms: a la crida a Catàleg, a la de Clients, a PostgreSQL, al mateix Express? I quan la petició acaba amb un 202, la saga continua per RabbitMQ en tres serveis més: els logs de cadascun existeixen, però ningú no els uneix en un sol recorregut. Les traces distribuïdes són el senyal que dibuixa aquest camí. Aquesta lliçó instrumenta els serveis de TechCorp amb OpenTelemetry, propaga el context per HTTP i per RabbitMQ, munta el Collector i Jaeger a Kubernetes i ensenya a llegir una traça real de com-88213.
Contingut
- El problema: una petició, quatre serveis i una saga
- Traces, spans i context:
traceparenti la seva relació ambX-Request-Id - OpenTelemetry: API, SDK, instrumentacions, Collector i exportadors
- Instrumentar Node.js:
src/telemetria.jsa la plantilla i al Dockerfile - Spans manuals al cas d'ús
crearComanda - Propagar el context per RabbitMQ: outbox, consumidors i
links - El Collector i Jaeger a Kubernetes
- Llegir una traça:
com-88213i el coll d'ampolla - Correlació traces ↔ logs i traces ↔ mètriques
- Mostreig i cost
- El problema: una petició, quatre serveis i una saga
Seguim la comanda de l'Ana Ruiz des del navegador:
sequenceDiagram participant G as gateway :8080 participant P as servei-comandes :3002 participant C as servei-cataleg :3001 participant K as servei-clients :3004 participant R as RabbitMQ participant I as servei-inventari :3006 G->>P: POST /v1/comandes (X-Request-Id) P->>C: GET /v1/productes?ids=p-501,p-777 P->>K: GET /v1/clients/c-1024 P->>P: INSERT comanda + outbox (pg) P-->>G: 202 Accepted Note over P,R: relay de l'outbox, segons després P->>R: comanda.creada R->>I: inventari.comandes I->>R: estoc.reservat
Amb el que tenim, cada tram deixa un log d'accés amb el seu responseTime i una observació a l'histograma de 06-01. Però per respondre "per què aquesta petició va trigar 1,8 s?" caldria obrir Loki, buscar el requestId, anotar a mà els temps de quatre serveis i restar-los. I la part asíncrona ni tan sols comparteix requestId de manera natural: el consumidor d'Inventari rep un missatge, no una petició.
Una traça fa aquesta feina automàticament: registra cada operació com un span amb inici, durada, atributs i pare, i les uneix amb un identificador comú. Veure-les a Jaeger és veure el sequenceDiagram anterior amb temps reals.
- Traces, spans i context:
traceparent i la seva relació amb X-Request-Id
traceparent i la seva relació amb X-Request-Id| Concepte | Definició | A TechCorp |
|---|---|---|
| Traça (trace) | L'arbre complet d'operacions que provoca una acció; s'identifica per un trace_id de 128 bits |
Tot el que passa arran d'un POST /v1/comandes, saga inclosa |
| Span | Una operació amb nom, inici, fi, atributs, esdeveniments i estat; té span_id i parent_span_id |
POST /v1/comandes a Comandes, GET /v1/productes a Catàleg, pg.query INSERT, crearComanda |
| Context de traça | El parell (trace_id, span_id del pare) més flags, que viatja entre processos |
Capçalera HTTP traceparent; capçalera AMQP traceparent |
| Propagació | Injectar el context en sortir i extreure'l en entrar | Automàtica a fetch/Express; manual a RabbitMQ (apartat 6) |
El format estàndard és W3C Trace Context: una capçalera traceparent amb quatre camps separats per guions:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ │ │ │
│ trace_id (32 hex) span_id pare flags (01 = mostrejada)
versióI una d'opcional, tracestate, per a dades de proveïdors. Tot el que parli HTTP a TechCorp (gateway, serveis, fetch) ha de reenviar traceparent igual que reenviava X-Request-Id.
Substitueix traceparent l'X-Request-Id? No: conviuen amb papers diferents.
X-Request-Id (03-01) |
traceparent (W3C) |
|
|---|---|---|
| Qui el genera | middlewareRequestId() o el gateway |
L'SDK d'OpenTelemetry |
| Què identifica | La petició de negoci, llegible (req-01J5Q…) |
La traça tècnica i l'span pare |
| On es veu | Logs, respostes d'error RFC 7807, suport al client | Jaeger, i com a trace_id als logs (apartat 9) |
| Mostreig | Sempre present | Pot no mostrejar-se (apartat 10) |
Decisió: es mantenen tots dos. El requestId és el que un operador de suport demana al client i busca a Loki; el trace_id és el que el desenvolupador obre a Jaeger. Tots dos apareixen a cada línia de log, de manera que passar de l'un a l'altre és una consulta.
- OpenTelemetry: API, SDK, instrumentacions, Collector i exportadors
OpenTelemetry (OTel) és l'estàndard de la CNCF que unifica com es generen i es transporten traces, mètriques i logs, independentment del proveïdor que les emmagatzemi. Les seves peces:
flowchart LR
subgraph proces [Procés Node.js: servei-comandes]
API[API OTel<br/>tracer.startActiveSpan] --> SDK[SDK<br/>processadors + mostreig]
AUTO[Instrumentacions automàtiques<br/>http, express, pg, mongodb, amqplib] --> SDK
SDK --> EXP[Exportador OTLP]
end
EXP -->|OTLP gRPC :4317| COL[OpenTelemetry Collector<br/>receivers → processors → exporters]
COL --> J[(Jaeger / Tempo)]
COL --> PR[(Prometheus)]
- API: el que fa servir el codi de l'aplicació (
trace.getTracer,startActiveSpan,propagation.inject). És estable i no depèn de la implementació: si no hi ha SDK carregat, no fa res. - SDK: la implementació que crea els spans de debò, aplica el mostreig, els agrupa en lots i els lliura a l'exportador.
- Instrumentacions automàtiques: monkey-patching de llibreries conegudes per crear spans sense tocar el nostre codi:
http(entrant i sortint, inclòs elfetchglobal de Node 20 des de@opentelemetry/instrumentation-undici),express(un span per middleware/ruta),pg,mongodb,amqplib. - Exportadors: envien per OTLP (el protocol natiu d'OTel, gRPC al 4317 o HTTP al 4318) al Collector, o directament a un backend.
- Collector: un procés intermedi que rep, processa (lots, mostreig de cua, enriquiment amb metadades de Kubernetes) i reexporta a un o diversos destins. Desacobla l'aplicació del backend: canviar Jaeger per Tempo és canviar la configuració del Collector, no redesplegar set serveis.
- Instrumentar Node.js:
src/telemetria.js a la plantilla i al Dockerfile
src/telemetria.js a la plantilla i al DockerfileLes instrumentacions automàtiques apedacen els mòduls quan es carreguen, així que l'SDK s'ha d'inicialitzar abans que Express, pg o amqplib. La manera neta és un fitxer a part carregat amb --require, sense tocar servidor.js:
npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-grpc @opentelemetry/resources @opentelemetry/semantic-conventions// src/telemetria.js — s'executa abans que l'aplicació
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { Resource } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions');
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME, // "servei-comandes"
[ATTR_SERVICE_VERSION]: process.env.SERVEI_VERSIO, // "1.4.2", la mateixa que el logger de 06-01
'deployment.environment': process.env.ENTORN || 'local'
}),
traceExporter: new OTLPTraceExporter(), // llegeix OTEL_EXPORTER_OTLP_ENDPOINT
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false }, // soroll: cada lectura de fitxer seria un span
'@opentelemetry/instrumentation-http': {
ignoreIncomingRequestHook: (req) => req.url.startsWith('/health') || req.url === '/metrics'
},
'@opentelemetry/instrumentation-express': { enabled: true },
'@opentelemetry/instrumentation-pg': { enhancedDatabaseReporting: false }, // no incloure els valors dels paràmetres SQL
'@opentelemetry/instrumentation-mongodb': { enabled: true },
'@opentelemetry/instrumentation-amqplib': { enabled: true },
'@opentelemetry/instrumentation-pino': { enabled: true } // trace_id/span_id als logs (apartat 9)
})
]
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown().catch(() => {}).finally(() => process.exit(0)); // buidar el lot d'spans abans de morir
});Explicació:
resourcedescriu qui emet:service.nameés el que Jaeger mostra com a servei;service.versionpermet comparar el canary (05-04) amb la versió estable.OTLPTraceExporter()sense arguments agafa el destí d'OTEL_EXPORTER_OTLP_ENDPOINT, que va alConfigMapdel servei (05-02).getNodeAutoInstrumentationsactiva tot el catàleg; desactivemfsi excloem les sondes i/metricsdels spans entrants per la mateixa raó que als logs de 06-01.enhancedDatabaseReporting: false: l'span depginclou la sentència SQL però no els paràmetres: les dades de l'Ana Ruiz no han de viatjar a Jaeger (mateixa regla que la redacció de logs).- El handler de
SIGTERMés important: els spans s'exporten en lots cada pocs segons; senseshutdown()l'últim lot d'un pod que mor en un rolling es perd. Conviu amb l'aturada ordenada del servidor de 04-02.
Variables d'entorn estàndard d'OTel que afegim a la plantilla de configuració (04-03) i al ConfigMap:
| Variable | Valor a techcorp |
Per a què |
|---|---|---|
OTEL_SERVICE_NAME |
servei-comandes |
Nom del servei a les traces |
OTEL_EXPORTER_OTLP_ENDPOINT |
http://otel-collector.observabilitat.svc.cluster.local:4317 |
Destí OTLP |
OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG |
parentbased_traceidratio / 0.1 en prod, 1.0 en dev |
Mostreig (apartat 10) |
OTEL_PROPAGATORS |
tracecontext,baggage (per defecte) |
W3C Trace Context |
SERVEI_VERSIO |
Injectada per Kustomize amb l'etiqueta de la imatge | service.version i camp versio del logger |
L'arrencada canvia al package.json i al Dockerfile de 05-01:
"scripts": {
"start": "node --require ./src/telemetria.js src/servidor.js",
"dev": "nodemon -r dotenv/config --require ./src/telemetria.js src/servidor.js"
}# última etapa del Dockerfile multi-stage (05-01)
USER node
CMD ["node", "--require", "./src/telemetria.js", "src/servidor.js"]--require carrega el mòdul abans de la primera línia de servidor.js; a partir d'aquí, cada petició Express, cada fetch sortint i cada consulta pg produeix spans sense més codi. Amb això, un POST /v1/comandes ja genera a Jaeger la traça gateway → comandes → catàleg/clients → pg. El que falta és el que la instrumentació automàtica no sap: on comença i acaba la nostra lògica de negoci.
- Spans manuals al cas d'ús
crearComanda
crearComandaUn span propi marca l'operació de negoci i hi afegeix atributs que després es poden cercar a Jaeger. Modifiquem el cas d'ús de 04-04:
// servei-comandes/src/casosUs/crearComanda.js
const { trace, SpanStatusCode } = require('@opentelemetry/api');
const tracer = trace.getTracer('servei-comandes');
function crearCasUsCrearComanda({ repositori, catalegClient, clientsClient, logger, metriques }) {
return async function crearComanda(dades, { requestId }) {
return tracer.startActiveSpan('crearComanda', async (span) => {
span.setAttribute('client.id', dades.clientId);
span.setAttribute('comanda.linies', dades.linies.length);
span.setAttribute('techcorp.request_id', requestId);
try {
const [productes, client] = await Promise.all([
catalegClient.obtenirProductes(dades.linies.map((l) => l.producteId), { requestId }),
clientsClient.obtenirClient(dades.clientId, { requestId })
]);
const comanda = await repositori.desarAmbOutbox(construirComanda(dades, productes, client));
span.setAttribute('comanda.id', comanda.id);
span.setAttribute('comanda.total', comanda.total);
span.addEvent('comanda.persistida');
metriques.creades.inc();
return comanda;
} catch (err) {
span.recordException(err);
span.setStatus({ code: SpanStatusCode.ERROR, message: err.codi || err.message });
throw err;
} finally {
span.end();
}
});
};
}trace.getTracer('servei-comandes'): obté un tracer de l'API. Només importem@opentelemetry/api: el cas d'ús no coneix l'SDK, i a les proves de 04-05 sense SDK carregat els spans són "no-op".startActiveSpan('crearComanda', fn): crea l'span com a fill de l'actiu (l'spanPOST /v1/comandesd'Express) i el deixa actiu durantfn, de manera que elsfetcha Catàleg i Clients i l'INSERTdepgen pengen automàticament. Aquest és el truc del "context actiu": la instrumentació automàtica busca l'span actiu a l'AsyncLocalStoragede Node i s'hi enganxa.- Els atributs segueixen la convenció
domini.camp(comanda.id,client.id); són valors d'alta cardinalitat, que en traces sí que són benvinguts (a diferència de les etiquetes de mètriques de 06-01): Jaeger permet cercarcomanda.id=com-88213. addEventafegeix una marca temporal dins de l'span;recordException+setStatus(ERROR)fan que la traça aparegui en vermell i amb l'stack (sense això, unErrorNegocique acaba en 503 es veuria com un span verd que acaba abans d'hora); ispan.end()va sempre alfinally: un span sense tancar no s'exporta mai.
Els mateixos tres gestos (startActiveSpan, atributs, recordException) s'apliquen a crearCasUsReservarEstoc d'Inventari i a cobrar de Pagaments.
- Propagar el context per RabbitMQ: outbox, consumidors i
links
linksLa instrumentació d'amqplib injecta traceparent a les capçaleres quan es publica dins d'un span actiu i l'extreu en consumir. A TechCorp hi ha una trampa: l'esdeveniment comanda.creada no el publica el cas d'ús, sinó el relay de l'outbox (04-04), en un altre moment i sense cap span actiu relacionat amb la petició. Si no fem res, la saga arrenca una traça nova sense relació amb el POST.
Solució: desar el context a la fila de l'outbox en escriure-la i restaurar-lo en publicar.
// servei-comandes/src/repositori/comandaRepositori.js — en inserir a outbox
const { propagation, context } = require('@opentelemetry/api');
function capcaleresDeContext() {
const portador = {};
propagation.inject(context.active(), portador); // escriu traceparent (i tracestate) a l'objecte
return portador; // { traceparent: '00-4bf9…-00f0…-01' }
}
// INSERT INTO outbox (id, tipus, dades, capcaleres) VALUES ($1, $2, $3, $4)
// capcaleres = { requestId, ...capcaleresDeContext() }propagation.inject agafa el context actiu (som dins de crearComanda, apartat 5) i escriu les capçaleres W3C en un objecte qualsevol; aquest objecte es desa a la columna capcaleres (JSONB) al costat del requestId que ja desàvem a 03-02.
Al relay (missatgeria/relayOutbox.js), en publicar cada fila:
const { propagation, context, trace, SpanKind } = require('@opentelemetry/api');
const tracer = trace.getTracer('servei-comandes');
async function publicarFila(fila) {
const contextOrigen = propagation.extract(context.active(), fila.capcaleres); // reconstrueix el context desat
const spanOrigen = trace.getSpan(contextOrigen)?.spanContext();
await tracer.startActiveSpan(`publicar ${fila.tipus}`, {
kind: SpanKind.PRODUCER,
links: spanOrigen ? [{ context: spanOrigen }] : [], // enllaç, no pare
attributes: { 'messaging.system': 'rabbitmq', 'messaging.destination.name': 'techcorp.esdeveniments', 'esdeveniment.id': fila.id, 'esdeveniment.tipus': fila.tipus }
}, async (span) => {
const capcaleres = { ...fila.capcaleres };
propagation.inject(context.active(), capcaleres); // traceparent de l'span PRODUCER
canal.publish('techcorp.esdeveniments', fila.tipus, Buffer.from(JSON.stringify(fila.dades)), { headers: capcaleres, messageId: fila.id, persistent: true });
span.end();
});
}Aquí hi ha una decisió de disseny. Podríem fer que l'span de publicació fos fill del POST /v1/comandes original, però aquest span va acabar fa segons: la traça mostraria un fill que comença després que el seu pare acabi, i les traces de sagues de 15 minuts serien inmanejables. OTel té el concepte de link per a això: l'span de publicació pertany a la seva pròpia traça (la del relay/la saga) però enllaça amb l'span original. Jaeger mostra els enllaços i permet saltar d'una traça a una altra. La regla a TechCorp:
- Dins d'una petició síncrona (HTTP): relació pare-fill.
- Entre la petició i el processament asíncron (outbox, consumidors): link a l'span d'origen i una traça per esdeveniment.
- Dins d'un consumidor, tot el que provoqui (consultes, publicació de l'esdeveniment següent) és fill de l'span de consum, de manera que cada pas de la saga és una traça petita enllaçada a l'anterior:
POST⇢publicar comanda.creada⇢consumir comanda.creada(Inventari) ⇢publicar estoc.reservat⇢ …
Al consumidor de la saga (missatgeria/consumidorSaga.js) la instrumentació d'amqplib ja crea un span comandes.saga process amb SpanKind.CONSUMER a partir del traceparent de les capçaleres. Només hi afegim atributs i, si el missatge ve d'un reintent o de la DLQ (06-03), conservem les capçaleres per no trencar la cadena. Quan un servei publica sense passar per outbox (Inventari publica estoc.reservat dins de l'span de consum), la injecció automàtica n'hi ha prou.
- El Collector i Jaeger a Kubernetes
L'equip de Plataforma desplega el Collector al namespace observabilitat com a Deployment (dues rèpliques darrere d'un Service otel-collector amb els ports 4317/4318) i la seva configuració en un ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-config
namespace: observabilitat
data:
config.yaml: |
receivers:
otlp:
protocols:
grpc: { endpoint: 0.0.0.0:4317 }
http: { endpoint: 0.0.0.0:4318 }
processors:
batch:
timeout: 5s
send_batch_size: 512
memory_limiter:
check_interval: 1s
limit_mib: 400
k8sattributes: {} # afegeix k8s.namespace, k8s.pod.name, k8s.deployment.name a cada span
tail_sampling: # vegeu l'apartat 10
decision_wait: 10s
policies:
- { name: errors, type: status_code, status_code: { status_codes: [ERROR] } }
- { name: lentes, type: latency, latency: { threshold_ms: 1000 } }
- { name: resta, type: probabilistic, probabilistic: { sampling_percentage: 10 } }
exporters:
otlp/jaeger:
endpoint: jaeger-collector.observabilitat.svc.cluster.local:4317
tls: { insecure: true }
prometheus:
endpoint: 0.0.0.0:8889 # mètriques derivades d'spans per a Prometheus (06-01)
connectors:
spanmetrics: {} # genera mètriques RED (crides, durada) a partir dels spans
service:
pipelines:
traces: { receivers: [otlp], processors: [memory_limiter, k8sattributes, tail_sampling, batch], exporters: [otlp/jaeger, spanmetrics] }
metrics: { receivers: [spanmetrics], exporters: [prometheus] }receivers.otlp: accepta el que envien els serveis (gRPC 4317, HTTP 4318).processors:memory_limiterprotegeix el Collector;k8sattributesetiqueta cada span amb el pod i elDeploymentd'origen (mateixos noms que a Loki: es creuen bé);tail_samplingdecideix quines traces conservar després de veure-les completes;batchagrupa abans d'exportar.exporters.otlp/jaeger: Jaeger v2 accepta OTLP directament. Canviar-lo per Grafana Tempo és canviar l'endpoint; Tempo s'integra millor amb Grafana/Loki, Jaeger té la interfície més coneguda. TechCorp comença amb Jaeger (jaeger-querypublicat internament ajaeger.techcorp.internal).- El connector
spanmetricsgenera mètriques RED (traces_span_metrics_calls_total,traces_span_metrics_duration_milliseconds) a partir dels spans; les exposa al 8889 perquè Prometheus (06-01) les rasqui. És una segona font de RED per servei i operació, útil sobretot per a les crides sortints.
Al ConfigMap de cada servei (05-02) afegim OTEL_EXPORTER_OTLP_ENDPOINT i OTEL_SERVICE_NAME; al compose.yaml de local (05-01) un contenidor jaegertracing/all-in-one amb OTLP activat i la interfície a http://localhost:16686 serveix per desenvolupar sense Collector.
- Llegir una traça:
com-88213 i el coll d'ampolla
com-88213 i el coll d'ampollaLa Marta cerca a Jaeger servei=servei-comandes, comanda.id=com-88213 i obre la traça. Jaeger la mostra com a diagrama de Gantt; en forma de taula (temps ficticis però realistes):
| Servei | Span | Inici (ms) | Durada (ms) | Pare |
|---|---|---|---|---|
| gateway | POST /v1/comandes |
0 | 186 | — |
| gateway | proxy → servei-comandes |
3 | 183 | gateway |
| servei-comandes | POST /v1/comandes (http) |
6 | 180 | proxy |
| servei-comandes | middleware - jsonParser |
6 | 1 | POST |
| servei-comandes | crearComanda |
8 | 176 | POST |
| servei-comandes | GET servei-cataleg:3001/v1/productes (fetch) |
9 | 40 | crearComanda |
| servei-cataleg | GET /v1/productes |
11 | 36 | fetch catàleg |
| servei-cataleg | mongodb.find productes |
14 | 9 | GET productes |
| servei-comandes | GET servei-clients:3004/v1/clients/c-1024 (fetch) |
9 | 25 | crearComanda |
| servei-clients | GET /v1/clients/:id |
11 | 21 | fetch clients |
| servei-clients | pg.query SELECT clients |
13 | 6 | GET clients |
| servei-comandes | pg.query BEGIN / INSERT comandes / INSERT outbox / COMMIT |
52 | 12 | crearComanda |
| servei-comandes | (sense span) | 64 | 120 | crearComanda |
Com es llegeix:
- El gateway afegeix 3 ms de sobrecàrrega: menyspreable.
- Catàleg (40 ms) i Clients (25 ms) s'executen en paral·lel (
Promise.allde l'apartat 5): comencen tots dos al ms 9. Si fossin seqüencials, la traça mostraria el segon començant quan acaba el primer: la primera optimització que una traça sol revelar. - Les consultes a base de dades són ràpides: MongoDB 9 ms, PostgreSQL 6 i 12 ms.
- Sumant: 40 (el més lent del paral·lel) + 12 de
pg= 52 ms de feina real, peròcrearComandadura 176 ms. Hi ha 120 ms sense span entre elCOMMIT(ms 64) i el final de l'span (ms 184). És un forat: temps que va passar dins del nostre codi sense cap operació instrumentada. En mirar el cas d'ús, el Luis troba que després de persistir es crida una funciócalcularRecomanacionsheretada del monòlit que fa un càlcul síncron pesat. Bloqueja l'event loop (06-04) i no aporta res a la comanda. S'elimina i el p95 dePOST /v1/comandescau de 250 a 90 ms.
Regles pràctiques de lectura: buscar l'span més llarg entre germans; buscar forats sense fills (codi propi no instrumentat o event loop bloquejat); comprovar si els fills són seqüencials quan podrien ser paral·lels; i, a la part asíncrona, seguir els links d'una traça a la següent per mesurar la saga completa (publicar comanda.creada a les 10:21:04.6, consumir comanda.confirmada a les 10:21:07.9: 3,3 s, coherent amb l'histograma saga_durada_segons de 06-01).
- Correlació traces ↔ logs i traces ↔ mètriques
Traces ↔ logs. La instrumentació @opentelemetry/instrumentation-pino (activada a l'apartat 4) afegeix automàticament trace_id, span_id i trace_flags a cada línia de log emesa dins d'un span actiu. La línia de 06-01 passa a ser:
{"nivell":"info","time":"2026-08-15T10:21:04.512Z","servei":"servei-comandes","versio":"1.4.2","requestId":"req-01J5Q7X2N9C4M8Z0K1T3V6W8Y","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7","comandaId":"com-88213","missatge":"comanda creada"}Si no es vol dependre d'aquesta instrumentació, un mixin a crearLogger fa el mateix a mà:
const { trace, context } = require('@opentelemetry/api');
// a les opcions de pino():
mixin() {
const span = trace.getSpan(context.active());
if (!span) return {};
const { traceId, spanId } = span.spanContext();
return { trace_id: traceId, span_id: spanId };
}Amb això, a Grafana es configura al datasource de Loki un derived field: expressió "trace_id":"(\w+)" → enllaç a Jaeger/Tempo per trace_id. I en sentit invers, des d'un span a Jaeger, un enllaç a Loki amb {namespace="techcorp"} | json | trace_id="…". L'operador ja no tria entre logs i traces: salta.
Traces ↔ mètriques. Els exemplars permeten que un bucket de l'histograma http_request_duration_seconds porti adjunt el trace_id d'una petició representativa; Grafana els pinta com a punts sobre la gràfica del p95 i un clic obre la traça. Requereix prom-client amb suport d'exemplars i --enable-feature=exemplar-storage a Prometheus. Ho deixem com a menció: TechCorp ho activarà quan la pila sigui estable; el salt mètrica → logs → traça del punt anterior cobreix el mateix cas amb un pas més.
- Mostreig i cost
Cada POST /v1/comandes genera uns 12 spans; amb la saga, uns 30. A 3.000 comandes/dia és poc, però el catàleg serveix centenars de milers de GET /v1/productes i en Black Friday ×20. Desar el 100 % és car (emmagatzematge i CPU al Collector) i gairebé sempre innecessari: les traces "normals" s'assemblen totes. Per això es mostreja:
| Estratègia | On decideix | Avantatge | Inconvenient |
|---|---|---|---|
| Head sampling | Al servei, en crear l'span arrel | Barat; els serveis descarten abans d'exportar | Decideix sense saber si la traça acabarà en error o serà lenta |
| Tail sampling | Al Collector, amb la traça completa | Conserva el 100 % d'errors i lentes | El Collector ha de retenir traces en memòria (decision_wait) i veure tots els spans d'una traça a la mateixa rèplica |
TechCorp combina totes dues:
- Als serveis,
OTEL_TRACES_SAMPLER=parentbased_traceidratioambOTEL_TRACES_SAMPLER_ARG=0.1en producció (10 % de les traces arrel;parentbasedvol dir que si el pare va ser mostrejat, el fill també: una traça mai no surt a mitges) i1.0en dev/staging. - Al Collector, la política
tail_samplingde l'apartat 7: totes les traces amb error o de més d'1 s, i el 10 % de la resta. Com que el head ja va reduir al 10 %, el tail actua sobre aquest subconjunt; perquè els errors no es perdin al head es pot apujar la ràtio dels serveis crítics (Comandes, Pagaments: 0,5) i deixar el 0,1 per a Catàleg. - Retenció a Jaeger: 7 dies. Els
requestIddels logs continuen 30 dies, així que un incident antic s'investiga amb logs encara que la seva traça hagi expirat.
Última nota, per tancar el fil amb 05-05: un service mesh com Istio genera spans a cada sidecar (entrada i sortida de cada pod) i els envia al mateix Collector, però no pot propagar el context dins de l'aplicació: entre la petició entrant i la sortint hi ha codi nostre, i només el nostre procés sap que la segona és conseqüència de la primera. Si algun dia TechCorp adopta el mesh, l'aplicació continuarà necessitant exactament aquesta lliçó: reenviar traceparent, i amb RabbitMQ, injectar-lo i extreure'l a mà.
Errors Comuns i Consells
- Carregar l'SDK després d'Express o
pg. No s'instrumenta res i no hi ha error. Sempre--require ./src/telemetria.js(oNODE_OPTIONS=--require), mai unrequiredins deservidor.jsdesprés d'altres imports. - Oblidar
span.end()en un camí d'error. L'span no s'exporta i la traça queda "oberta" a Jaeger. Fer servir sempretry/finallyo la variant amb callback que tanca sola. - Perdre el context a
setTimeout, cues internes oEventEmitter. L'AsyncLocalStoragesegueixawaiti promises, però unsetTimeouto unemitter.ones poden executar fora del context.context.with(ctx, fn)el restaura. - Fer pare-fill el que ha de ser link. Traces de 15 minuts amb milers d'spans i pares que acaben abans que els seus fills. Asíncron =
links. - Posar dades personals als atributs (
client.email, cossos de peticions). Apliquen les mateixes regles que als logs (06-01, 07-03). - Mostreig 100 % en producció "per no perdre res". Es perd el Collector. Head 10 % + tail per a errors.
- Consell: en local,
jaegertracing/all-in-oneacompose.yamliOTEL_TRACES_SAMPLER_ARG=1.0; veure la traça d'uncurlaPOST /v1/comandesés la millor manera d'entendre l'estructura d'una petició.
Exercicis
Exercici 1: reintent des de la DLQ sense trencar la traça
Un missatge de comandes.saga va anar a comandes.saga.dlq i es reprocessa manualment (06-03). Què cal conservar perquè el nou processament aparegui enllaçat a la traça original? Hauria de ser fill o link? Escriu el fragment que crea l'span de reprocés.
Exercici 2: llegir la traça
En una traça de POST /v1/comandes veus: crearComanda 410 ms; fetch catàleg comença al ms 5 i dura 45 ms; fetch clients comença al ms 52 i dura 30 ms; pg 15 ms a partir del ms 84; res més fins al ms 410. Enumera els dos problemes i què faries amb cadascun.
Solucions
Exercici 1
Cal conservar les capçaleres del missatge original (traceparent, requestId, esdevenimentId) quan l'script mou el missatge de la DLQ a la cua principal. El reprocés passa molt després i per iniciativa humana: ha de ser link, no fill. Fragment de l'script reprocessarDlq.js:
const ctxOriginal = propagation.extract(context.active(), missatge.properties.headers);
const spanOriginal = trace.getSpan(ctxOriginal)?.spanContext();
await tracer.startActiveSpan('reprocessar dlq', { kind: SpanKind.PRODUCER, links: spanOriginal ? [{ context: spanOriginal }] : [],
attributes: { 'esdeveniment.id': missatge.properties.messageId, 'dlq.origen': 'comandes.saga.dlq' } }, async (span) => {
const headers = { ...missatge.properties.headers, 'x-reproces': 'manual' };
propagation.inject(context.active(), headers); // sobreescriu traceparent amb el de l'span de reprocés
canal.publish('techcorp.esdeveniments', missatge.fields.routingKey, missatge.content, { headers, messageId: missatge.properties.messageId, persistent: true });
span.end();
});El consumidor rebrà el traceparent de l'span de reprocés i, a través del link, es podrà arribar a la traça de la fallada original.
Exercici 2
- Crides seqüencials que podrien ser paral·leles. Clients comença al ms 52, just quan acaba Catàleg. Amb
Promise.allla fase de validació duraria 45 ms en comptes de 75. Canvi al cas d'ús. - Forat de ~310 ms sense spans després del
COMMIT(ms 99 → ms 410). És codi propi no instrumentat o event loop bloquejat. Es localitza embolcallant les funcions sospitoses del cas d'ús en spans manuals (o ambnodejs_eventloop_lag_secondsde 06-01 en aquest pod) i s'elimina, es fa asíncron, o es mou fora de la petició (a un esdeveniment). El resultat esperat:crearComandaal voltant de 60-70 ms.
Conclusió
Amb aquesta lliçó, TechCorp veu per fi el camí de cada petició. Cada servei arrenca amb node --require ./src/telemetria.js, que carrega l'SDK d'OpenTelemetry amb instrumentacions automàtiques d'http, express, pg, mongodb, amqplib i pino, s'identifica amb OTEL_SERVICE_NAME i service.version, i exporta per OTLP al Collector; els casos d'ús afegeixen spans manuals (crearComanda, reservarEstoc, cobrar) amb atributs comanda.id/client.id i excepcions registrades; el context W3C traceparent viatja per HTTP al costat de l'X-Request-Id, es desa a la fila de l'outbox i es reinjecta al relay, i les etapes asíncrones de la saga s'uneixen amb links; el Collector aplica k8sattributes, tail_sampling i batch i reexporta a Jaeger i a Prometheus; i trace_id a cada línia de log tanca el triangle amb Loki. En llegir la traça de com-88213 hem trobat un forat de 120 ms que cap mètrica no delatava. Ja podem veure les fallades i les lentituds; la lliçó següent s'ocupa de sobreviure-hi: els patrons de gestió d'errors i recuperació (timeouts, reintents, circuit breaker, bulkhead, cues de reintent i DLQ, el vigilant de la saga) que els mòduls 2, 3 i 5 van anar remetent a 06-03.
Curs de Microserveis
Mòdul 1: Introducció als Microserveis
- Conceptes Bàsics de Microserveis
- Avantatges i Desavantatges dels Microserveis
- Comparació amb l'Arquitectura Monolítica
- Quan Adoptar Microserveis: Criteris de Decisió
- El Cas Pràctic del Curs: la Botiga Online de TechCorp
Mòdul 2: Disseny de Microserveis
- Principis de Disseny de Microserveis
- Descomposició d'Aplicacions Monolítiques
- Definició de Bounded Contexts
- Gestió de Dades: una Base de Dades per Servei
- Consistència Distribuïda: Sagues, CQRS i Event Sourcing
Mòdul 3: Comunicació entre Microserveis
- APIs RESTful
- Missatgeria Asíncrona
- Protocols de Comunicació: gRPC, GraphQL
- API Gateway i Backend for Frontend
- Descobriment de Serveis i Balanceig de Càrrega
- Contractes i Versionat d'APIs
Mòdul 4: Implementació de Microserveis
- Elecció de Tecnologies i Eines
- Desenvolupament d'un Microservei Simple
- Gestió de Configuració
- Integració Pràctica: Consumir APIs i Publicar Esdeveniments
- Proves en Microserveis: Unitàries, d'Integració i de Contracte
Mòdul 5: Desplegament i Orquestració
- Contenidors i Docker
- Orquestració amb Kubernetes
- CI/CD per a Microserveis
- Estratègies de Desplegament: Rolling, Blue-Green i Canary
- Service Mesh: Istio i Linkerd
Mòdul 6: Monitoratge i Manteniment
- Monitoratge i Logging
- Traçabilitat Distribuïda amb OpenTelemetry
- Gestió d'Errors i Recuperació
- Escalabilitat i Rendiment
- SLOs, Alertes i Gestió d'Incidents
Mòdul 7: Seguretat en Microserveis
- Autenticació i Autorització
- Seguretat en la Comunicació
- Pràctiques de Seguretat
- Seguretat en Contenidors i Kubernetes
