Bibliotecas de observabilidade precisam acompanhar operações internas sem obrigar cada aplicação a modificar o código de negócio. O módulo Diagnostics Channel no Node.js oferece canais nomeados para publicar eventos de diagnóstico com baixo acoplamento entre quem executa uma operação e quem deseja observá-la.
Um módulo HTTP, cliente de banco, fila ou biblioteca própria pode publicar informações em pontos importantes. Ferramentas de tracing, métricas e logs se inscrevem nesses canais somente quando necessário. Quando não existem assinantes, o custo pode permanecer pequeno.
Neste guia, você aprenderá a criar canais, publicar mensagens, verificar assinantes, usar subscribe e unsubscribe, preservar contexto assíncrono, evitar dados sensíveis e integrar Diagnostics Channel a métricas e traces.
O que é diagnostics_channel?
node:diagnostics_channel é um módulo nativo que implementa um sistema de publicação e assinatura voltado para diagnóstico. Canais são identificados por nomes estáveis. Produtores enviam mensagens e consumidores registram callbacks para recebê-las.
A documentação oficial de Diagnostics Channel descreve canais, tracingChannel e recursos de contexto. Para revisar conceitos relacionados, veja EventEmitter no Node.js e OpenTelemetry no Node.js.
Diagnostics Channel não é EventEmitter
As duas APIs distribuem eventos, mas possuem objetivos diferentes. EventEmitter normalmente faz parte do comportamento funcional de um objeto. Diagnostics Channel cria pontos de observação globais e nomeados para instrumentação.
Uma regra de negócio não deve depender da presença de um assinante de diagnóstico. O produtor precisa funcionar corretamente mesmo quando nenhum consumidor está registrado.
Criando um canal
const diagnosticsChannel = require('node:diagnostics_channel');
const requestChannel = diagnosticsChannel.channel(
'app:http:request'
);Chamadas com o mesmo nome retornam uma referência para o mesmo canal lógico. Escolha nomes específicos, documentados e com namespace para evitar colisões.
Publicando uma mensagem
requestChannel.publish({
method: req.method,
route: req.route?.path || 'unknown',
startedAt: Date.now()
});A mensagem pode ser qualquer valor, mas objetos com campos estáveis facilitam integração. Evite publicar o objeto completo de requisição sem necessidade, pois ele contém headers, tokens, cookies, body e referências grandes.
Inscrevendo um consumidor
function onRequest(message, name) {
console.log({
channel: name,
method: message.method,
route: message.route
});
}
requestChannel.subscribe(onRequest);O callback recebe a mensagem e o nome do canal. Ele é executado de forma síncrona durante publish(). Trabalho lento dentro do assinante aumenta a duração da operação instrumentada.
Removendo a inscrição
requestChannel.unsubscribe(onRequest);Use a mesma referência de função. Uma função anônima criada novamente não corresponde ao callback original. Remova assinantes em testes, recarregamentos e módulos temporários.
Verificando se existem assinantes
if (requestChannel.hasSubscribers) {
requestChannel.publish({
method: req.method,
route: normalizeRoute(req)
});
}A verificação é útil quando construir a mensagem exige trabalho. Se o payload já é barato, publicar diretamente pode deixar o código mais simples.
Evite lógica condicional de negócio
Este padrão é incorreto:
if (channel.hasSubscribers) {
await saveOrder();
}A operação passaria a depender da ferramenta de observabilidade. O canal deve observar, não controlar a execução principal.
Projetando o formato da mensagem
Trate o payload como um contrato:
{
operation: 'database.query',
databaseSystem: 'postgresql',
statementName: 'find-user',
startedAt: 12345,
requestId: 'req-123'
}Prefira nomes de operação e identificadores normalizados. SQL completo, URLs com parâmetros e nomes de usuário podem expor dados ou criar cardinalidade alta.
Canais de início e fim
Uma operação pode publicar eventos separados:
const startChannel = channel('app:query:start');
const endChannel = channel('app:query:end');
const errorChannel = channel('app:query:error');
async function executeQuery(query) {
const context = {
name: query.name,
startedAt: performance.now()
};
startChannel.publish(context);
try {
const result = await database.query(query);
endChannel.publish({
...context,
duration: performance.now() - context.startedAt,
rowCount: result.rowCount
});
return result;
} catch (error) {
errorChannel.publish({
...context,
duration: performance.now() - context.startedAt,
error
});
throw error;
}
}O erro deve ser relançado para manter o comportamento original. Publicar diagnóstico não substitui tratamento funcional.
TracingChannel
O recurso tracingChannel() agrupa canais relacionados ao ciclo de uma operação. Ele oferece eventos para início, sucesso, erro e outras etapas, reduzindo convenções manuais.
const {
tracingChannel
} = require('node:diagnostics_channel');
const tracing = tracingChannel('app:payment');Verifique a versão mínima do Node.js utilizada no projeto. Recursos mais novos podem não existir em runtimes antigos.
Executando uma operação instrumentada
Uma tracing channel pode envolver uma função e publicar o ciclo automaticamente de acordo com a API disponível:
const result = await tracing.tracePromise(
async context => {
context.paymentId = payment.id;
return paymentProvider.capture(payment);
},
{ operation: 'capture-payment' }
);O formato exato deve seguir a documentação da versão instalada. Cubra essa integração com testes para detectar mudanças durante atualizações.
Contexto assíncrono
Instrumentação frequentemente precisa associar eventos à requisição atual. AsyncLocalStorage pode armazenar request ID, trace ID e informações normalizadas:
const store = asyncLocalStorage.getStore();
channel.publish({
operation: 'cache.get',
requestId: store?.requestId,
keyNamespace: 'users'
});Não armazene objetos enormes ou dados pessoais desnecessários. Veja o guia de AsyncLocalStorage no Node.js.
Assinantes devem ser rápidos
Como o callback roda durante a publicação, evite escrita de arquivo síncrona, serialização enorme ou chamadas de rede. Agregue métricas em memória e exporte em lote.
channel.subscribe(message => {
metrics.increment('operation_total', {
operation: message.operation
});
});Mesmo operações aparentemente pequenas podem gerar alto volume. Meça o overhead da instrumentação com carga real.
Tratando erros no assinante
Um assinante com bug não deve comprometer a aplicação. Proteja transformações próprias:
function safeSubscriber(message) {
try {
collectMetric(message);
} catch (error) {
diagnosticsLogger.warn('Falha na instrumentação', {
error: error.message
});
}
}Evite registrar o próprio erro no mesmo canal, criando uma recursão infinita.
Integração com métricas
Um consumidor pode observar duração e status:
endChannel.subscribe(message => {
histogram.observe(message.duration, {
operation: message.name,
status: 'success'
});
});
errorChannel.subscribe(message => {
histogram.observe(message.duration, {
operation: message.name,
status: 'error'
});
});Use conjuntos limitados de labels. Identificadores únicos criam séries demais e aumentam custo do sistema de métricas.
Integração com tracing
Uma biblioteca de tracing pode iniciar um span no canal de início e finalizá-lo em sucesso ou erro. O contexto precisa ser associado de forma segura à operação. Não use um mapa global sem limpeza, porque operações abandonadas podem permanecer na memória.
Prefira identificadores ou objetos de contexto que acompanhem o ciclo e sejam liberados no encerramento.
Instrumentação de bibliotecas
Diagnostics Channel é especialmente útil para autores de bibliotecas. A biblioteca publica eventos sem depender de um fornecedor de observabilidade. A aplicação escolhe se deseja conectar OpenTelemetry, métricas próprias ou nenhum consumidor.
Documente nomes, campos, tipos, momento de publicação e estabilidade do contrato. Alterações silenciosas quebram integrações.
Instrumentação condicional
Quando coletar detalhes custa caro:
if (queryChannel.hasSubscribers) {
queryChannel.publish({
operation: query.name,
parameterCount: query.values.length,
estimatedSize: estimateSize(query.values)
});
}Não calcule estimatedSize se ninguém está ouvindo. Essa otimização mantém baixo o custo normal.
Dados sensíveis
Não publique:
- senhas, tokens e cookies;
- números de cartão ou documentos;
- corpo completo de requisições;
- SQL com valores interpolados;
- URLs contendo segredos;
- objetos de usuário sem filtragem.
Crie uma função de sanitização central e teste campos proibidos.
Testando canais
test('publica evento de consulta', async () => {
const messages = [];
function subscriber(message) {
messages.push(message);
}
queryChannel.subscribe(subscriber);
try {
await repository.findUser('123');
assert.equal(messages[0].operation, 'find-user');
} finally {
queryChannel.unsubscribe(subscriber);
}
});O bloco finally evita que um assinante vaze para outros testes.
Versionando contratos
Quando uma mudança incompatível for necessária, crie outro nome de canal ou campo de versão. Consumidores antigos continuam funcionando enquanto a migração acontece.
channel('app:query:v2:start');Não adicione versão a cada alteração compatível; use-a para mudanças de contrato realmente relevantes.
Erros comuns
- Executar trabalho lento no assinante: a operação principal fica mais demorada.
- Publicar dados completos: segredos e alta cardinalidade chegam à telemetria.
- Depender do assinante: a regra de negócio muda quando a instrumentação é desligada.
- Não remover inscrição em testes: callbacks são duplicados.
- Mudar payload sem documentar: consumidores quebram.
- Criar recursão de diagnóstico: o próprio logger publica no mesmo canal.
- Instrumentar tudo: overhead e volume superam o benefício.
Boas práticas para produção
- Use nomes com namespace e contrato estável.
- Mantenha payloads pequenos e normalizados.
- Evite dados sensíveis.
- Garanta que assinantes sejam rápidos.
- Use hasSubscribers para detalhes caros.
- Integre contexto assíncrono com cuidado.
- Limite cardinalidade de métricas.
- Teste publicação e remoção de assinantes.
- Meça o overhead com carga representativa.
- Documente canais públicos de bibliotecas.
Conclusão
O Diagnostics Channel no Node.js cria uma fronteira limpa entre código instrumentado e ferramentas de observabilidade. Bibliotecas publicam eventos estáveis, enquanto consumidores escolhem como transformar esses eventos em métricas, logs ou traces.
O benefício depende de disciplina: mensagens pequenas, assinantes rápidos, contratos documentados e nenhuma dependência funcional. Com esses cuidados, a aplicação ganha visibilidade sem espalhar código específico de um fornecedor por todas as camadas.




