Diagnostics Channel é uma API do Node.js para publicar eventos de diagnóstico entre bibliotecas e ferramentas de observabilidade com baixo acoplamento. Um módulo pode emitir informações sobre requisições, consultas ou tarefas, enquanto outro módulo assina esses eventos para gerar métricas, logs ou traces.
A API evita que bibliotecas dependam diretamente de um agente de APM específico. Quando não há assinantes, o custo pode ser pequeno, mas toda instrumentação precisa ser medida sob carga.
Conceito de canal
Um canal é identificado por uma string. Publicadores e assinantes precisam usar exatamente o mesmo nome e concordar sobre o formato da mensagem.
import diagnosticsChannel from 'node:diagnostics_channel';
const channel = diagnosticsChannel.channel('app.orders.created');O método channel() devolve uma referência ao canal nomeado.
Publicando uma mensagem
const ordersCreated = diagnosticsChannel.channel('app.orders.created');
function createOrder(order) {
const result = repository.save(order);
if (ordersCreated.hasSubscribers) {
ordersCreated.publish({
orderId: result.id,
createdAt: result.createdAt,
});
}
return result;
}Verificar hasSubscribers evita construir payloads caros quando ninguém está ouvindo. Para objetos simples já disponíveis, publicar diretamente pode ser suficiente.
Assinando um canal
function onOrderCreated(message, name) {
metrics.ordersCreated.inc();
logger.debug({
channel: name,
orderId: message.orderId,
}, 'Pedido criado');
}
ordersCreated.subscribe(onOrderCreated);O callback recebe a mensagem e o nome. Ele executa de forma síncrona durante publish, portanto deve ser rápido e não lançar exceções.
Desinscrevendo
ordersCreated.unsubscribe(onOrderCreated);Guarde a mesma referência da função. Em testes e plugins recarregáveis, remover assinantes evita duplicação e vazamentos.
Publicação é síncrona
O publicador fica bloqueado enquanto os assinantes processam a mensagem. Não faça:
- requisições HTTP;
- consultas ao banco;
- serialização grande;
- escrita síncrona em disco;
- cálculos pesados;
- logs excessivos.
Atualize métricas em memória ou enfileire uma amostra para processamento posterior.
Tratamento de erros
Um assinante defeituoso não deve quebrar a operação instrumentada. Envolva código próprio:
function safeSubscriber(message, name) {
try {
collect(message, name);
} catch (error) {
diagnosticsLogger.warn({ error, name }, 'Falha no assinante');
}
}Não crie um ciclo em que o logger também publica no mesmo canal.
Contrato da mensagem
Documente:
- nome do canal;
- momento da publicação;
- campos e tipos;
- campos opcionais;
- unidades;
- possíveis dados sensíveis;
- compatibilidade entre versões.
Canal é uma interface pública quando outras bibliotecas dependem dele. Mudanças silenciosas quebram instrumentação.
Canais de início e fim
Uma operação pode publicar eventos distintos:
const startChannel = diagnosticsChannel.channel('app.payment.start');
const endChannel = diagnosticsChannel.channel('app.payment.end');
const errorChannel = diagnosticsChannel.channel('app.payment.error');
async function charge(input) {
const context = { paymentId: input.id, start: performance.now() };
startChannel.publish(context);
try {
const result = await gateway.charge(input);
endChannel.publish({
...context,
durationMs: performance.now() - context.start,
status: result.status,
});
return result;
} catch (error) {
errorChannel.publish({
...context,
durationMs: performance.now() - context.start,
error,
});
throw error;
}
}Não publique detalhes de cartão, token ou payload completo.
Tracing com start e end
Um assinante pode criar span no início e encerrá-lo no fim. O desafio é manter o mesmo contexto entre eventos assíncronos. Use AsyncLocalStorage ou APIs de tracing, não um mapa global sem limpeza.
TracingChannel
A API oferece abstrações para representar etapas de tracing como start, end, asyncStart, asyncEnd e error. Ela reduz convenções manuais e integra contexto assíncrono. Confirme a disponibilidade e estabilidade na versão mínima adotada.
const tracing = diagnosticsChannel.tracingChannel('app.inventory.lookup');Bibliotecas podem envolver operações e ferramentas podem assinar os canais correspondentes.
Instrumentando uma biblioteca
Uma biblioteca reutilizável não deveria importar o logger da aplicação. Em vez disso:
const queryChannel = diagnosticsChannel.channel('mydb.query');
export async function executeQuery(sql, parameters) {
const start = performance.now();
try {
const result = await driver.query(sql, parameters);
queryChannel.publish({
operation: classifySql(sql),
durationMs: performance.now() - start,
rowCount: result.rowCount,
success: true,
});
return result;
} catch (error) {
queryChannel.publish({
operation: classifySql(sql),
durationMs: performance.now() - start,
success: false,
error,
});
throw error;
}
}Não publique SQL bruto por padrão. Normalize operação e proteja parâmetros.
Métricas
queryChannel.subscribe((message) => {
queryDuration.observe(
{
operation: message.operation,
success: String(message.success),
},
message.durationMs / 1000,
);
});Use labels de baixa cardinalidade. Nunca use query, user ID ou URL completa como label.
Logs amostrados
queryChannel.subscribe((message) => {
if (message.durationMs > 500 || Math.random() < 0.01) {
logger.info({
operation: message.operation,
durationMs: message.durationMs,
success: message.success,
}, 'Diagnóstico de consulta');
}
});Amostragem aleatória simples pode ser suficiente para diagnóstico, mas traces distribuídos costumam aplicar decisões mais consistentes.
Contexto com AsyncLocalStorage
queryChannel.subscribe((message) => {
const context = requestContext.getStore();
metrics.record({
...message,
requestId: context?.requestId,
});
});Não transforme requestId em label de métrica. Ele pode aparecer em logs ou exemplares controlados.
Descobrindo assinantes
hasSubscribers é útil para evitar trabalho:
if (channel.hasSubscribers) {
channel.publish(createDiagnosticPayload());
}O estado pode mudar entre a verificação e a publicação. Isso não é um problema para diagnóstico; não use como sincronização de negócio.
Nomeação de canais
Use namespaces previsíveis:
empresa.produto.modulo.operacao.start
empresa.produto.modulo.operacao.end
empresa.produto.modulo.operacao.errorEvite nomes genéricos como request. Canais globais compartilham o processo.
Versionamento
Se o payload precisar de mudança incompatível, crie canal novo ou adicione campo de versão:
channel.publish({
schemaVersion: 2,
operation,
durationMs,
});Campos adicionais opcionais costumam ser compatíveis. Remover ou mudar unidade não é.
Testes do publicador
const messages = [];
const subscriber = (message) => messages.push(message);
channel.subscribe(subscriber);
try {
await executeOperation();
assert.equal(messages.length, 1);
assert.equal(messages[0].success, true);
} finally {
channel.unsubscribe(subscriber);
}Teste sucesso, erro e ausência de assinantes.
Testes de overhead
Compare:
- sem canal;
- canal sem assinante;
- um assinante leve;
- vários assinantes;
- payload simples e complexo.
Meça throughput, p99, CPU, alocações e event loop delay.
Worker Threads
Cada thread possui seu próprio ambiente. Um canal na thread principal não recebe automaticamente publicações internas do worker. Agregue via mensagens se necessário, preservando volume baixo.
Processos cluster
Cada processo possui assinantes e métricas separados. Use um coletor externo ou agregação adequada. Não assuma que o canal atravessa IPC.
Ativação dinâmica
Uma aplicação pode carregar um módulo de diagnóstico somente quando uma variável de ambiente estiver habilitada. Evite adicionar e remover assinantes repetidamente em alta frequência.
Segurança
Mensagens permanecem no processo, mas agentes e assinantes podem exportá-las. Trate o canal como uma fronteira de observabilidade: publique apenas dados aprovados, com redaction e limites.
Quando não usar
Diagnostics Channel não é EventEmitter de negócio, fila durável ou mecanismo de comunicação entre serviços. Eventos podem não ter assinantes e não são armazenados. Use para diagnóstico opcional, não para ações essenciais.
Erros comuns
- usar canal para lógica de negócio;
- executar I/O pesado no assinante;
- publicar payload sensível;
- não documentar contrato;
- usar labels de alta cardinalidade;
- deixar assinantes duplicados em testes;
- assumir comunicação entre workers;
- não medir overhead;
- lançar exceção no assinante.
Fluxo recomendado
Defina canais estáveis, mantenha payload pequeno, use hasSubscribers para construção cara, proteja assinantes e integre com métricas e traces. Combine com AsyncLocalStorage, Async Hooks, perf_hooks e OpenTelemetry.
Consulte a documentação oficial de Diagnostics Channel e a referência oficial de TracingChannel.




