node:diagnostics_channel 是运行时内置的、用于插桩的发布/订阅机制。evlog 可以在 evlog.event 频道上发布每个宽事件,因此消费者只需通过频道名称订阅:无需导入 evlog,也无需在 initLogger() 中添加条目。
用途
- 从不依赖 evlog 的包中发布 evlog 集成。 订阅者只需要频道名称,因此供应商 SDK、内部共享库或 APM 代理都可以消费宽事件,而无需对等依赖、版本约束或在
initLogger()调用中添加一行配置。 - 在没有 drain 的情况下从 Cloudflare Worker 中导出事件。 Workers 会将每条频道消息转发给 Tail Worker,它会在响应发送后运行,并拥有自己的 CPU 预算,无需
waitUntil,也不会有 drain 与请求竞争。
对于其他所有情况(批处理、重试、添加字段、向多个进程内消费者分发),插件或 drain 是更好的工具。本页面底部有一份对比。
启用它
默认情况下处于关闭状态。启动时启用一次:
import { enableDiagnosticsChannel } from 'evlog/diagnostics'
await enableDiagnosticsChannel()
该调用不接受任何选项,在每个框架中的用法都相同。只有应用运行启动代码的位置不同:
// server/plugins/evlog-diagnostics.ts
import { enableDiagnosticsChannel } from 'evlog/diagnostics'
export default defineNitroPlugin(async () => {
await enableDiagnosticsChannel()
})
// instrumentation.ts
import { defineNodeInstrumentation } from 'evlog/next/instrumentation'
export const { register, onRequestError } = defineNodeInstrumentation(async () => {
const { createInstrumentation } = await import('evlog/next/instrumentation/create')
const { enableDiagnosticsChannel } = await import('evlog/diagnostics')
const { register: evlogRegister, onRequestError } = createInstrumentation({ service: 'my-app' })
return {
async register() {
await evlogRegister()
await enableDiagnosticsChannel()
},
onRequestError,
}
})
// src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { enableDiagnosticsChannel } from 'evlog/diagnostics'
await enableDiagnosticsChannel()
export const { handle, handleError } = createEvlogHooks()
// Express、Fastify、Elysia、NestJS 和 oRPC 的用法相同:
// 在服务器入口中启用一次,并在应用开始处理请求之前完成。
import { Hono } from 'hono'
import { evlog } from 'evlog/hono'
import { enableDiagnosticsChannel } from 'evlog/diagnostics'
await enableDiagnosticsChannel()
const app = new Hono()
app.use(evlog())
// src/worker.ts — 需要 nodejs_compat 标志
import { initWorkersLogger, withEvlog } from 'evlog/workers'
import { enableDiagnosticsChannel } from 'evlog/diagnostics'
initWorkersLogger({ env: { service: 'my-worker' } })
await enableDiagnosticsChannel()
export default withEvlog(async (request, env, ctx, log) => {
log.set({ action: 'handle_request' })
return Response.json({ ok: true })
})
// 任意 Node、Bun 或 Deno 入口点
import { initLogger } from 'evlog'
import { enableDiagnosticsChannel } from 'evlog/diagnostics'
initLogger({ env: { service: 'worker' } })
await enableDiagnosticsChannel()
enableDiagnosticsChannel() 是异步的,因为它会延迟加载 node:diagnostics_channel,这使得在 Convex、workerd 和其他非 Node 目标中,内置模块不会被打包到主 bundle 中。Promise 完成前发出的事件不会被发布,因此应在启动时调用,而不是在请求处理过程中调用。入口点无法使用顶层 await 的框架,应在调用 initLogger() 的同一位置调用它,并在那里等待其完成。
waitUntil。如需将事件传送到后端,请使用 drain;两者看到的是同一个事件。订阅
该频道的要点是,消费者除了频道名称外,不需要从 evlog 获取任何内容:
import { channel } from 'node:diagnostics_channel'
/** 发布的消息。在本地声明,因此此文件无需导入 evlog。 */
type EvlogMessage = { event: Record<string, unknown> & { level: string; service: string } }
channel('evlog.event').subscribe((message) => {
const { event } = message as EvlogMessage
if (event.level === 'error') metrics.increment('errors', { path: String(event.path ?? 'unknown') })
})
Node 会将发布的消息类型标记为 unknown,因此请在处理程序顶部将其缩小一次。使用 channel(name).subscribe() 而不是模块级别的 subscribe(),是因为前者存在于 evlog 支持的每个 Node 版本中。
如果你已经依赖 evlog,并希望为载荷添加类型:
import { subscribeToWideEvents } from 'evlog/diagnostics'
const stop = await subscribeToWideEvents((event) => {
// ^? WideEvent
if (typeof event.status === 'number' && event.status >= 500) {
alerts.push({ path: String(event.path ?? '-'), requestId: String(event.requestId ?? '-') })
}
})
基础事件之外的字段类型为 unknown,而在请求外发出的事件完全不包含 HTTP 字段,因此请先缩小类型再使用它们,而不是进行类型断言。
订阅者收到的内容
与 drain 接收的对象相同:经过审计、脱敏和 enrich 之后的对象。请求会携带 enrichers 添加的所有内容:地理位置、用户代理、trace 上下文:
{
"timestamp": "2026-08-02T10:23:45.612Z",
"level": "error",
"service": "checkout",
"environment": "production",
"method": "POST",
"path": "/api/checkout",
"status": 500,
"duration": "1.20s",
"requestId": "4a8ff3a8-...",
"user": { "id": "usr_123", "plan": "premium" },
"error": { "name": "PaymentDeclined", "message": "Card declined" }
}
在请求之外发出的事件(log.info({ ... })、createLogger().emit())到达时不包含 HTTP 字段,而来自 log.fork() 的事件则会携带 operation 和 _parentRequestId。
Channel.publish() 会在下一次 tick 将其作为未捕获异常重新抛出,而这在大多数应用中都是致命的。请确保订阅者能够处理所有情况。在 pretty 模式(开发环境默认模式)下,像 log.info('auth', 'User logged in') 这样的带标签日志会直接写入控制台,永远不会成为宽事件,因此不会出现在 channel 上。宽事件本身在两种模式下都会发布。
你可以构建的内容
不依赖 evlog 的集成
需要订阅的包只需要频道名称,不需要其他内容:无需 evlog 依赖,无需需要保持同步的对等版本范围,宿主应用除了导入它之外也无需进行其他配置:
import { channel } from 'node:diagnostics_channel'
type EvlogMessage = {
event: Record<string, unknown> & { timestamp: string; level: string; service: string }
}
channel('evlog.event').subscribe((message) => {
const { event } = message as EvlogMessage
acme.ingest({
at: event.timestamp,
level: event.level,
service: event.service,
attributes: event,
})
})
import 'acme-apm/evlog'
同样的形式也适用于跨服务共享的内部库:一个包负责订阅,每个导入它的服务都会上报,而且它们都不需要接触 initLogger()。
在应用旁边统计计数并设置告警
订阅者是一个普通函数,因此你可以在进程内计算的任何内容都可以在这里计算,无需占用 drain 插槽,这样它就可以继续负责将事件发送到你的后端:
import { channel } from 'node:diagnostics_channel'
const errorsByPath = new Map<string, number>()
channel('evlog.event').subscribe((message) => {
const { event } = message as { event: Record<string, unknown> & { level: string } }
if (event.level !== 'error') return
const path = String(event.path ?? 'unknown')
errorsByPath.set(path, (errorsByPath.get(path) ?? 0) + 1)
if (typeof event.status === 'number' && event.status >= 500) {
void pager.notify(`5xx on ${path}`, { requestId: event.requestId })
}
})
export function errorCounts(): Record<string, number> {
return Object.fromEntries(errorsByPath)
}
插件也可以完成同样的工作;当代码位于你自己的应用中时,这是更好的选择。当订阅者作为独立包发布,或必须在不编辑应用日志配置的情况下附加时,频道则更具优势。
Cloudflare Workers
Workers 会将每条诊断频道消息转发给 Tail Worker。在 Worker 中启用该频道后,宽事件无需 drain 或 waitUntil 即可离开隔离区,并拥有自己的 CPU 预算;Tail Worker 会在响应已经发出后负责传送这些事件:
export default {
async tail(events) {
for (const event of events) {
for (const messageData of event.diagnosticsChannelEvents) {
if (messageData.channel !== 'evlog.event') continue
await fetch('https://logs.example.com/ingest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(messageData.message.event),
})
}
}
},
}
每个条目都包含 timestamp、channel 和 message,其中 message 就是 evlog 发布的内容,因此宽事件位于 messageData.message.event。需要 nodejs_compat 标志,并且消息会经过结构化克隆算法,因此只有可克隆的值能够保留。
插件是更好工具的情况
该频道不是 插件 的替代品。它有意保持更窄的范围:
| 你想要…… | 使用 |
|---|---|
| 将事件发送到后端,并进行批处理和重试 | 自定义 drain |
| 在事件被 drain 之前为其添加字段 | Enricher 或插件 |
| 将事件分发给多个进程内消费者 | 插件——initLogger({ plugins: [a, b, c] }) 已经支持此功能 |
| 从必须依赖 evlog 的包中订阅 | 此通道 |
| 在没有 drain 的情况下从 Cloudflare Worker 中导出事件 | 此通道 |
diagnostics_channel 仅限进程内使用:外部不会有任何内容附加到正在运行的进程上。无论如何,订阅者的代码都必须由你的应用加载,因此该频道省去的是一行配置,而不是一个依赖。