Instrumentação

Instrumentação para OpenTelemetry JavaScript

Instrumentação é o ato de adicionar código de observabilidade a uma aplicação por conta própria.

Se você estiver instrumentando uma aplicação, será necessário utilizar o SDK do OpenTelemetry para sua linguagem. Você irá utilizar o SDK para inicializar o OpenTelemetry e a API para instrumentar seu código. Isso passará a emitir dados de telemetria da sua aplicação e de qualquer biblioteca que você tenha instalado que também possua instrumentação.

Se você estiver instrumentando uma biblioteca, instale apenas o pacote da API do OpenTelemetry para sua linguagem. Sua biblioteca não emitirá telemetria por conta própria; ela só emitirá telemetria quando fizer parte de uma aplicação que utiliza o SDK do OpenTelemetry. Para mais informações sobre a instrumentação de bibliotecas, consulte a seção Bibliotecas.

Para mais informações sobre a API e o SDK do OpenTelemetry, consulte a especificação.

Preparação da aplicação de exemplo

Esta página utiliza uma versão modificada da aplicação de exemplo mostrada em Primeiros Passos para auxiliar no aprendizado sobre instrumentação manual.

Não é obrigatório utilizar a aplicação de exemplo: caso deseje instrumentar uma aplicação ou biblioteca própria, basta seguir as instruções desta seção para adaptar o processo ao seu código.

Dependências

Crie um arquivo package.json vazio do NPM em um novo diretório:

npm init -y

Em seguida, instale as dependências do Express.

npm install express @types/express
npm install -D tsx  # ferramenta para executar arquivos TypeScript (.ts) diretamente com node
npm install express

Criar e iniciar um servidor HTTP

Para destacar a diferença entre instrumentar uma biblioteca e uma aplicação, separe a lógica de rolagem de dados em um arquivo de biblioteca, que será importado como dependência pelo arquivo da aplicação.

Crie o arquivo de biblioteca chamado dice.ts (ou dice.js caso não esteja utilizando TypeScript) e adicione o seguinte código:

/*dice.ts*/
function rollOnce(min: number, max: number) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

export function rollTheDice(rolls: number, min: number, max: number) {
  const result: number[] = [];
  for (let i = 0; i < rolls; i++) {
    result.push(rollOnce(min, max));
  }
  return result;
}
/*dice.js*/
function rollOnce(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

function rollTheDice(rolls, min, max) {
  const result = [];
  for (let i = 0; i < rolls; i++) {
    result.push(rollOnce(min, max));
  }
  return result;
}

module.exports = { rollTheDice };

Crie o arquivo da aplicação chamado app.ts (ou app.js caso não esteja utilizando TypeScript) e adicione o seguinte código:

/*app.ts*/
import express, { type Express } from 'express';
import { rollTheDice } from './dice';

const PORT: number = parseInt(process.env.PORT || '8080');
const app: Express = express();

app.get('/rolldice', (req, res) => {
  const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
  if (isNaN(rolls)) {
    res
      .status(400)
      .send("O parâmetro 'rolls' está ausente ou não é um número.");
    return;
  }
  res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});

app.listen(PORT, () => {
  console.log(`Escutando requisições em http://localhost:${PORT}`);
});
/*app.js*/
const express = require('express');
const { rollTheDice } = require('./dice.js');

const PORT = parseInt(process.env.PORT || '8080');
const app = express();

app.get('/rolldice', (req, res) => {
  const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
  if (isNaN(rolls)) {
    res
      .status(400)
      .send("O parâmetro 'rolls' está ausente ou não é um número.");
    return;
  }
  res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});

app.listen(PORT, () => {
  console.log(`Escutando requisições em http://localhost:${PORT}`);
});

Para garantir que tudo está funcionando, execute a aplicação com o seguinte comando e abra http://localhost:8080/rolldice?rolls=12 no navegador.

$ npx tsx app.ts
Escutando requisições em http://localhost:8080
$ node app.js
Escutando requisições em http://localhost:8080

Configuração de instrumentação manual

Dependências

Instale os pacotes da API do OpenTelemetry:

npm install @opentelemetry/api @opentelemetry/resources @opentelemetry/semantic-conventions

Inicializar o SDK

Para instrumentar uma aplicação Node.js, instale o SDK OpenTelemetry para Node.js:

npm install @opentelemetry/sdk-node

Antes que qualquer outro módulo da aplicação seja carregado, é necessário inicializar o SDK. Caso o SDK não seja inicializado, ou seja inicializado tarde demais, implementações no-op (pronunciada “no-op”, de “no operation”, significando “sem operação”) serão fornecidas a qualquer biblioteca que obtenha um Tracer ou Meter da API.

/*instrumentation.ts*/
import { NodeSDK } from '@opentelemetry/sdk-node';
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';
import {
  PeriodicExportingMetricReader,
  ConsoleMetricExporter,
} from '@opentelemetry/sdk-metrics';
import { resourceFromAttributes } from '@opentelemetry/resources';
import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'nomeDoServico',
    [ATTR_SERVICE_VERSION]: '1.0',
  }),
  traceExporter: new ConsoleSpanExporter(),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new ConsoleMetricExporter(),
  }),
});

sdk.start();
/*instrumentation.mjs*/
import { NodeSDK } from '@opentelemetry/sdk-node';
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';
import {
  PeriodicExportingMetricReader,
  ConsoleMetricExporter,
} from '@opentelemetry/sdk-metrics';
import { resourceFromAttributes } from '@opentelemetry/resources';
import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'dice-server',
    [ATTR_SERVICE_VERSION]: '0.1.0',
  }),
  traceExporter: new ConsoleSpanExporter(),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new ConsoleMetricExporter(),
  }),
});

sdk.start();

Para fins de depuração (debugging) e desenvolvimento local, o exemplo a seguir exporta telemetria para o console. Após concluir a configuração da instrumentação manual, será necessário configurar um exportador apropriado para exportar os dados de telemetria da aplicação para um ou mais backends de observabilidade.

O exemplo também define o atributo padrão obrigatório do SDK service.name, que contém o nome lógico do serviço, e o atributo opcional (porém altamente recomendado!) service.version, que contém a versão da API ou implementação do serviço.

Existem métodos alternativos para definir atributos de recurso. Para mais informações, consulte Recursos.

npx tsx --import ./instrumentation.ts app.ts
node --import ./instrumentation.mjs app.js

Esta configuração básica ainda não tem efeito na aplicação. É necessário adicionar código para rastros, métricas e/ou logs.

Também é possível registrar bibliotecas de instrumentação com o SDK OpenTelemetry para Node.js a fim de gerar dados de telemetria para as dependências. Para mais informações, veja Bibliotecas.

Rastros

Inicializar rastros

Para habilitar rastros em uma aplicação, será necessário ter um TracerProvider inicializado, que permitirá criar um Tracer.

Caso um TracerProvider não seja criado, as APIs do OpenTelemetry irão utilizar uma implementação no-op e não irão gerar dados. Conforme explicado a seguir, o arquivo instrumentation.ts (ou instrumentation.js) deve incluir todo o código de inicialização do SDK.

Node.js

Caso as instruções para inicializar o SDK acima tenham sido seguidas, já existe um TracerProvider configurado. É possível continuar com obter um Tracer.

Navegador

Primeiro, certifique-se de ter instalado os pacotes corretos:

npm install @opentelemetry/sdk-trace-web

Em seguida, atualize o arquivo instrumentation.ts (ou instrumentation.js) para conter todo o código de inicialização do SDK:

import {
  defaultResource,
  resourceFromAttributes,
} from '@opentelemetry/resources';
import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import {
  BatchSpanProcessor,
  ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-base';

const resource = defaultResource().merge(
  resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'nome-do-servico',
    [ATTR_SERVICE_VERSION]: '0.1.0',
  }),
);

const exporter = new ConsoleSpanExporter();
const processor = new BatchSpanProcessor(exporter);

const provider = new WebTracerProvider({
  resource: resource,
  spanProcessors: [processor],
});

provider.register();
const opentelemetry = require('@opentelemetry/api');
const {
  defaultResource,
  resourceFromAttributes,
} = require('@opentelemetry/resources');
const {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} = require('@opentelemetry/semantic-conventions');
const { WebTracerProvider } = require('@opentelemetry/sdk-trace-web');
const {
  ConsoleSpanExporter,
  BatchSpanProcessor,
} = require('@opentelemetry/sdk-trace-base');

const resource = defaultResource().merge(
  resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'nome-do-servico',
    [ATTR_SERVICE_VERSION]: '0.1.0',
  }),
);

const exporter = new ConsoleSpanExporter();
const processor = new BatchSpanProcessor(exporter);

const provider = new WebTracerProvider({
  resource: resource,
  spanProcessors: [processor],
});

provider.register();

Será necessário gerar um pacote que inclua este arquivo na sua aplicação para que seja possível utilizar os rastros em todo o restante da aplicação web.

Isso ainda não terá efeito algum na sua aplicação: é preciso criar trechos para que a telemetria seja emitida.

Escolhendo o processador de trechos correto

Por padrão, o SDK do Node utiliza o BatchSpanProcessor, e este processador de trechos também é escolhido no exemplo do SDK Web. O BatchSpanProcessor processa trechos em lotes antes de serem exportados. Geralmente, este é o processador correto a ser utilizado por uma aplicação.

Por outro lado, o SimpleSpanProcessor processa trechos conforme são criados. Isso significa que se forem criados 5 trechos, cada um deles será processado e exportado antes que o próximo seja criado no código. Esse comportamento pode ser útil em cenários nos quais não se deseja correr o risco de perder um lote, ou ao experimentar o OpenTelemetry em ambiente de desenvolvimento. No entanto, esta escolha também pode gerar uma sobrecarga significativa, especialmente se os trechos estiverem sendo exportados por uma rede — cada vez que um trecho é criado, este trecho seria processado e enviado antes que a execução da aplicação pudesse continuar.

Na maioria dos casos, utilize BatchSpanProcessor em vez de SimpleSpanProcessor.

Obtendo um Tracer

Em qualquer parte da sua aplicação onde for escrito código de rastreamento manual, deve-se chamar getTracer para obter um Tracer. Exemplo:

import opentelemetry from '@opentelemetry/api';
//...

const tracer = opentelemetry.trace.getTracer(
  'nome-do-escopo-de-instrumentacao',
  'versao-do-escopo-de-instrumentacao',
);

// Agora é possível usar 'tracer' para fazer rastreamento!
const opentelemetry = require('@opentelemetry/api');
//...

const tracer = opentelemetry.trace.getTracer(
  'nome-do-escopo-de-instrumentacao',
  'versao-do-escopo-de-instrumentacao',
);

// Agora é possível usar 'tracer' para fazer rastreamento!

Os valores nome-do-escopo-de-instrumentacao e versao-do-escopo-de-instrumentacao devem identificar exclusivamente o Escopo de Instrumentação, como o nome de um pacote, módulo ou classe. O nome é obrigatório, enquanto a versão, embora opcional, é recomendada.

De modo geral, recomenda-se chamar getTracer na aplicação sempre que for necessário, em vez de exportar a instância de tracer para o restante do código. Essa abordagem ajuda a evitar problemas mais complexos de carregamento da aplicação quando há outras dependências envolvidas.

No caso da aplicação de exemplo, há dois pontos em que um Tracer pode ser obtido com o Escopo de Instrumentação apropriado:

Primeiro, no arquivo da aplicação app.ts (ou app.js):

/*app.ts*/
import { trace } from '@opentelemetry/api';
import express, { type Express } from 'express';
import { rollTheDice } from './dice';

const tracer = trace.getTracer('dice-server', '0.1.0');

const PORT: number = parseInt(process.env.PORT || '8080');
const app: Express = express();

app.get('/rolldice', (req, res) => {
  const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
  if (isNaN(rolls)) {
    res
      .status(400)
      .send("O parâmetro 'rolls' está ausente ou não é um número.");
    return;
  }
  res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});

app.listen(PORT, () => {
  console.log(`Escutando requisições em http://localhost:${PORT}`);
});
/*app.js*/
const { trace } = require('@opentelemetry/api');
const express = require('express');
const { rollTheDice } = require('./dice.js');

const tracer = trace.getTracer('dice-server', '0.1.0');

const PORT = parseInt(process.env.PORT || '8080');
const app = express();

app.get('/rolldice', (req, res) => {
  const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
  if (isNaN(rolls)) {
    res
      .status(400)
      .send("O parâmetro 'rolls' está ausente ou não é um número.");
    return;
  }
  res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});

app.listen(PORT, () => {
  console.log(`Escutando requisições em http://localhost:${PORT}`);
});

E segundo, no arquivo de biblioteca dice.ts (ou dice.js):

/*dice.ts*/
import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('dice-lib');

function rollOnce(min: number, max: number) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

export function rollTheDice(rolls: number, min: number, max: number) {
  const result: number[] = [];
  for (let i = 0; i < rolls; i++) {
    result.push(rollOnce(min, max));
  }
  return result;
}
/*dice.js*/
const { trace } = require('@opentelemetry/api');

const tracer = trace.getTracer('dice-lib');

function rollOnce(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

function rollTheDice(rolls, min, max) {
  const result = [];
  for (let i = 0; i < rolls; i++) {
    result.push(rollOnce(min, max));
  }
  return result;
}

module.exports = { rollTheDice };

Criar trechos (spans)

Agora que os Tracers foram inicializados, é possível criar trechos.

A API do OpenTelemetry JavaScript oferece dois métodos para criar trechos:

  • tracer.startSpan: Inicia um novo trecho sem defini-lo no contexto.
  • tracer.startActiveSpan: Inicia um novo trecho e executa a função de callback fornecida, passando o trecho criado como primeiro argumento. O novo trecho é definido no contexto ativo enquanto a função é executada.

Na maioria dos casos, é preferível utilizar tracer.startActiveSpan, pois ele gerencia automaticamente o contexto ativo.

O código a seguir ilustra como criar um trecho ativo.

import { trace, type Span } from '@opentelemetry/api';

/* ... */

export function rollTheDice(rolls: number, min: number, max: number) {
  // Cria um trecho ativo. O trecho deve ser finalizado.
  return tracer.startActiveSpan('rollTheDice', (span: Span) => {
    const result: number[] = [];
    for (let i = 0; i < rolls; i++) {
      result.push(rollOnce(min, max));
    }
    // Certifique-se de encerrar o trecho!
    span.end();
    return result;
  });
}
function rollTheDice(rolls, min, max) {
  // Cria um trecho ativo. O trecho deve ser finalizado.
  return tracer.startActiveSpan('rollTheDice', (span) => {
    const result = [];
    for (let i = 0; i < rolls; i++) {
      result.push(rollOnce(min, max));
    }
    // Certifique-se de encerrar o trecho!
    span.end();
    return result;
  });
}

Caso as instruções utilizando a aplicação de exemplo tenham sido seguidas até este ponto, é possível copiar o código acima no arquivo de biblioteca dice.ts (ou dice.js). Você poderá visualizar trechos sendo emitidos pela sua aplicação.

Execute o comando abaixo para iniciar a aplicação e, em seguida, envie requisições acessando http://localhost:8080/rolldice?rolls=12 pelo navegador ou utilizando o curl.

npx tsx --import ./instrumentation.ts app.ts
node --import ./instrumentation.mjs app.js

Após algum tempo, os trechos poderão ser vistos impressos no console pelo ConsoleSpanExporter, similar a isto:

{
  resource: {
    attributes: {
      'service.name': 'dice-server',
      'service.version': '0.1.0',
      // ...
    }
  },
  instrumentationScope: { name: 'dice-lib', version: undefined, schemaUrl: undefined },
  traceId: '30d32251088ba9d9bca67b09c43dace0',
  parentSpanContext: undefined,
  traceState: undefined,
  name: 'rollTheDice',
  id: 'cc8a67c2d4840402',
  kind: 0,
  timestamp: 1756165206470000,
  duration: 35.584,
  attributes: {},
  status: { code: 0 },
  events: [],
  links: []
}

Criar trechos aninhados

Trechos aninhados permitem rastrear operações que são aninhadas por natureza. Por exemplo, a função rollOnce() abaixo representa uma operação aninhada. O exemplo a seguir cria um trecho aninhado que rastreia rollOnce().

function rollOnce(i: number, min: number, max: number) {
  return tracer.startActiveSpan(`rollOnce:${i}`, (span: Span) => {
    const result = Math.floor(Math.random() * (max - min + 1) + min);
    span.end();
    return result;
  });
}

export function rollTheDice(rolls: number, min: number, max: number) {
  // Cria um trecho ativo. O trecho deve ser finalizado.
  return tracer.startActiveSpan('rollTheDice', (parentSpan: Span) => {
    const result: number[] = [];
    for (let i = 0; i < rolls; i++) {
      result.push(rollOnce(i, min, max));
    }
    // Certifique-se de encerrar o trecho!
    parentSpan.end();
    return result;
  });
}
function rollOnce(i, min, max) {
  return tracer.startActiveSpan(`rollOnce:${i}`, (span) => {
    const result = Math.floor(Math.random() * (max - min + 1) + min);
    span.end();
    return result;
  });
}

function rollTheDice(rolls, min, max) {
  // Cria um trecho ativo. O trecho deve ser finalizado.
  return tracer.startActiveSpan('rollTheDice', (parentSpan) => {
    const result = [];
    for (let i = 0; i < rolls; i++) {
      result.push(rollOnce(i, min, max));
    }
    // Certifique-se de encerrar o trecho!
    parentSpan.end();
    return result;
  });
}

Este código cria um trecho filho para cada rolagem, tendo o ID do parentSpan como seu ID de pai (parent ID):

{
  traceId: '6469e115dc1562dd768c999da0509615',
  parentSpanContext: {
    traceId: '6469e115dc1562dd768c999da0509615',
    spanId: '38691692d6bc3395',
    // ...
  },
  name: 'rollOnce:0',
  id: '36423bc1ce7532b0',
  timestamp: 1756165362215000,
  duration: 85.667,
  // ...
}
{
  traceId: '6469e115dc1562dd768c999da0509615',
  parentSpanContext: {
    traceId: '6469e115dc1562dd768c999da0509615',
    spanId: '38691692d6bc3395',
    // ...
  },
  name: 'rollOnce:1',
  id: 'ed9bbba2264d6872',
  timestamp: 1756165362215000,
  duration: 16.834,
  // ...
}
{
  traceId: '6469e115dc1562dd768c999da0509615',
  parentSpanContext: undefined,
  name: 'rollTheDice',
  id: '38691692d6bc3395',
  timestamp: 1756165362214000,
  duration: 1022.209,
  // ...
}

Criar trechos independentes

Os exemplos anteriores mostraram como criar um trecho ativo. Em alguns casos, você vai querer criar trechos inativos que são irmãos uns dos outros, em vez de serem aninhados.

const doWork = () => {
  const span1 = tracer.startSpan('work-1');
  // realizar alguma operação
  const span2 = tracer.startSpan('work-2');
  // realizar mais alguma operação
  const span3 = tracer.startSpan('work-3');
  // realizar ainda mais operações

  span1.end();
  span2.end();
  span3.end();
};

Neste exemplo, span1, span2 e span3 são trechos irmãos e nenhum deles é considerado o trecho ativo no momento. Eles compartilham o mesmo pai, em vez de serem aninhados uns sob os outros.

Essa disposição pode ser útil caso você tenha unidades de trabalho que estão agrupadas, mas são conceitualmente independentes umas das outras.

Obter o trecho atual

Às vezes, é útil fazer algo com o trecho atual/ativo em um determinado ponto na execução da aplicação.

const activeSpan = opentelemetry.trace.getActiveSpan();

// fazer algo com o trecho ativo, opcionalmente encerrando-o se for apropriado para o caso de uso.

Obter um trecho do contexto

Também pode ser útil obter o trecho de um determinado contexto que não é necessariamente o trecho ativo.

const ctx = getContextFromSomewhere();
const span = opentelemetry.trace.getSpan(ctx);

// fazer algo com o trecho adquirido, opcionalmente encerrando-o se for apropriado para o caso de uso.

Atributos

Atributos permitem anexar pares de chave/valor a um Trecho para que ele carregue mais informações sobre a operação atual que está sendo rastreada.

function rollOnce(i: number, min: number, max: number) {
  return tracer.startActiveSpan(`rollOnce:${i}`, (span: Span) => {
    const result = Math.floor(Math.random() * (max - min + 1) + min);

    // Adiciona um atributo ao trecho
    span.setAttribute('dicelib.rolled', result.toString());

    span.end();
    return result;
  });
}
function rollOnce(i, min, max) {
  return tracer.startActiveSpan(`rollOnce:${i}`, (span) => {
    const result = Math.floor(Math.random() * (max - min + 1) + min);

    // Adiciona um atributo ao trecho
    span.setAttribute('dicelib.rolled', result.toString());

    span.end();
    return result;
  });
}

Também é possível adicionar atributos a um trecho quando ele é criado:

tracer.startActiveSpan(
  'app.new-span',
  { attributes: { attribute1: 'value1' } },
  (span) => {
    // realizar alguma operação...

    span.end();
  },
);
function rollTheDice(rolls: number, min: number, max: number) {
  return tracer.startActiveSpan(
    'rollTheDice',
    { attributes: { 'dicelib.rolls': rolls.toString() } },
    (span: Span) => {
      /* ... */
    },
  );
}
function rollTheDice(rolls, min, max) {
  return tracer.startActiveSpan(
    'rollTheDice',
    { attributes: { 'dicelib.rolls': rolls.toString() } },
    (span) => {
      /* ... */
    },
  );
}

Atributos semânticos

Existem convenções semânticas para trechos que representam operações em protocolos bem conhecidos, como HTTP ou chamadas de banco de dados. As convenções semânticas para esses trechos estão definidas na especificação em Convenções Semânticas de Rastreamento (Trace semantic conventions). No exemplo deste guia, podem ser utilizados atributos de código fonte.

Primeiro, adicione o pacote de convenções semânticas como dependência da aplicação:

npm install --save @opentelemetry/semantic-conventions

Adicione as seguintes importações no topo do arquivo da aplicação:

import {
  ATTR_CODE_FUNCTION_NAME,
  ATTR_CODE_FILE_PATH,
} from '@opentelemetry/semantic-conventions';
const {
  ATTR_CODE_FUNCTION_NAME,
  ATTR_CODE_FILE_PATH,
} = require('@opentelemetry/semantic-conventions');

Por fim, atualize o código para incluir os atributos semânticos:

const doWork = () => {
  tracer.startActiveSpan('app.doWork', (span) => {
    span.setAttribute(ATTR_CODE_FUNCTION_NAME, 'doWork');
    span.setAttribute(ATTR_CODE_FILE_PATH, __filename);

    // Realizar alguma operação...

    span.end();
  });
};

Eventos de trecho

Um Evento de Trecho (Span Event) é uma mensagem legível por humanos em um Trecho que representa um evento discreto, sem duração, que pode ser rastreado por um único carimbo de data e hora (timestamp). Você pode pensar nisso como uma forma primitiva de log.

span.addEvent('Executando algo');

const result = doWork();

Também é possível criar Eventos de Trecho com Atributos:

span.addEvent('some log', {
  'log.severity': 'error',
  'log.message': 'Data not found',
  'request.id': requestId,
});

Trechos podem ser criados com zero ou mais Links para outros Trechos que estão casualmente relacionados. Um cenário comum é correlacionar um ou mais rastros com o trecho atual.

const someFunction = (spanToLinkFrom) => {
  const options = {
    links: [
      {
        context: spanToLinkFrom.spanContext(),
      },
    ],
  };

  tracer.startActiveSpan('app.someFunction', options, (span) => {
    // Executa alguma operação...

    span.end();
  });
};

Estado de trecho

Um estado pode ser definido em um Trecho, sendo tipicamente utilizado para indicar que um Trecho não foi concluído com sucesso - Error. Por padrão, todos os trechos possuem estado Unset, o que significa que o trecho foi concluído sem erros. O estado Ok é reservado para quando você precisa definir explicitamente um trecho como bem-sucedido, em vez de manter o padrão Unset (ou seja, “sem erro”).

O estado pode ser definido a qualquer momento antes que o trecho seja finalizado.

import opentelemetry, { SpanStatusCode } from '@opentelemetry/api';

// ...

tracer.startActiveSpan('app.doWork', (span) => {
  for (let i = 0; i <= Math.floor(Math.random() * 40000000); i += 1) {
    if (i > 10000) {
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: 'Erro',
      });
    }
  }

  span.end();
});
const opentelemetry = require('@opentelemetry/api');

// ...

tracer.startActiveSpan('app.doWork', (span) => {
  for (let i = 0; i <= Math.floor(Math.random() * 40000000); i += 1) {
    if (i > 10000) {
      span.setStatus({
        code: opentelemetry.SpanStatusCode.ERROR,
        message: 'Erro',
      });
    }
  }

  span.end();
});

Registrar exceções

Registrar exceções no momento em que ocorrem é uma boa prática, especialmente quando combinado com a definição de estado do trecho.

import opentelemetry, { SpanStatusCode } from '@opentelemetry/api';

// ...

try {
  doWork();
} catch (ex) {
  if (ex instanceof Error) {
    span.recordException(ex);
  }
  span.setStatus({ code: SpanStatusCode.ERROR });
}
const opentelemetry = require('@opentelemetry/api');

// ...

try {
  doWork();
} catch (ex) {
  if (ex instanceof Error) {
    span.recordException(ex);
  }
  span.setStatus({ code: opentelemetry.SpanStatusCode.ERROR });
}

Usando sdk-trace-base e propagando manualmente o contexto do trecho

Em alguns casos, pode não ser possível utilizar o SDK do Node.js nem o SDK para Web. A maior diferença, além do código de inicialização, é que será necessário definir manualmente trechos como ativos no contexto atual para poder criar trechos aninhados.

Inicializando rastreamento com sdk-trace-base

A inicialização é semelhante a como seria feito com Node.js ou o SDK Web.

import opentelemetry from '@opentelemetry/api';
import {
  CompositePropagator,
  W3CTraceContextPropagator,
  W3CBaggagePropagator,
} from '@opentelemetry/core';
import {
  BasicTracerProvider,
  BatchSpanProcessor,
  ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-base';

opentelemetry.trace.setGlobalTracerProvider(
  new BasicTracerProvider({
    // Configura o Span Processor para enviar trechos ao exportador
    spanProcessors: [new BatchSpanProcessor(new ConsoleSpanExporter())],
  }),
);

opentelemetry.propagation.setGlobalPropagator(
  new CompositePropagator({
    propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()],
  }),
);

// Isso é o que será acessado em todo o código de instrumentação
const tracer = opentelemetry.trace.getTracer('example-basic-tracer-node');
const opentelemetry = require('@opentelemetry/api');
const {
  CompositePropagator,
  W3CTraceContextPropagator,
  W3CBaggagePropagator,
} = require('@opentelemetry/core');
const {
  BasicTracerProvider,
  ConsoleSpanExporter,
  BatchSpanProcessor,
} = require('@opentelemetry/sdk-trace-base');

opentelemetry.trace.setGlobalTracerProvider(
  new BasicTracerProvider({
    // Configura o Span Processor para enviar trechos ao exportador
    spanProcessors: [new BatchSpanProcessor(new ConsoleSpanExporter())],
  }),
);

opentelemetry.propagation.setGlobalPropagator(
  new CompositePropagator({
    propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()],
  }),
);

// Isso é o que será acessado em todo o código de instrumentação
const tracer = opentelemetry.trace.getTracer('example-basic-tracer-node');

Assim como os outros exemplos neste documento, isso exporta um Tracer que pode ser usado em toda a aplicação.

Criando trechos aninhados com sdk-trace-base

Para criar trechos aninhados, é necessário definir qualquer trecho atualmente criado como o trecho ativo no contexto atual. O uso de startActiveSpan não é recomendado aqui, pois ele não define o trecho como ativo.

const mainWork = () => {
  const parentSpan = tracer.startSpan('main');

  for (let i = 0; i < 3; i += 1) {
    doWork(parentSpan, i);
  }

  // Certifique-se de encerrar o trecho pai!
  parentSpan.end();
};

const doWork = (parent, i) => {
  // Para criar um trecho filho, é necessário marcar o trecho atual (pai) como o trecho ativo
  // no contexto, e então utilizar o contexto resultante para criar um trecho filho.
  const ctx = opentelemetry.trace.setSpan(
    opentelemetry.context.active(),
    parent,
  );
  const span = tracer.startSpan(`doWork:${i}`, undefined, ctx);

  // simular alguma operação aleatória.
  for (let i = 0; i <= Math.floor(Math.random() * 40000000); i += 1) {
    // vazio
  }

  // Certifique-se de encerrar o span filho! Se não o fizer,
  // ele continuará a rastrear as operações além de 'doWork'!
  span.end();
};

Todas as outras APIs se comportam da mesma forma ao usar o sdk-trace-base, em comparação com os SDKs do Node.js ou Web.

Métricas

As métricas combinam medições individuais em agregados, gerando dados constantes independentemente da carga do sistema. Os agregados não contêm detalhes suficientes para diagnosticar problemas de baixo nível, mas complementam os trechos ao ajudar a identificar tendências e fornecer telemetria sobre o tempo de execução da aplicação.

Inicializar métricas

Para ativar métricas em sua aplicação, é necessário inicializar um MeterProvider, que permitirá criar um Meter.

Caso um MeterProvider não seja criado, as APIs do OpenTelemetry para métricas utilizarão uma implementação no-op (pronunciada “no-op”, de “no operation”, significando “sem operação”) e não conseguirão gerar dados. Conforme explicado a seguir, modifique o arquivo instrumentation.ts (ou instrumentation.js) para incluir todo o código de inicialização do SDK no Node e no navegador.

Node.js

Caso tenha seguido as instruções para inicializar o SDK acima, você já possui um MeterProvider configurado. É possível continuar com Obtendo um Meter.

Inicializando métricas com sdk-metrics

Em alguns casos, pode não ser possível — ou desejável — utilizar o SDK completo do OpenTelemetry para Node.js. O mesmo vale se você quiser utilizar o usar OpenTelemetry JavaScript no navegador.

Nesses casos, é possível inicializar as métricas com o pacote @opentelemetry/sdk-metrics:

npm install @opentelemetry/sdk-metrics

Caso ainda não tenha feito isso, crie um arquivo instrumentation.ts (ou instrumentation.js) que contenha todo o código de inicialização do SDK:

import opentelemetry from '@opentelemetry/api';
import {
  ConsoleMetricExporter,
  MeterProvider,
  PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import {
  defaultResource,
  resourceFromAttributes,
} from '@opentelemetry/resources';
import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';

const resource = defaultResource().merge(
  resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'dice-server',
    [ATTR_SERVICE_VERSION]: '0.1.0',
  }),
);

const metricReader = new PeriodicExportingMetricReader({
  exporter: new ConsoleMetricExporter(),
  // O valor padrão é 60000ms (60 segundos). Vamos utilizar 10 segundos apenas para fins de demonstração.
  exportIntervalMillis: 10000,
});

const myServiceMeterProvider = new MeterProvider({
  resource: resource,
  readers: [metricReader],
});

// Defina este MeterProvider para ser global para a aplicação que está sendo instrumentada.
opentelemetry.metrics.setGlobalMeterProvider(myServiceMeterProvider);
const opentelemetry = require('@opentelemetry/api');
const {
  MeterProvider,
  PeriodicExportingMetricReader,
  ConsoleMetricExporter,
} = require('@opentelemetry/sdk-metrics');
const {
  defaultResource,
  resourceFromAttributes,
} = require('@opentelemetry/resources');
const {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} = require('@opentelemetry/semantic-conventions');

const resource = defaultResource().merge(
  resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'service-name-here',
    [ATTR_SERVICE_VERSION]: '0.1.0',
  }),
);

const metricReader = new PeriodicExportingMetricReader({
  exporter: new ConsoleMetricExporter(),

  // O valor padrão é 60000ms (60 segundos). Vamos utilizar 10 segundos apenas para fins de demonstração.
  exportIntervalMillis: 10000,
});

const myServiceMeterProvider = new MeterProvider({
  resource: resource,
  readers: [metricReader],
});
// Defina este MeterProvider para ser global para a aplicação que está sendo instrumentada.
opentelemetry.metrics.setGlobalMeterProvider(myServiceMeterProvider);

Será necessário utilizar --import com este arquivo ao executar a aplicação, como:

npx tsx --import ./instrumentation.ts app.ts
node --import ./instrumentation.mjs app.js

Agora que o MeterProvider está configurado, é possível adquirir um Meter.

Obtendo um Meter

Em qualquer parte da aplicação onde houver código instrumentado manualmente, é possível chamar getMeter para obter um Meter. Por exemplo:

import opentelemetry from '@opentelemetry/api';

const myMeter = opentelemetry.metrics.getMeter(
  'nome-do-escopo-de-instrumentacao',
  'versao-do-escopo-de-instrumentacao',
);

// Agora é possível utilizar 'meter' para criar instrumentos!
const opentelemetry = require('@opentelemetry/api');

const myMeter = opentelemetry.metrics.getMeter(
  'nome-do-escopo-de-instrumentacao',
  'versao-do-escopo-de-instrumentacao',
);

// Agora é possível utilizar 'meter' para criar instrumentos!

Os valores nome-do-escopo-de-instrumentacao e versao-do-escopo-de-instrumentacao devem identificar exclusivamente o Escopo de Instrumentação, como o nome de um pacote, módulo ou classe. O nome é obrigatório, enquanto a versão, embora opcional, é recomendada.

De modo geral, recomenda-se chamar getMeter na aplicação sempre que for necessário, em vez de exportar a instância de meter para o restante do código. Essa abordagem ajuda a evitar problemas mais complexos de carregamento da aplicação quando há outras dependências envolvidas.

No caso da aplicação de exemplo, há dois pontos em que um Meter pode ser obtido com o Escopo de Instrumentação apropriado:

Primeiro, no arquivo da aplicação app.ts (ou app.js):

/*app.ts*/
import { metrics, trace } from '@opentelemetry/api';
import express, { type Express } from 'express';
import { rollTheDice } from './dice';

const tracer = trace.getTracer('dice-server', '0.1.0');
const meter = metrics.getMeter('dice-server', '0.1.0');

const PORT: number = parseInt(process.env.PORT || '8080');
const app: Express = express();

app.get('/rolldice', (req, res) => {
  const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
  if (isNaN(rolls)) {
    res
      .status(400)
      .send("O parâmetro 'rolls' está ausente ou não é um número.");
    return;
  }
  res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});

app.listen(PORT, () => {
  console.log(`Escutando requisições em http://localhost:${PORT}`);
});
/*app.js*/
const { trace, metrics } = require('@opentelemetry/api');
const express = require('express');
const { rollTheDice } = require('./dice.js');

const tracer = trace.getTracer('dice-server', '0.1.0');
const meter = metrics.getMeter('dice-server', '0.1.0');

const PORT = parseInt(process.env.PORT || '8080');
const app = express();

app.get('/rolldice', (req, res) => {
  const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
  if (isNaN(rolls)) {
    res
      .status(400)
      .send("O parâmetro 'rolls' está ausente ou não é um número.");
    return;
  }
  res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});

app.listen(PORT, () => {
  console.log(`Escutando requisições em http://localhost:${PORT}`);
});

E segundo, no arquivo de biblioteca dice.ts (ou dice.js):

/*dice.ts*/
import { trace, metrics } from '@opentelemetry/api';

const tracer = trace.getTracer('dice-lib');
const meter = metrics.getMeter('dice-lib');

function rollOnce(min: number, max: number) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

export function rollTheDice(rolls: number, min: number, max: number) {
  const result: number[] = [];
  for (let i = 0; i < rolls; i++) {
    result.push(rollOnce(min, max));
  }
  return result;
}
/*dice.js*/
const { trace, metrics } = require('@opentelemetry/api');

const tracer = trace.getTracer('dice-lib');
const meter = metrics.getMeter('dice-lib');

function rollOnce(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

function rollTheDice(rolls, min, max) {
  const result = [];
  for (let i = 0; i < rolls; i++) {
    result.push(rollOnce(min, max));
  }
  return result;
}

module.exports = { rollTheDice };

Agora que os Meters foram inicializados, é possível criar instrumentos de métrica.

Utilizando Contadores (Counters)

Contadores são usados para medir valores cumulativos, não-negativos e crescentes.

No caso da aplicação de exemplo, é possível contar quantas vezes os dados foram lançados:

/*dice.ts*/
const counter = meter.createCounter('dice-lib.rolls.counter');

function rollOnce(min: number, max: number) {
  counter.add(1);
  return Math.floor(Math.random() * (max - min + 1) + min);
}
/*dice.js*/
const counter = meter.createCounter('dice-lib.rolls.counter');

function rollOnce(min, max) {
  counter.add(1);
  return Math.floor(Math.random() * (max - min + 1) + min);
}

Utilizando Contadores UpDown (UpDown Counters)

Contadores UpDown podem incrementar e decrementar, permitindo observar um valor cumulativo que aumenta ou diminui ao longo do tempo.

const counter = myMeter.createUpDownCounter('events.counter');

//...

counter.add(1);

//...

counter.add(-1);

Utilizando Histogramas (Histograms)

Histogramas são utilizados para medir uma distribuição de valores ao longo do tempo.

Por exemplo, veja como reportar uma distribuição de tempos de resposta para uma rota de API com Express:

import express from 'express';

const app = express();

app.get('/', (_req, _res) => {
  const histogram = myMeter.createHistogram('task.duration');
  const startTime = new Date().getTime();

  // realiza alguma operação em uma chamada de API

  const endTime = new Date().getTime();
  const executionTime = endTime - startTime;

  // Registra a duração da operação da tarefa
  histogram.record(executionTime);
});
const express = require('express');

const app = express();

app.get('/', (_req, _res) => {
  const histogram = myMeter.createHistogram('task.duration');
  const startTime = new Date().getTime();

  // realiza alguma operação em uma chamada de API

  const endTime = new Date().getTime();
  const executionTime = endTime - startTime;

  // Registra a duração da operação da tarefa
  histogram.record(executionTime);
});

Utilizando Contadores Observáveis (Assíncronos)

Contadores observáveis (Observable counters) são utilizados para medir um valor aditivo, não-negativo e monotonicamente crescente.

const events = [];

const addEvent = (name) => {
  events.push(name);
};

const counter = myMeter.createObservableCounter('events.counter');

counter.addCallback((result) => {
  result.observe(events.length);
});

//... chamadas para addEvent

Utilizando Contadores UpDown Observáveis (Assíncronos)

Contadores UpDown observáveis (Observable UpDown counters) podem incrementar e decrementar, permitindo medir um valor cumulativo aditivo, não-negativo e não-monotonicamente crescente.

const events = [];

const addEvent = (name) => {
  events.push(name);
};

const removeEvent = () => {
  events.pop();
};

const counter = myMeter.createObservableUpDownCounter('events.counter');

counter.addCallback((result) => {
  result.observe(events.length);
});

//... chamadas para addEvent e removeEvent

Utilizando Medidores Observáveis (Assíncronos)

Medidores Observáveis (Observable Gauges) devem ser usados para medir valores não-aditivos.

let temperature = 32;

const gauge = myMeter.createObservableGauge('temperature.gauge');

gauge.addCallback((result) => {
  result.observe(temperature);
});

//... a variável temperature é modificada por um sensor

Descrevendo instrumentos

Ao criar instrumentos como contadores, histogramas, etc., é possível fornecer uma descrição.

const httpServerResponseDuration = myMeter.createHistogram(
  'http.server.duration',
  {
    description: 'Distribuição do tempo de resposta do servidor HTTP',
    unit: 'milliseconds',
    valueType: ValueType.INT,
  },
);

Em JavaScript, cada tipo de configuração significa o seguinte:

  • description - Uma descrição para o instrumento, legível por humanos.
  • unit - A descrição da unidade de medida que o valor pretende representar. Por exemplo, milliseconds para medir duração, ou bytes para contar número de bytes.
  • valueType - O tipo de valor numérico usado nas medições.

De modo geral, recomenda-se descrever cada instrumento criado.

Adicionando Atributos

É possível adicionar atributos às métricas no momento em que são geradas:

const counter = myMeter.createCounter('my.counter');

counter.add(1, { 'algum.atributo.opcional': 'algum valor' });

Configurar Metric Views

Uma Metric View fornece aos desenvolvedores a capacidade de personalizar métricas expostas pelo SDK de Métricas.

Seletores

Para instanciar uma view, primeiro é necessário selecionar o instrumento de destino. Os seguintes seletores são válidos para métricas:

  • instrumentType
  • instrumentName
  • meterName
  • meterVersion
  • meterSchemaUrl

A seleção por instrumentName (do tipo string) oferece suporte a curingas (wildcards), permitindo, por exemplo, selecionar todos os instrumentos com * ou todos aqueles cujo nome começa com http, utilizando http*.

Exemplos

Filtrar atributos em todos os tipos de métrica:

const limitAttributesView = {
  // exporta apenas o atributo 'environment'
  attributeKeys: ['environment'],
  // aplica a view a todos os instrumentos
  instrumentName: '*',
};

Descartar todos os instrumentos cujo nome do Meter seja pubsub:

const dropView = {
  aggregation: { type: AggregationType.DROP },
  meterName: 'pubsub',
};

Definir intervalos de buckets explícitos para o Histograma chamado http.server.duration:

const histogramView = {
  aggregation: {
    type: AggregationType.EXPLICIT_BUCKET_HISTOGRAM,
    options: { boundaries: [0, 1, 5, 10, 15, 20, 25, 30] },
  },
  instrumentName: 'http.server.duration',
  instrumentType: InstrumentType.HISTOGRAM,
};

Anexar ao Meter Provider

Depois de configuradas as views, é preciso anexá-las ao Meter Provider correspondente:

const meterProvider = new MeterProvider({
  views: [limitAttributesView, dropView, histogramView],
});

Logs

As APIs e SDKs de logs estão atualmente em desenvolvimento.

Próximos passos

Também é necessário configurar um exportador apropriado para exportar seus dados de telemetria para um ou mais backends de observabilidade.


Last modified November 11, 2025: Apply suggestions from code review (bf9f9d5f)