Trasarea API-urilor Node.js cu OpenTelemetry
- 6 minute de citit
- 16 exemple de cod în JavaScript, YAML și Shell
- 4 imagini
Disponibil și în English
Pe această pagină
Salut tuturor 👋,
În acest articol vom instrumenta un API simplu în Node.js cu trasări (traces) OpenTelemetry.
OpenTelemetry este noul standard pentru trasarea distribuită și pentru metrici. Datele de trasare sunt folosite pentru a monitoriza și a depana sisteme distribuite complexe.
La finalul acestui articol vom avea un API Node.js instrumentat cu OpenTelemetry și vom putea vedea trasările în Grafana Tempo:

API-ul
Să începem prin a configura proiectul NodeJS și prin a instala dependențele:
npm install fastifyIar API-ul pe care îl vom instrumenta este următorul:
// Importăm framework-ul și îl instanțiemconst fastify = require('fastify')({logger: true})async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms));}async function doWork1() { await sleep(1000); return {work_one: 1};}async function doWork2() { await sleep(1000); return {work_two: 2};}// Declarăm o rutăfastify.get('/', async function handler(request, reply) { let [doWork1Data, doWork2Data] = await Promise.all([doWork1(), doWork2()]) reply.send({hello: 'world', data: {...doWork1Data, ...doWork2Data}})})// Pornim serverul!fastify.listen({port: 4000}, (err) => { if (err) { fastify.log.error(err) process.exit(1) }})Să presupunem că în funcțiile doWork1 și doWork2 facem o muncă grea care durează 1 secundă, cum ar fi
interogări complexe de agregare în ElasticSearch sau o altă operație I/O.
OpenTelemetry - SDK pentru NodeJS
Să începem prin a instala SDK-ul OpenTelemetry pentru NodeJS:
Notă: urmez instrucțiunile din OpenTelemetry - JavaScript Manual
npm install @opentelemetry/sdk-node @opentelemetry/api @opentelemetry/resources @opentelemetry/semantic-conventionsAcum că pachetele sunt instalate, putem începe instrumentarea API-ului nostru.
Actualizează codul API-ului astfel:
// Declarăm o rută// [...]// Configurarea OpenTelemetryconst {NodeSDK} = require('@opentelemetry/sdk-node');const {ConsoleSpanExporter} = require('@opentelemetry/sdk-trace-node');const {Resource} = require('@opentelemetry/resources');const {SemanticResourceAttributes,} = require('@opentelemetry/semantic-conventions');const sdk = new NodeSDK({ resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'our-cool-fastify-api', [SemanticResourceAttributes.SERVICE_VERSION]: '0.1.0', }), traceExporter: new ConsoleSpanExporter(),});sdk.start()// Pornim serverul!// [...]Notă: importurile trebuie plasate la începutul fișierului.
Acum SDK-ul OpenTelemetry este configurat să exporte trasările în consolă. Mai târziu îl vom configura să exporte către un OpenTelemetry Collector.
OpenTelemetry - Instrumentare manuală
Acest articol se concentrează pe instrumentarea manuală. Dacă ești interesat de instrumentarea automată, consultă pagina OpenTelemetry Automatic Instrumentation.
Trasarea API-ului
Actualizează codul API-ului astfel:
- Mai întâi declarăm tracer-ul:
const opentelemetry = require('@opentelemetry/api');const tracer = opentelemetry.trace.getTracer( 'cool-api-root-handlers', 'v1.0.0',);- Apoi înfășurăm funcțiile handler cu tracer-ul:
async function doWork1() { return await tracer.startActiveSpan('doWork1', async (span) => { span.setAttribute('work1', 'work1'); await sleep(1000); span.end() return {work_one: 1}; });}async function doWork2() { return await tracer.startActiveSpan('doWork2', async (span) => { span.setAttribute('work2', 'work2'); await sleep(1000); span.end() return {work_two: 2}; });}// Declarăm o rutăfastify.get('/', async function handler(request, reply) { await tracer.startActiveSpan('root-handler', async (span) => { span.setAttribute('path', '/'); let [doWork1Data, doWork2Data] = await Promise.all([doWork1(), doWork2()]) span.end() reply.send({hello: 'world', data: {...doWork1Data, ...doWork2Data}}) });})- Rulează API-ul și verifică ieșirea din consolă:
{ traceId: '6962e5a9a070979ab2409af6e6b4b1e9', parentId: 'b991c5da04183817', traceState: undefined, name: 'doWork1', id: '8ef739b49dbc41eb', kind: 0, timestamp: 1691435811423000, duration: 1006130, attributes: { work1: 'work1' }, status: { code: 0 }, events: [], links: []}API-ul nostru este acum instrumentat cu OpenTelemetry!
Vizualizarea trasărilor
Pentru a vizualiza trasările, vom folosi Grafana și Tempo. În această secțiune vom configura o instanță locală de Grafana și Tempo folosind Docker și Docker Compose. Presupun că ești familiarizat cu Docker, Docker Compose și Grafana.
Grafana și Tempo
Pentru a configura Grafana și Tempo, voi folosi fișierele de configurare din acest repo GitHub.
Creează un fișier docker-compose.yaml cu următorul conținut:
version: "3"services: tempo: image: grafana/tempo:latest command: [ "-config.file=/etc/tempo.yaml" ] volumes: - ./tempo.yaml:/etc/tempo.yaml - ./tempo-data:/tmp/tempo ports: - "3200" # tempo - "4317" # otlp grpc - "4318" # otlp http grafana: image: grafana/grafana:9.4.3 ports: - "3000:3000"Creează un fișier tempo.yaml cu următorul conținut. Este o configurare minimală pentru Tempo.
server: http_listen_port: 3200distributor: receivers: # această configurare ascultă pe toate porturile și protocoalele pe care le poate folosi tempo. otlp: protocols: http: grpc:ingester: max_block_duration: 5m # taie headblock-ul când trece atâta timp. este setat pentru demo și de obicei ar trebui lăsat neschimbatcompactor: compaction: block_retention: 1h # retenția generală a trasărilor Tempo. setată pentru demostorage: trace: backend: local # configurarea backend-ului de folosit wal: path: /tmp/tempo/wal # unde să stocăm local wal-ul local: path: /tmp/tempo/blocksAcum rulează docker-compose up și așteaptă ca serviciile să pornească.
Grafana - Sursă de date Tempo
Deschide http://localhost:3000 în browser și autentifică-te cu datele implicite:
- username:
admin - password:
admin
Adaugă o sursă de date nouă:
- Fă clic pe iconița roată din bara laterală din stânga
- Fă clic pe
Data Sources - Fă clic pe
Add data source - Selectează
Tempodin listă - Setează URL-ul la
http://tempo:3200 - Fă clic pe
Save & Test

Acum avem o instanță Grafana funcțională, cu o sursă de date Tempo.
Putem explora datele din Tempo accesând http://localhost:3000/explore și
selectând sursa de date Tempo.
OpenTelemetry Collector
OpenTelemetry Collector este un agent care poate primi, procesa și exporta date de telemetrie. Îl vom folosi ca sidecar pentru API-ul nostru. Collector-ul va primi trasările de la API și le va exporta către Tempo.
OpenTelemetry Collector - Configurare
Adaugă collector-ul în fișierul docker-compose.yaml:
services: otel-collector: image: otel/opentelemetry-collector-contrib volumes: - ./collector.yaml:/etc/otelcol-contrib/config.yaml ports: - "1888:1888" # extensia pprof - "8888:8888" # metrici Prometheus expuse de collector - '8889:8889' # metrici exportate prin Prometheus - '13133:13133' # extensia health_check - "4317:4317" # receptor OTLP gRPC - "4318:4318" # receptor OTLP http - "55679:55679" # extensia zpages# [...]Creează un fișier collector.yaml cu următorul conținut. Este o configurare minimală pentru Collector, care trimite
trasările grupate (batch) către tempo:4317.
receivers: otlp: protocols: grpc: http:processors: batch:exporters: otlp: endpoint: "tempo:4317" tls: insecure: trueextensions: health_check: pprof: zpages:service: extensions: [health_check, pprof, zpages] pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlp] metrics: receivers: [otlp] processors: [batch] exporters: [otlp] logs: receivers: [otlp] processors: [batch] exporters: [otlp]Dacă rulezi din nou docker-compose up, serviciul otel-collector ar trebui să pornească. Ieșirea din consolă ar trebui
să arate astfel:
otel-collector-1 | 2023-08-07T19:37:11.863Z info otlpreceiver/otlp.go:70 Starting GRPC server on endpoint 0.0.0.0:4317 {"kind": "receiver", "name": "otlp", "pipeline": "traces"}otel-collector-1 | 2023-08-07T19:37:11.863Z info otlpreceiver/otlp.go:88 Starting HTTP server on endpoint 0.0.0.0:4318 {"kind": "receiver", "name": "otlp", "pipeline": "traces"}otel-collector-1 | 2023-08-07T19:37:11.864Z info service/service.go:129 Everything is ready. Begin running and processing data.Înapoi la API
SDK-ul OpenTelemetry este configurat să scrie trasările în consolă. Vom schimba asta, astfel încât trasările să fie trimise către OpenTelemetry Collector.
Instalează pachetele exportatorului OLTP:
npm install --save @opentelemetry/exporter-trace-otlp-proto @opentelemetry/exporter-metrics-otlp-protoAcum înlocuiește ConsoleSpanExporter cu OTLPTraceExporter:
const {OTLPTraceExporter} = require("@opentelemetry/exporter-trace-otlp-proto");// Configurarea OpenTelemetryconst sdk = new NodeSDK({ resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'our-cool-fastify-api', [SemanticResourceAttributes.SERVICE_VERSION]: '0.1.0', }), traceExporter: new OTLPTraceExporter(),});sdk.start()Dacă rulezi API-ul cu node main.js și accesezi / în browser, ar trebui să vezi trasările în Tempo după puțin timp.

Și atât! Acum ai o configurare OpenTelemetry funcțională cu Tempo și Grafana.

Concluzie
OpenTelemetry combinat cu Tempo sunt instrumente puternice pentru monitorizarea și depanarea sistemelor tale complexe.
Totuși, configurarea poate fi destul de complexă dacă nu ești familiarizat cu aceste instrumente.
Mulțumesc că ai citit! 📚
Resurse suplimentare
Docker Compose:
version: "3"services: otel-collector: image: otel/opentelemetry-collector-contrib volumes: - ./collector.yaml:/etc/otelcol-contrib/config.yaml ports: - "1888:1888" # extensia pprof - "8888:8888" # metrici Prometheus expuse de collector - '8889:8889' # metrici exportate prin Prometheus - '13133:13133' # extensia health_check - "4317:4317" # receptor OTLP gRPC - "4318:4318" # receptor OTLP http - "55679:55679" # extensia zpages tempo: image: grafana/tempo:latest command: [ "-config.file=/etc/tempo.yaml" ] volumes: - ./tempo.yaml:/etc/tempo.yaml - ./tempo-data:/tmp/tempo ports: - "3200" # tempo - "4317" # otlp grpc - "4318" # otlp http grafana: image: grafana/grafana:9.4.3 ports: - "3000:3000"Main.js
// Importăm framework-ul și îl instanțiemconst fastify = require('fastify')({logger: true})const {NodeSDK} = require('@opentelemetry/sdk-node');const {ConsoleSpanExporter} = require('@opentelemetry/sdk-trace-node');const {Resource} = require('@opentelemetry/resources');const {SemanticResourceAttributes,} = require('@opentelemetry/semantic-conventions');const opentelemetry = require('@opentelemetry/api');const {OTLPTraceExporter} = require("@opentelemetry/exporter-trace-otlp-proto");const tracer = opentelemetry.trace.getTracer( 'cool-api-root-handlers', 'v1.0.0',);async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms));}async function doWork1() { return await tracer.startActiveSpan('doWork1', async (span) => { span.setAttribute('work1', 'work1'); await sleep(100); span.end() return {work_one: 1}; });}async function doWork2() { return await tracer.startActiveSpan('doWork2', async (span) => { span.setAttribute('work2', 'work2'); await sleep(100); span.end() return {work_two: 2}; });}// Declarăm o rutăfastify.get('/', async function handler(request, reply) { await tracer.startActiveSpan('root-handler', async (span) => { span.setAttribute('path', '/'); let [doWork1Data, doWork2Data] = await Promise.all([doWork1(), doWork2()]) span.end() reply.send({hello: 'world', data: {...doWork1Data, ...doWork2Data}}) });})// Configurarea OpenTelemetryconst sdk = new NodeSDK({ resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'our-cool-fastify-api', [SemanticResourceAttributes.SERVICE_VERSION]: '0.1.0', }), traceExporter: new OTLPTraceExporter(),});sdk.start()// Pornim serverul!fastify.listen({port: 4000}, (err) => { if (err) { fastify.log.error(err) process.exit(1) }})