Propagação

Propagação de contexto para o SDK JS

With context propagation, Signals can be correlated with each other, regardless of where they are generated. Although not limited to tracing, context propagation allows traces to build causal information about a system across services that are arbitrarily distributed across process and network boundaries.

For the vast majority of use cases, libraries that natively support OpenTelemetry or instrumentation libraries will automatically propagate trace context across services for you. It is only in rare cases that you will need to propagate context manually.

To learn more, see Context propagation.

Propagação automática de contexto

Bibliotecas de instrumentação como @opentelemetry/instrumentation-http ou @opentelemetry/instrumentation-express propagam o contexto entre serviços por você.

Se você seguiu o Guia de Primeiros Passos, você pode criar uma aplicação cliente que consulta o endpoint /rolldice.

Comece criando uma nova pasta chamada dice-client e instale as dependências necessárias:

npm init -y
npm install undici \
  @opentelemetry/instrumentation-undici \
  @opentelemetry/sdk-node
npm install -D tsx  # uma ferramenta para executar arquivos TypeScript (.ts) diretamente com node
npm init -y
npm install undici \
  @opentelemetry/instrumentation-undici \
  @opentelemetry/sdk-node

Em seguida, crie um novo arquivo chamado client.ts (ou client.js) com o seguinte conteúdo:

/* client.ts */
import { NodeSDK } from '@opentelemetry/sdk-node';
import {
  SimpleSpanProcessor,
  ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-node';
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';

const sdk = new NodeSDK({
  spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())],
  instrumentations: [new UndiciInstrumentation()],
});
sdk.start();

import { request } from 'undici';

request('http://localhost:8080/rolldice').then((response) => {
  response.body.json().then((json: any) => console.log(json));
});
/* instrumentation.mjs */
import { NodeSDK } from '@opentelemetry/sdk-node';
import {
  SimpleSpanProcessor,
  ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-node';
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';

const sdk = new NodeSDK({
  spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())],
  instrumentations: [new UndiciInstrumentation()],
});
sdk.start();

const { request } = require('undici');

request('http://localhost:8080/rolldice').then((response) => {
  response.body.json().then((json) => console.log(json));
});

Certifique-se de que a versão instrumentada de app.ts (ou app.js) do Primeiros Passos esteja em execução em um terminal:

$ npx tsx --import ./instrumentation.ts app.ts
Listening for requests on http://localhost:8080
$ node --import ./instrumentation.mjs app.js
Listening for requests on http://localhost:8080

Abra um segundo terminal e execute o client.ts (ou client.js):

npx tsx client.ts
node client.js

Ambos os terminais devem emitir detalhes dos trechos no console. A saída do cliente é semelhante à seguinte:

{
  resource: {
    attributes: {
      // ...
    }
  },
  traceId: 'cccd19c3a2d10e589f01bfe2dc896dc2',
  parentSpanContext: undefined,
  traceState: undefined,
  name: 'GET',
  id: '6f64ce484217a7bf',
  kind: 2,
  timestamp: 1718875320295000,
  duration: 19836.833,
  attributes: {
    'url.full': 'http://localhost:8080/rolldice',
    // ...
  },
  status: { code: 0 },
  events: [],
  links: []
}

Anote o traceId (cccd19c3a2d10e589f01bfe2dc896dc2) e o ID (6f64ce484217a7bf). Ambos também podem ser encontrados na saída do cliente:

{
  resource: {
    attributes: {
      // ...
    }
  },
  traceId: 'cccd19c3a2d10e589f01bfe2dc896dc2',
  parentSpanContext: {
    traceId: 'cccd19c3a2d10e589f01bfe2dc896dc2',
    spanId: '6f64ce484217a7bf',
    traceFlags: 1,
    isRemote: true
  },
  traceState: undefined,
  name: 'GET /rolldice',
  id: '027c5c8b916d29da',
  kind: 1,
  timestamp: 1718875320310000,
  duration: 3894.792,
  attributes: {
    'http.url': 'http://localhost:8080/rolldice',
    // ...
  },
  status: { code: 0 },
  events: [],
  links: []
}

Suas aplicações cliente e servidora relatam com sucesso trechos conectados. Se você enviar ambas para um backend agora, a visualização mostrará essa dependência para você.

Propagação manual de contexto

Em alguns casos, não é possível propagar o contexto automaticamente como descrito na seção anterior. Pode não existir uma biblioteca de instrumentação correspondente à biblioteca que você usa para fazer os serviços se comunicarem entre si. Ou você pode ter requisitos que essas bibliotecas não conseguem atender, mesmo que existissem.

Quando você precisa propagar o contexto manualmente, pode usar a API de Contexto.

Exemplo genérico

O exemplo genérico a seguir demonstra como você pode propagar o contexto do rastro manualmente.

Primeiro, no serviço de envio, você precisa injetar o context atual:

// Serviço de envio
import { context, propagation, trace } from '@opentelemetry/api';

// Define uma interface para o objeto de saída que conterá as informações do rastro.
interface Carrier {
  traceparent?: string;
  tracestate?: string;
}

// Cria um objeto de saída que segue essa interface.
const output: Carrier = {};

// Serializa o traceparent e o tracestate do contexto para
// um objeto de saída.
//
// Este exemplo usa o contexto ativo de rastro, mas você pode
// usar o contexto que for apropriado ao seu cenário.
propagation.inject(context.active(), output);

// Extrai os valores de traceparent e tracestate do objeto de saída.
const { traceparent, tracestate } = output;

// Em seguida, você pode passar os dados de traceparent e
// tracestate para o mecanismo que você usa para propagar
// entre serviços.
// Serviço de envio
const { context, propagation } = require('@opentelemetry/api');
const output = {};

// Serializa o traceparent e o tracestate do contexto para
// um objeto de saída.
//
// Este exemplo usa o contexto ativo de rastro, mas você pode
// usar o contexto que for apropriado ao seu cenário.
propagation.inject(context.active(), output);

const { traceparent, tracestate } = output;
// Em seguida, você pode passar os dados de traceparent e
// tracestate para o mecanismo que você usa para propagar
// entre serviços.

No serviço de recebimento, você precisa extrair o context (por exemplo, de cabeçalhos HTTP analisados) e defini-lo como o contexto atual de rastro.

// Serviço de recebimento
import {
  type Context,
  propagation,
  trace,
  Span,
  context,
} from '@opentelemetry/api';

// Define uma interface para o objeto de entrada que inclui 'traceparent' e 'tracestate'.
interface Carrier {
  traceparent?: string;
  tracestate?: string;
}

// Suponha que "input" seja um objeto com as chaves 'traceparent' e 'tracestate'.
const input: Carrier = {};

// Extrai os dados de 'traceparent' e 'tracestate' para um objeto de contexto.
//
// Em seguida, você pode tratar este contexto como o contexto ativo para os
// seus rastros.
let activeContext: Context = propagation.extract(context.active(), input);

let tracer = trace.getTracer('app-name');

let span: Span = tracer.startSpan(
  spanName,
  {
    attributes: {},
  },
  activeContext,
);

// Define o trecho criado como ativo no contexto desserializado.
trace.setSpan(activeContext, span);
// Serviço de recebimento
import { context, propagation, trace } from '@opentelemetry/api';

// Suponha que "input" seja um objeto com as chaves 'traceparent' e 'tracestate'
const input = {};

// Extrai os dados de 'traceparent' e 'tracestate' para um objeto de contexto.
//
// Em seguida, você pode tratar este contexto como o contexto ativo para os
// seus rastros.
let activeContext = propagation.extract(context.active(), input);

let tracer = trace.getTracer('app-name');

let span = tracer.startSpan(
  spanName,
  {
    attributes: {},
  },
  activeContext,
);

// Define o trecho criado como ativo no contexto desserializado.
trace.setSpan(activeContext, span);

A partir daí, quando você tem um contexto ativo desserializado, você pode criar trechos que farão parte do mesmo rastro do outro serviço.

Você também pode usar a API de Contexto para modificar ou definir o contexto desserializado de outras formas.

Exemplo de protocolo personalizado

Um caso de uso comum para quando você precisa propagar o contexto manualmente é quando você usa um protocolo personalizado de comunicação entre serviços. O exemplo a seguir usa um protocolo TCP básico baseado em texto para enviar um objeto serializado de um serviço para outro.

Comece criando uma nova pasta chamada propagation-example e inicialize-a com as dependências da seguinte forma:

npm init -y
npm install @opentelemetry/api @opentelemetry/sdk-node

Em seguida, crie os arquivos client.js e server.js com o seguinte conteúdo:

// client.js
const net = require('net');
const { context, propagation, trace } = require('@opentelemetry/api');

let tracer = trace.getTracer('client');

// Conecta ao servidor
const client = net.createConnection({ port: 8124 }, () => {
  // Envia o objeto serializado para o servidor
  let span = tracer.startActiveSpan('send', { kind: 1 }, (span) => {
    const output = {};
    propagation.inject(context.active(), output);
    const { traceparent, tracestate } = output;

    const objToSend = { key: 'value' };

    if (traceparent) {
      objToSend._meta = { traceparent, tracestate };
    }

    client.write(JSON.stringify(objToSend), () => {
      client.end();
      span.end();
    });
  });
});
// server.js
const net = require('net');
const { context, propagation, trace } = require('@opentelemetry/api');

let tracer = trace.getTracer('server');

const server = net.createServer((socket) => {
  socket.on('data', (data) => {
    const message = data.toString();
    // Analisa o objeto JSON recebido do cliente
    try {
      const json = JSON.parse(message);
      let activeContext = context.active();
      if (json._meta) {
        activeContext = propagation.extract(context.active(), json._meta);
        delete json._meta;
      }
      span = tracer.startSpan('receive', { kind: 1 }, activeContext);
      trace.setSpan(activeContext, span);
      console.log('Parsed JSON:', json);
    } catch (e) {
      console.error('Error parsing JSON:', e.message);
    } finally {
      span.end();
    }
  });
});

// Escuta na porta 8124
server.listen(8124, () => {
  console.log('Server listening on port 8124');
});

Abra um primeiro terminal para executar o servidor:

$ node server.js
Server listening on port 8124

Depois, em um segundo terminal, execute o cliente:

node client.js

O cliente deve encerrar imediatamente e o servidor deve exibir o seguinte:

Parsed JSON: { key: 'value' }

Como o exemplo até aqui dependeu apenas da API do OpenTelemetry, todas as chamadas a ela são instruções sem operação (no-op) e o cliente e o servidor se comportam como se o OpenTelemetry não estivesse sendo usado.

Para habilitar o OpenTelemetry e ver a propagação de contexto em ação, crie um arquivo adicional chamado instrumentation.js com o seguinte conteúdo:

// instrumentation.mjs
import { NodeSDK } from '@opentelemetry/sdk-node';
import {
  ConsoleSpanExporter,
  SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-node';

const sdk = new NodeSDK({
  spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())],
});

sdk.start();

Use este arquivo para executar tanto o servidor quanto o cliente com a instrumentação habilitada:

$ node --import ./instrumentation.mjs server.js
Server listening on port 8124

e

node --import ./instrumentation.mjs client.js

Depois que o cliente enviar os dados para o servidor e encerrar, você verá trechos na saída do console de ambos os terminais.

A saída do cliente é semelhante à seguinte:

{
  resource: {
    attributes: {
      // ...
    }
  },
  traceId: '4b5367d540726a70afdbaf49240e6597',
  parentId: undefined,
  traceState: undefined,
  name: 'send',
  id: '92f125fa335505ec',
  kind: 1,
  timestamp: 1718879823424000,
  duration: 1054.583,
  // ...
}

A saída do servidor é semelhante à seguinte:

{
  resource: {
    attributes: {
      // ...
    }
  },
  traceId: '4b5367d540726a70afdbaf49240e6597',
  parentId: '92f125fa335505ec',
  traceState: undefined,
  name: 'receive',
  id: '53da0c5f03cb36e5',
  kind: 1,
  timestamp: 1718879823426000,
  duration: 959.541,
  // ...
}

Assim como no exemplo manual, os trechos são conectados usando o traceId e o id/parentId.

Próximos passos

Para saber mais sobre propagação, leia a especificação da API de Propagadores.