---
title: "诊断通道"
description: "在 node:diagnostics_channel 上发布每个宽事件，使消费者无需导入 evlog 即可订阅，并让 Cloudflare 将其交给 Tail Worker。"
canonical_url: "https://evlog.zhcndoc.com/extend/diagnostics-channel"
---
# 诊断通道

> 在 node:diagnostics_channel 上发布每个宽事件，使消费者无需导入 evlog 即可订阅，并让 Cloudflare 将其交给 Tail Worker。

[`node:diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html) 是运行时内置的、用于插桩的发布/订阅机制。evlog 可以在 `evlog.event` 频道上发布每个宽事件，因此消费者只需通过频道名称订阅：无需导入 evlog，也无需在 `initLogger()` 中添加条目。

## 用途

以下两件事无法通过 [插件](https://evlog.zhcndoc.com/extend/plugins) 或 [排出器](https://evlog.zhcndoc.com/extend/custom-drains) 完成：

- **从不依赖 evlog 的包中发布 evlog 集成。** 订阅者只需要频道名称，因此供应商 SDK、内部共享库或 APM 代理都可以消费宽事件，而无需对等依赖、版本约束或在 `initLogger()` 调用中添加一行配置。
- **在没有 drain 的情况下从 Cloudflare Worker 中导出事件。** Workers 会将每条频道消息转发给 [Tail Worker](#cloudflare-workers)，它会在响应发送后运行，并拥有自己的 CPU 预算，无需 `waitUntil`，也不会有 drain 与请求竞争。

对于其他所有情况（批处理、重试、添加字段、向多个进程内消费者分发），插件或 drain 是更好的工具。本页面底部有一份[对比](#when-a-plugin-is-the-better-tool)。

## 启用它

默认情况下处于关闭状态。启动时启用一次：

```typescript
import { enableDiagnosticsChannel } from 'evlog/diagnostics'

await enableDiagnosticsChannel()
```

该调用不接受任何选项，在每个框架中的用法都相同。只有应用运行启动代码的*位置*不同：

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog-diagnostics.ts
import { enableDiagnosticsChannel } from 'evlog/diagnostics'

export default defineNitroPlugin(async () => {
  await enableDiagnosticsChannel()
})
```

```typescript [Nitro]server/plugins/evlog-diagnostics.ts
import { definePlugin } from 'nitro'
import { enableDiagnosticsChannel } from 'evlog/diagnostics'

export default definePlugin(async () => {
  await enableDiagnosticsChannel()
})
```

```typescript [Next.js]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,
  }
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { enableDiagnosticsChannel } from 'evlog/diagnostics'

await enableDiagnosticsChannel()

export const { handle, handleError } = createEvlogHooks()
```

```typescript [Hono]src/index.ts
// Same shape for Express, Fastify, Elysia, NestJS and oRPC:
// enable once in the server entry, before the app starts serving.
import { Hono } from 'hono'
import { evlog } from 'evlog/hono'
import { enableDiagnosticsChannel } from 'evlog/diagnostics'

await enableDiagnosticsChannel()

const app = new Hono()
app.use(evlog())
```

```typescript [Cloudflare Workers]src/worker.ts
// Needs the nodejs_compat flag
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 })
})
```

```typescript [Standalone]
// 任意 Node、Bun 或 Deno 入口点
import { initLogger } from 'evlog'
import { enableDiagnosticsChannel } from 'evlog/diagnostics'

initLogger({ env: { service: 'worker' } })
await enableDiagnosticsChannel()
```

</framework-tabs>

`enableDiagnosticsChannel()` 是异步的，因为它会延迟加载 `node:diagnostics_channel`，这使得在 Convex、workerd 和其他非 Node 目标中，内置模块不会被打包到主 bundle 中。Promise 完成前发出的事件不会被发布，因此应在启动时调用，而不是在请求处理过程中调用。入口点无法使用顶层 `await` 的框架，应在调用 `initLogger()` 的同一位置调用它，并在那里等待其完成。

<callout color="info" icon="i-lucide-info">

这是一个观测侧通道，而不是传输机制。订阅者会同步运行，且不会被等待：没有批处理、没有重试、没有 `waitUntil`。如需将事件传送到后端，请使用 [drain](https://evlog.zhcndoc.com/extend/custom-drains)；两者看到的是同一个事件。

</callout>

## 订阅

该频道的要点是，消费者除了频道名称外，不需要从 evlog 获取任何内容：

```typescript [metrics.ts]
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，并希望为载荷添加类型：

```typescript [alerts.ts]
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 ?? '-') })
  }
})
```

[基础事件](https://evlog.zhcndoc.com/learn/wide-events)之外的字段类型为 `unknown`，而在请求外发出的事件完全不包含 HTTP 字段，因此请先缩小类型再使用它们，而不是进行类型断言。

## 订阅者收到的内容

与 drain 接收的对象相同：经过审计、脱敏和 enrich 之后的对象。请求会携带 enrichers 添加的所有内容：地理位置、用户代理、trace 上下文：

```json
{
  "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`。

<callout color="warning" icon="i-lucide-triangle-alert">

该事件是实时对象，而不是副本，因此修改它也会修改 drain 接收到的内容。请将其视为只读对象。此外，抛出异常的订阅者**不会被隔离**：`Channel.publish()` 会在下一次 tick 将其作为未捕获异常重新抛出，而这在大多数应用中都是致命的。请确保订阅者能够处理所有情况。

</callout>

在 pretty 模式（开发环境默认模式）下，像 `log.info('auth', 'User logged in')` 这样的带标签日志会直接写入控制台，永远不会成为宽事件，因此不会出现在 channel 上。宽事件本身在两种模式下都会发布。

## 你可以构建的内容

### 不依赖 evlog 的集成

需要订阅的包只需要频道名称，不需要其他内容：无需 `evlog` 依赖，无需需要保持同步的对等版本范围，宿主应用除了导入它之外也无需进行其他配置：

```typescript [acme-apm/src/evlog.ts]
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,
  })
})
```

```typescript [宿主应用]
import 'acme-apm/evlog'
```

同样的形式也适用于跨服务共享的内部库：一个包负责订阅，每个导入它的服务都会上报，而且它们都不需要接触 `initLogger()`。

### 在应用旁边统计计数并设置告警

订阅者是一个普通函数，因此你可以在进程内计算的任何内容都可以在这里计算，无需占用 drain 插槽，这样它就可以继续负责将事件发送到你的后端：

```typescript [observability.ts]
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)
}
```

[插件](https://evlog.zhcndoc.com/extend/plugins)也可以完成同样的工作；当代码位于你自己的应用中时，这是更好的选择。当订阅者作为独立包发布，或必须在不编辑应用日志配置的情况下附加时，频道则更具优势。

## Cloudflare Workers

Workers 会将每条诊断频道消息转发给 [Tail Worker](https://developers.cloudflare.com/workers/observability/logs/tail-workers/)。在 Worker 中启用该频道后，宽事件无需 drain 或 `waitUntil` 即可离开隔离区，并拥有自己的 CPU 预算；Tail Worker 会在响应已经发出后负责传送这些事件：

```typescript [tail-worker/index.ts]
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` 标志，并且消息会经过结构化克隆算法，因此只有可克隆的值能够保留。

## 插件是更好工具的情况

该频道不是 [插件](https://evlog.zhcndoc.com/extend/plugins) 的替代品。它有意保持更窄的范围：

<table>
<thead>
  <tr>
    <th>
      你想要……
    </th>
    
    <th>
      使用
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      将事件发送到后端，并进行批处理和重试
    </td>
    
    <td>
      <a href="https://evlog.zhcndoc.com/extend/custom-drains">
        自定义 drain
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      在事件被 drain 之前为其添加字段
    </td>
    
    <td>
      <a href="https://evlog.zhcndoc.com/extend/custom-enrichers">
        Enricher
      </a>
      
       或插件
    </td>
  </tr>
  
  <tr>
    <td>
      将事件分发给多个进程内消费者
    </td>
    
    <td>
      插件——<code>
        initLogger({ plugins: [a, b, c] })
      </code>
      
       已经支持此功能
    </td>
  </tr>
  
  <tr>
    <td>
      从必须依赖 evlog 的包中订阅
    </td>
    
    <td>
      此通道
    </td>
  </tr>
  
  <tr>
    <td>
      在没有 drain 的情况下从 Cloudflare Worker 中导出事件
    </td>
    
    <td>
      此通道
    </td>
  </tr>
</tbody>
</table>

`diagnostics_channel` 仅限进程内使用：外部不会有任何内容附加到正在运行的进程上。无论如何，订阅者的代码都必须由你的应用加载，因此该频道省去的是一行配置，而不是一个依赖。

---

- [插件](https://evlog.zhcndoc.com/extend/plugins)
- [流](https://evlog.zhcndoc.com/extend/stream)


## Sitemap

See the full [sitemap](https://evlog.zhcndoc.com/sitemap.md) for all pages.
