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:

Grafana - Tempo Trace

API-ul

Să începem prin a configura proiectul NodeJS și prin a instala dependențele:

npm install fastify

Iar 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-conventions

Acum 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:

  1. Mai întâi declarăm tracer-ul:
const opentelemetry = require('@opentelemetry/api');const tracer = opentelemetry.trace.getTracer(  'cool-api-root-handlers',  'v1.0.0',);
  1. 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}})  });})
  1. 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/blocks

Acum 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ă:

  1. Fă clic pe iconița roată din bara laterală din stânga
  2. Fă clic pe Data Sources
  3. Fă clic pe Add data source
  4. Selectează Tempo din listă
  5. Setează URL-ul la http://tempo:3200
  6. Fă clic pe Save & Test

Grafana - Tempo Datasource

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-proto

Acum î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.

Grafana - Tempo Traces

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

Grafana - Tempo Trace

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)    }})