# @ddd-ts-kit/tracing 中文文档

规范地址：https://npmcn.com/package/%40ddd-ts-kit/tracing/

版本：0\.106\.0
npm 元数据更新时间：2026-10-31T21:46:57.203Z
中文译文更新时间：2026-10-31T23:05:04.893Z
软件包许可：MIT

本文由上游 README 机器翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40ddd-ts-kit/tracing
原文对照：https://npmcn.com/package/%40ddd-ts-kit/tracing/?lang=original
项目主页：https://gbourgeat.github.io/ddd-ts-kit/packages/tracing/
源码仓库：https://github.com/gbourgeat/ddd-ts-kit

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：287 kB，272 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=22\.12\.0
- 运行依赖：1 个
- peer 依赖：@opentelemetry/api ^1\.10\.0，@opentelemetry/sdk-node \>=0\.300\.0，@opentelemetry/resources \>=3\.0\.0，@opentelemetry/sdk-trace \>=3\.0\.0，@opentelemetry/sdk-metrics \>=3\.0\.0，@opentelemetry/context-async-hooks \>=3\.0\.0，@opentelemetry/exporter-trace-otlp-http \>=0\.300\.0，@opentelemetry/auto-instrumentations-node \>=0\.82\.0，@opentelemetry/exporter-metrics-otlp-http \>=0\.300\.0
- 首次发布：2026-10-11T09:42:03.900Z
- 最新版本发布：2026-10-31T21:46:56.856Z
- 已发布版本数：63（近 12 个月 63 个）

## 中文简介

非侵入式OpenTelemetry跟踪：适配器被封装在组合根，域永远不会知道。

## 安装

```sh
npm install @ddd-ts-kit/tracing
```

也可以使用：`pnpm add @ddd-ts-kit/tracing`、`yarn add @ddd-ts-kit/tracing`、`bun add @ddd-ts-kit/tracing`

## README

**英语** · [法语](https://npmcn.com/package/%40ddd-ts-kit/tracing/README.fr.md)

# @ddd-ts-kit /跟踪

OpenTelemetry跟踪，域永远不会看到。中没有 `domain/` 或者 `application/` 导入 这个包：用例、适配器、事件和作业被包装在它们连接的地方，在 一个用户操作读取为一个跟踪。

```
npm i @ddd-ts-kit/tracing @opentelemetry/api
# for the Node bootstrap (@ddd-ts-kit/tracing/node)
npm i @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http @opentelemetry/sdk-metrics @opentelemetry/exporter-metrics-otlp-http
# for the test harness (@ddd-ts-kit/tracing/testing)
npm i -D @opentelemetry/sdk-trace @opentelemetry/context-async-hooks
```

![Where the package sits: the platform wraps each place where work enters or leaves a bounded context, and each wrapper opens one kind of span. tracedRoutes around the HTTP pipeline opens the SERVER span of the request. traceApplication, given once to kernelModule, holds the wrappers that open spans: traceUseCases on the buses opens command and query spans, traceBindings on repositories, gateways and handlers opens their spans, tracedDomainEvents on the domain event dispatcher opens the unit-of-work and domain-event spans, tracedPublisher on the publisher of the kernel opens outbox append, tracedConsumer opens process, and tracedJob and tracedBackgroundJob open job; it also hands the error reporter and the execution scope to the tracing. tracedPublisher around the publisher of the outbox relay opens publish. initTracing starts the OpenTelemetry SDK with its instrumentations and span processors, and exports over OTLP to Jaeger or a collector. The code of the contexts imports none of it](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/tracing-package-place.svg)

轨迹的每一个跨度都是由这几个地方中的一个打开的，它们都在站台上。这个 域，上下文的应用程序和适配器保持纯类，这是规则 `tracing/platform-only` 持有：的导入 `@ddd-ts-kit/tracing` 或 `@opentelemetry/*` 在源代码根目录下的其他任何地方都会被报告。而不是在适配器中打开一个span：

```ts
// src/contexts/billing/infrastructure/external/stripe-payment.gateway.ts
import { trace } from '@opentelemetry/api'; // reported by tracing/platform-only

export class StripePaymentGateway implements PaymentGateway {
  async charge(invoiceId: string): Promise<void> {
    await trace.getTracer('billing').startActiveSpan('charge', () => this.client.charge(invoiceId));
  }
}
```

单独编写适配器，并将其与内核模块的插装类型绑定 封装它，每个调用都是 `external PaymentGateway.charge` 具有相同属性的Span 和其他适配器一样。

```ts
export class StripePaymentGateway implements PaymentGateway {
  async charge(invoiceId: string): Promise<void> {
    await this.client.charge(invoiceId);
  }
}

// the module of the context, explicit style; the declared style writes
// @Adapter(PaymentGateway, { kind: 'external' }) on the class instead
builder.bind(PaymentGateway).toClass(StripePaymentGateway, [StripeClient]).as('external');
```

## 设置

```ts
// platform/observability/tracing.ts: imported before everything else
import { initTracing } from '@ddd-ts-kit/tracing/node';

export const tracing = initTracing({ serviceName: 'clinic-api' });
```

```ts
// main.ts
import { tracing } from '@/platform/observability/tracing'; // first import, see "Load order"
import { kernelModule } from '@ddd-ts-kit/di';
import { createApplication } from '@ddd-ts-kit/di/tsyringe';
import { traceApplication, tracedPublisher, tracedRoutes } from '@ddd-ts-kit/tracing';

const app = createApplication({
  modules: [kernelModule({ instrumentation: traceApplication(), eventPublisher: outbox }), schedulingModule, notificationModule],
});

mountExpress(server, routes, { executionScope: app.resolve(ExecutionScope), around: tracedRoutes() });
new OutboxRelay({ store, publisher: tracedPublisher(bus), clock }).start({ intervalMs: 1_000 });

await app.start();
process.on('SIGTERM', () => void app.stop().then(() => tracing.shutdown()));
```

这就是全部： `traceApplication()` 是值 `kernelModule` 使用和包装应用程序 没有班级知道这件事。有三个地方位于内核模块之外，并使用一个包装器 每个：HTTP管道(`around: tracedRoutes()`)，发件箱中继使用的发布者 （`tracedPublisher(bus)`)，以及构建自己的跟踪程序提供程序的项目(请参阅“Span 处理器”)。

### 耶格，五行

Jaeger v2本地接收OTLP，因此在应用程序和Jaeger之间没有任何东西：

```yaml
services:
  jaeger:
    image: jaegertracing/jaeger:2.22.0
    profiles: [observability]
    ports: ['127.0.0.1:16686:16686', '127.0.0.1:4318:4318']
```

```
docker compose --profile observability up -d jaeger
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 node dist/main.js
```

`initTracing({ serviceName })` 不需要其他任何东西：端点来自标准 `OTEL_EXPORTER_OTLP_ENDPOINT` (或 `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`)变量，并且没有 要么什么都不输出。开放 `http://127.0.0.1:16686` 搜索服务。

### 加载顺序

Auto-instrumentation补丁 `http` 加载数据库驱动程序时 `initTracing` 以前一定跑过。在ES模块中，静态导入在 文件的第一个语句：将调用保留在自己的模块中，并首先导入该模块(如 )，或者预先加载 `node --import ./dist/platform/observability/tracing.js`． 作为ES发布的包 模块还需要 `@opentelemetry/instrumentation` 装载机钩。

### 指标

`openTelemetryMetrics()` 是 `Metrics` OpenTelemetry度量API的内核端口： `counter(name)` 是一个计数器， `histogram(name)` 一个柱状图, `gauge(name)` 同步压力表 这将保留最后一个值集，每个值为每个名称创建一次。的属性 测量时，遗漏了一个未定义的标签。

```ts
// platform/observability/create-metrics.ts
import type { Metrics } from '@ddd-ts-kit/kernel';
import { openTelemetryMetrics } from '@ddd-ts-kit/tracing';

export function createMetrics(): Metrics {
  return openTelemetryMetrics({ meterName: 'clinic-api' });
}
```

当它被调用时，它从全局仪表提供程序中获取它的仪表 `initTracing`： 之前叫它，它不测量任何东西。 `initTracing` 用周期性的OTLP/HTTP注册该提供者 读者当 `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` 或者 `OTEL_EXPORTER_OTLP_ENDPOINT` ，则 离开每一个 `metricsExportIntervalMillis` (其他 `OTEL_METRIC_EXPORT_INTERVAL`,否则 60秒)，再一次是 `shutdown()`． 如果没有变量，它将注册none和every 仪器什么也测量不了，正如跨度什么也输出不了。 `OTEL_METRICS_EXPORTER=none` 寄存器 也没有读取器，无论端点集与否：度量保留在流程中，而跨度仍然存在 离开。任何其他值 `otlp` （`prometheus`， `console`（一个列表）读作 `none`，没有。 首先要说的是：该软件包不服务于其他出口商。积格记录的是痕迹，而不是指标 度量标准被发送到接受OTLP度量标准的后端，OpenTelemetry Collector在其前面 比如普罗米修斯。

## 痕迹长什么样啊

一个预订约会的请求，将其宣布给另外两个有界上下文 发件箱，并在片刻之后由中继从计时器和任何请求之外发布：

```
POST /appointments                                        SERVER    scheduling
  command BookAppointment                                 INTERNAL
    repository AppointmentRepository.findOverlapping      INTERNAL
    repository AppointmentRepository.save                 INTERNAL
    unit-of-work before-commit                            INTERNAL  after every save, before the commit
      domain-event scheduling.appointment.booked          INTERNAL
        handler AppointmentBookedPublisher                INTERNAL
          outbox append scheduling.appointment.booked     INTERNAL  (the response is sent once this is committed)
            publish scheduling.appointment.booked         PRODUCER  by the relay, later, same trace
              process notification.appointment-booked     CONSUMER  notification
                command SendBookingConfirmation           INTERNAL
                  external MailGateway.send               INTERNAL
              process analytics.appointment-booked        CONSUMER  analytics
                command RecordBooking                     INTERNAL
                  repository BookingRepository.save       INTERNAL
```

这是树所产生的，断言span为span by `test/scenarios/one-action-one-trace.spec.ts` 和真正的机甲对决，是 `test/jaeger/real-jaeger.spec.ts`． 计划作业是它自己的跟踪：

```
job scheduling.send-reminders                             INTERNAL  (root)
  command SendReminders                                   INTERNAL
    repository AppointmentRepository.findOverlapping      INTERNAL
```

### 工作单元的各个阶段

域事件的处理程序不在记录事件的保存（即工作单元）之下 在命令处理程序返回后分派它们。在命令的范围内，按顺序：

```
command SendParcels                                       INTERNAL
  repository ParcelRepository.save                        INTERNAL
  repository ParcelRepository.save                        INTERNAL
  unit-of-work before-commit                              INTERNAL  the events of both saves
    domain-event shipping.parcel.sent                     INTERNAL
      repository ParcelRepository.save                    INTERNAL  a handler saves another aggregate
    domain-event shipping.parcel.sent                     INTERNAL
  unit-of-work before-commit                              INTERNAL  what the handlers recorded in turn
    domain-event shipping.parcel.tracked                  INTERNAL
  unit-of-work after-commit                               INTERNAL  once committed, same trace
    domain-event shipping.parcel.sent                     INTERNAL
    domain-event shipping.parcel.sent                     INTERNAL
```

-   `unit-of-work before-commit` 跟随保存跨度。处理程序依次记录的是什么 在同名的第二个span下。
-   `unit-of-work after-commit` 是否存在事件具有该阶段的处理程序并保持的情况 只有那些事件。它是命令范围的一个子命令，并且在执行命令时保留在跟踪中 处理程序失败：命令范围仍然显示 `succeeded`.
-   类声明的处理程序(`@DomainEventHandler`)是一个 `handler <ClassName>` 跨下 它的 `domain-event` 跨度;附带的回调函数 `dispatcher.on` 没有。
-   在任何工作单元（称为单独的存储库）之外 `domain-event` 跨度保持在 保存分派它们的范围，没有阶段范围。

这棵树是由 `test/unit-of-work-phases.spec.ts`.

### Span名称、类型和属性

| 跨度 | 种类 | 由 | 属性 |
| --- | --- | --- | --- |
| `<METHOD> <route template>` | 服务器 | `tracedRoutes()` （重命名HTTP工具打开的跨度，或者启动一个） | `http.route`， `http.request.method`， `http.response.status_code`， `app.operation`， `app.correlation_id`， `app.origin`， `app.outcome`， `app.error.code` |
| `command <Name>`， `query <Name>` | 内部 | `traceUseCases()`总线中间件 | `app.use_case.kind`， `app.use_case.name`， `app.context`， `app.outcome`， `app.error.code`， `error.type` |
| `<kind> <Port>.<method>`： `repository`， `gateway`， `external`， `publisher` | 内部 | `traceBindings()` | `code.namespace` （适配器类）， `code.function`， `app.dependency.kind`， `app.context` |
| `unit-of-work before-commit` | 内部 | `tracedDomainEvents()`在提交之前，围绕在工作单元结束时分派的每轮事件 | 无 |
| `unit-of-work after-commit` | 内部 | `tracedDomainEvents()`，周围的事件有一个 `after-commit` 处理程序，一旦工作单元提交 | 无 |
| `domain-event <name>` | 内部 | `tracedDomainEvents()` | `app.domain_event.name` |
| `handler <ClassName>` | 内部 | `traceBindings()`、善良 `domain-event-handler` | `code.namespace`， `code.function`， `app.dependency.kind`， `app.context` |
| `outbox append <type>` | 内部 | `tracedPublisher()` 围绕一个出版商，存储 | `messaging.system` （`outbox`)， `messaging.operation.name` （`append`)， `messaging.operation.type` （`create`)， `messaging.destination.name`， `messaging.message.id` |
| `publish <type>` | 生产商 | `tracedPublisher()` 在公共汽车周围 | `messaging.system`， `messaging.operation.name` （`publish`)， `messaging.operation.type` （`send`)， `messaging.destination.name`， `messaging.message.id` |
| `process <consumer>` | 消费者 | `tracedConsumer()`，每次一个 | `messaging.system`， `messaging.operation.*` （`process`)， `messaging.destination.name`， `messaging.message.id`， `messaging.consumer.group.name`， `app.messaging.source`， `app.messaging.attempt`， `app.messaging.redelivered`， `app.context`， `app.correlation_id`， `app.origin`， `app.outcome` |
| `job <name>` | 内部的，总是根 | `tracedJob()` | `app.job.name`， `app.job.scheduled_at`， `app.job.attempt`， `app.context`， `app.correlation_id`， `app.origin`， `app.outcome` |
| `job <name>` 一份背景工作 | CONSUMER，使其进入队列的span的子span | `tracedBackgroundJob()`，每次尝试一次； `traceparent()` （`captureTraceparent`)在作业进入队列时与作业一起存储 | `app.job.name`， `app.job.id`， `app.job.attempt`， `app.context`， `app.correlation_id`， `app.origin` （`queue`)， `app.outcome` |

-   名称稳定且基数低：路由模板，而不是URL；一个类，一个端口，等等 事件类型或使用者名称，而不是标识符。
-   `app.outcome` 是 `succeeded`， `refused` （商务回答）， `failed`，并要求送货 `dead-lettered`． 一个 `DomainError` 是答案，而不是事件：用例范围说的是 `refused` 和 `app.error.code`，而不是错误。任何其他设置状态ERROR `error.type` （类名）和 `app.error.code` ( `code`（当它的形状像一个时）。
-   **被授权拒绝的请求没有用例范围**：中间件放置在使用后 case guard，因此跟踪仅是SERVER span，包含已回答的状态和代码 （`FORBIDDEN`）. 被它自己的规则拒绝的用例有它的范围， `refused`.
-   **每个张成的空间有一个机构。** 用例由总线中间件跟踪，由消费者跟踪 `process <consumer>`一份工作 `job <name>`： `traceBindings()` 离开类的绑定 `use-case`， `consumer` 和 `job` 独自一人。
-   **隐私**：类、方法、端口、上下文、代码和工具包的标识符(事件id、 关联id，作业名称)。绝不是参数、结果、有效负载、SQL参数值、错误 消息、聚合id，而不是关于参与者或租户的任何信息。

### 跨越异步边界

在请求中创建集成事件时捕获跟踪上下文： `outbox append <type>` 印上自己的印记 `traceparent` ，并且发件箱将其存储为 套件的记录（每个发件箱存储）将被保留并完全恢复：合同套件 的 `@ddd-ts-kit/testing` 对于内存中的存储（Drizzle、Knex、Prisma和TypeORM）也是如此。 随后，中继从另一个调用链或另一个进程发布： `tracedPublisher(bus)` 使 `publish <type>` 记录名称的子条目，而不是中继循环的子条目 把自己的跨度印在产品上，所以消费者是它的孩子。一只蜱虫 任何要发布的内容都不会产生span，而且每个tick也没有span。

![One trace across two call chains. In the request, the span POST /appointments contains the command, which contains, through the unit-of-work, domain-event and handler spans, outbox append; outbox append stamps its own traceparent on the event, and the outbox stores it with the record. Later, the relay claims the record from a timer, with no span of its own; tracedPublisher opens publish as a child of the span the record names, and stamps its own traceparent on the message, in the envelope and in a header. Each consumer opens process as a child of publish. The trace id is the same from the request to the consumers](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/tracing-package-across-the-outbox.svg)

没有的事件 `traceparent` （在配置跟踪之前存储）继续活动 跨度。

**继续或链接。** OpenTelemetry的消息传递约定允许消费者继续 生产者的跟踪或启动一个链接到它的新生产者。默认是继续， `traceApplication({ consumers: 'continue' })`：端到端跟着一个动作就是开口 一个跟踪，Jaeger只显示一个链接作为参考来点击，一个消费者 时间。选择 `consumers: 'link'` 对于一个将一项活动推广到众多消费者的系统来说， 或者重试的时间太长，以至于跟踪永远不会结束：每次传递都是它的根 自己的跟踪，链接到出版物。

![The two ways a consumer relates to the publication. With consumers: continue, the default, the process spans of the two consumers are children of publish, in the trace of the request: one action is one trace. With consumers: link, publish ends the trace of the request, and each delivery starts a trace of its own, whose root process span carries a link to publish](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/tracing-package-continue-or-link.svg)

**交付。** 提供者告诉每个处理程序这是哪个交付(`EventDelivery` 的 内核：尝试，最后一次尝试，传输，源，消费者)，并且每次尝试都是其 自己的: `app.messaging.attempt` 从1开始计数， `app.messaging.redelivered` 标记后面的 第一种，失败的尝试是抛出的代码和尝试的错误 之后，提供者放弃事件说 `app.outcome: dead-lettered`． RabbitMQ,卡夫卡, NATS、Redis Streams和内存总线都这么说；他们的 `traceparent` 在旅行中 信封和真正的标头(`traceparent`）。

## 碎片，没有 `kernelModule`

`traceApplication()` 对于一篇手写的作文，每位成员一人：

```ts
bus.use(traceUseCases());                                              // after authorization, outside the unit of work
traceDependency(new KnexOrderRepository(db), { kind: 'repository', context: 'ordering', port: 'OrderRepository' });
tracedDomainEvents(dispatcher);
tracedPublisher(new OutboxPublisher(store));                           // outbox append <type>
tracedPublisher(bus);                                                  // publish <type>
tracedSubscriber(bus, { trace: 'continue' });                          // process <type>
tracedConsumer(handler, { consumer: 'billing.invoice-on-order-placed', context: 'billing' });
tracedJob(run, { name: 'billing.send-reminders', context: 'billing' });
tracedErrorReporter(reporter);
createApplication({ modules, decorate: traceBindings() });             // bindings only
configureTracing({ executionScope });                                  // correlation id on spans and log fields
```

最后两行用于内核模块为no的应用程序 `instrumentation`. 有了它, `traceBindings()` 已经是 `binding` 成员内核模块把每个 绑定和执行范围是由模块移交的，所以 `decorate` 钩补充道 什么也没有（将一个实例包装两次是无操作的）。CLI生成的项目既没有行，也没有调用 `traceApplication()` 一次 `src/platform/composition/lay-foundations.ts`， `tracedRoutes()` 在 `src/platform/auth/authentication/http-boundary.ts`， `tracedPublisher(...)` 在 `src/platform/composition/compose-application.ts`, `initTracing` 在 `src/platform/observability/tracing.ts`。这个 规则 `tracing/platform-only` 将这些导入保存在平台目录中， `main.ts` 和 `apps/`.

发布者自己命名它的传输(`messagingSystem`： `rabbitmq`， `kafka`， `nats`， `redis`， `in-memory`, `outbox` 对于发件箱发布者)； `scopedPublisher` 和 `validatingPublisher` 把它传递下去，然后 `tracedPublisher(publisher, { system })` 说得对。 你自己的出版商。

对于自定义传输： `captureTraceparent()` 在存储消息时， `runWithTraceparent(traceparent, fn)` 处理的时候。这就是发件箱的全部功能 按原样运行：

```ts
import { captureTraceparent, runWithTraceparent, withSpan } from '@ddd-ts-kit/tracing';
import { expectSpanTree, installTestTracing } from '@ddd-ts-kit/tracing/testing';

interface QueuedMessage {
  readonly body: string;
  readonly traceparent: string | undefined;
}

const queue: QueuedMessage[] = [];
const tracing = installTestTracing();

const trace = await tracing.traceOf(async () => {
  // Inside the request: store the message with the trace it belongs to.
  await withSpan('POST /messages', async () => {
    queue.push({ body: 'hello', traceparent: captureTraceparent() });
  });

  // Later, from a timer, outside of any span: continue the stored trace.
  for (const message of queue.splice(0)) {
    await runWithTraceparent(message.traceparent, () => withSpan('handle message', async () => undefined));
  }
});

// One trace: the handling is a child of the request, though the request was over.
expectSpanTree(trace, [{ name: 'POST /messages', children: [{ name: 'handle message' }] }]);
tracing.uninstall();
```

有缺失或畸形的 `traceparent`， `runWithTraceparent` 方法中的函数 当前上下文。 `spanContextFromTraceparent` 接受的版本 `00` 只有和拒绝全部为零 id。 `withSpan(name, fn, attributes?)` 存在 平台代码（迁移，启动步骤）；域和应用程序代码从不调用它。

## 一个依赖跨度包含了什么

`traceDependency` 每次调用名为。的异步方法时打开一个span `<kind> <Port>.<method>` 给出端口时在端口之后，否则在类之后。 `traceBindings()` 叫它为 每个绑定都用一个类声明；用手，用测试绳固定；

```ts
import { traceDependency, withSpan } from '@ddd-ts-kit/tracing';
import { expectSpanTree, installTestTracing } from '@ddd-ts-kit/tracing/testing';

// An adapter as a context writes it: no import of the tracing package.
class InMemoryOrderRepository {
  private readonly rows = new Map<string, number>();

  async save(orderId: string, total: number): Promise<void> {
    this.rows.set(orderId, total);
  }

  async totalOf(orderId: string): Promise<number | undefined> {
    return this.rows.get(orderId);
  }
}

const tracing = installTestTracing();

// The composition root wraps it, under the name of its port.
const orders = traceDependency(new InMemoryOrderRepository(), {
  kind: 'repository',
  context: 'ordering',
  port: 'OrderRepository',
});

const trace = await tracing.traceOf(() =>
  withSpan('place order', async () => {
    await orders.save('order-1', 3000);
    await orders.totalOf('order-1');
  }),
);

expectSpanTree(trace, [
  {
    name: 'place order',
    children: [
      {
        name: 'repository OrderRepository.save',
        attributes: { 'code.namespace': 'InMemoryOrderRepository', 'code.function': 'save', 'app.context': 'ordering' },
      },
      { name: 'repository OrderRepository.totalOf' },
    ],
  },
]);

console.log(orders instanceof InMemoryOrderRepository); // true: the same object, wrapped in place
tracing.uninstall();
```

包装保留和遗漏的东西：

-   不要使用参数、返回值或错误消息；错误被原封不动地重新抛出。
-   同步方法不被跟踪。声明的方法 `async` 从它的第一次调用开始追踪；一个 仅返回承诺的方法从其第二次调用开始跟踪。跟踪调用返回A 因此，不要包装其方法返回可链查询构建器的对象。
-   getter、符号、生成器和生命周期钩子(`onModuleInit`， `onModuleDestroy`， `onApplicationBootstrap`， `onApplicationShutdown`， `dispose`)被单独留下。
-   实例被包装在：同一个对象， `instanceof` 和 `this` 完好无损，包装两次 是不可行的。没有添加任何可枚举的对象，因此实例像以前一样展开和比较。
-   `private` 是编译时关键字：在运行时私有方法是一个方法和一个函数 保存在私有字段中也是一个方法。在异步时跟踪两者，以及调用 另一个通过 `this` 生成子跨度。的各个方面 `@ddd-ts-kit/aspects` 在不同 目的：他们只建议从外面打来电话。
-   而冻结或密封的实例则由代理来处理。它的方法和getter运行 真实的例子是 `this`;它对自己的调用不会被跟踪。
-   **存储库包装器的顺序** 下 `kernelModule({ instrumentation })`， 最外层的第一个：the `decorate` 当有一个时，应用程序的钩子，跨度， 域事件的移交(`dispatchingOnSave` 的内核)，然后是方面的 绑定(`retry`， `timeout`）. 所以 `repository X.save` 包括重试和重试保存 宣布它的事件一次。在一个工作单元内部 `repository X.save` 不包含 `domain-event` Span：保存只将事件排成队列，然后在 `unit-of-work before-commit`，一个跟随保存的兄弟。在任何工作单位之外 在分派解决之前保存分派，并且 `domain-event` 跨度是它的子节点。

## 噪音

痕迹只讲述了一个动作的故事。默认值，以及每个选项：

| 什么 | 默认的 | 选项 |
| --- | --- | --- |
| 探测、指标、文档 | 没有跨度 `/health`， `/healthz`， `/live`， `/livez`， `/ready`， `/readyz`， `/startup`， `/metrics`， `/docs`， `/openapi.json`， `/openapi` （分割合同的文件）； `/favicon.ico` 下面是什么呢？的文档路径没有跨度 `@ddd-ts-kit/http` 无论它们被安装在哪里 | `initTracing({ ignoredPaths })`， `tracedRoutes({ ignore })` |
| 发件箱继电器 | 每个tick没有span，当没有发布时没有；其轮询的查询没有跨度（参见驱动程序） |  |
| 的设备上 | `http` 和 `undici` 和 `grpc` （请求的双方）， `pg`， `mysql2`， `mysql`， `mongodb`， `ioredis`， `redis` | `initTracing({ autoInstrumentations })`，或者 `instrumentations` 来替换它们 |
| 数据库驱动程序 | 写入的语句及其占位符，永远不会绑定到它们的值；仅在trace (`requireParentSpan`)，因此轮询器或池不会留下自己的痕迹；没有连接或池签出跨度(`ignoreConnectSpans`） | `autoInstrumentations` |
| 的设备了 | `fs`， `dns`， `net` （如何打电话，而不是做了什么）； `express`， `connect`， `router`， `koa`， `hapi`， `restify`， `nestjs-core` （每个中间件的一个span，其中工具包根据其路由命名请求）； `amqplib`， `kafkajs` (他们会加倍 `publish` 和 `process`); `knex`， `generic-pool`， `dataloader`， `lru-memoizer`， `mongoose` （他们重复司机的话）；记录网桥、运行时和主机指标 | `autoInstrumentations: { '@opentelemetry/instrumentation-fs': { enabled: true } }` |
| 属性 | 允许清单(`DEFAULT_ALLOWED_ATTRIBUTES`)：工具包写了什么，仪器保存了什么，比如一个请求、一个查询、一个对等。在导出之前删除标头、主体、参数值、用户和客户端标识符 | `initTracing({ allowedAttributes })`， `new AttributeAllowList({ allowed, alsoAllowed })` |
| 体积 | 在一个本地根（请求、交付、作业运行）下，最多有25个依赖项、域事件或发布跨度共享一个名称，总共存在200个；除此之外，调用运行不被跟踪（超过上限的发布事件仍然携带跟踪） `app.dependency_spans.dropped` 根计数 | `configureTracing({ maxSpansPerName, maxSpansPerRoot })` |
| 秘密 | 记录url的查询字符串值和userinfo变为 `REDACTED`;异常消息和堆栈跟踪将被丢弃 |  |

用例、消费者、作业和请求的跨度从来没有上限：每个工作单元都有一个。

### 抽样

`initTracing({ sampleRatio: 0.2 })` 采样它们开始的轨迹的一部分，并且 采样父元素总是紧随其后：整个跟踪被保留或删除，横跨发件箱和 上下文，因为决策在 `traceparent` 与事件一起存储。没有 选择标准 `OTEL_TRACES_SAMPLER` 变量应用，默认情况下保留每个跟踪。

保留每个以错误结束的跟踪，无论比率如何，都不能在跟踪时确定 就开始了。这是尾部采样，它属于一个收集器，它可以看到整个痕迹。样本 应用程序中的所有内容，并在那里决定：

```yaml
# OpenTelemetry Collector (contrib): between the applications and Jaeger
receivers:
  otlp: { protocols: { http: { endpoint: 0.0.0.0:4318 } } }
processors:
  tail_sampling:
    decision_wait: 30s
    policies:
      - { name: errors, type: status_code, status_code: { status_codes: [ERROR] } }
      - { name: dead-letters, type: string_attribute, string_attribute: { key: app.outcome, values: [dead-lettered] } }
      - { name: the-rest, type: probabilistic, probabilistic: { sampling_percentage: 10 } }
exporters:
  otlp_http/jaeger: { endpoint: http://jaeger:4318 }
service:
  pipelines:
    traces: { receivers: [otlp], processors: [tail_sampling], exporters: [otlp_http/jaeger] }
```

`decision_wait` 必须涵盖发件箱中继和重试的延迟，还是延迟跨度 一丝丝都被决定分开。

## 跨处理器

对于自己构建的提供程序，在导出处理器之前添加它们：

| 处理器 | 效果 |
| --- | --- |
| `LocalRootTracker` | 启用每个根的上限，命名本地根上的相关性，并允许 `tracedRoutes()` 识别HTTP工具的范围 |
| `UrlSecretSpanProcessor` | `url.full`， `http.url`， `url.query`：查询值和用户信息成为 `REDACTED`，参数名称保留 |
| `ErrorMessageScrubber` | 滴 `exception.message`， `exception.stacktrace` 以及由仪表记录的状态信息；保持 `exception.type` |
| `AttributeAllowList` | 只保留允许的属性 |

`initTracing` 然后安装四个 `extraSpanProcessors`，然后是出口商。

## 旋转：跟踪、日志、审计跟踪、错误跟踪

每个启动一个工作单元的跨度(SERVER跨度， `process <consumer>`， `job <name>`） 携带 `app.correlation_id` 和 `app.origin`：试剂盒的相关系数，每个试剂盒都相同 动作到达的有界上下文。从那里:

```ts
const logger = pino({ mixin: () => traceLogFields() });
// inside a unit of work: { correlationId, origin, actorId, tenantId?, causationId?, trace_id, span_id }
```

| 从 | 来 | 通过 |
| --- | --- | --- |
| 一个跟踪 | 它的计程线 | `trace_id` 日志字段的，或 `correlationId` |
| 一个跟踪 | 动作的审计表项 | `app.correlation_id` 是 `correlationId` 的 `AuditEntry` |
| 错误报告 | 它的跟踪 | `traceId` 的 `ErrorReport`，由 `tracedErrorReporter` （也适用于失败的工作或埋藏的事件，在跨度结束后报告） |
| 日志行，审计条目 | 跟踪 | 搜索标签 `app.correlation_id` 在Jaeger |

`traceApplication()` 将应用程序的执行范围提供给 `configureTracing`;一个 手工调用作曲 `configureTracing({ executionScope })` 本身, `traceLogFields(scope)` 将范围作为先前构建的记录器的参数。 `correlationIdFromActiveSpan()` 响应跟踪id，可作为支持参考 项目没有执行范围。 **没有任何个人属性成为span属性**：演员和 租户留在日志字段，在那里保留是你的。

## 断言项目中的跟踪

```ts
import { expectSpanTree, installTestTracing } from '@ddd-ts-kit/tracing/testing';

const tracing = installTestTracing();          // a real provider that keeps the ended spans; uninstall() when done

it('books an appointment in one trace', async () => {
  const trace = await tracing.traceOf(() => app.resolve(CommandBus).execute(new BookAppointment('09:00')));

  expectSpanTree(trace, [
    {
      name: 'command BookAppointment',
      attributes: { 'app.context': 'scheduling', 'app.outcome': 'succeeded' },
      children: [
        { name: 'repository AppointmentRepository.findOverlapping' },
        { name: 'repository AppointmentRepository.save' },
        { name: 'unit-of-work before-commit', children: [{ name: 'domain-event scheduling.appointment.booked' }] },
      ],
    },
  ]);
});
```

-   `traceOf(action)` 将动作期间结束的跨度作为普通数据(`SpanNode`： 名称、种类、状态、属性、跟踪和跨度id、链接、子节点 已启动)：每个根一棵树，所以两条轨迹就是两棵树。
-   `expectSpanTree(actual, expected)` 名称和结构是否准确：未列出的跨度 失败了，列表中缺少的一个也失败了。检查类型、状态和属性 当给定(`undefined` 期望属性不存在)，而期望跨度不存在 错误，除非 `status: 'ERROR'` 所以说。在不匹配的情况下，错误会列出不同之处 将两个树显示为缩进文本。它不依赖于测试运行器。
-   `installTestTracing({ processors, sampler })` 取项目的处理器和 取样器，用于进行擦洗或取样。

## 测试

```
npx vitest run packages/tracing
DDD_TS_KIT_TEST_JAEGER_IMAGE=jaegertracing/jaeger:2.22.0 npx vitest run packages/tracing/test/jaeger
```

第二个命令需要Docker：它启动自己的Jaeger(一个名为 `ddd-ts-kit-test-trace-jaeger-<pid>`（在两个空闲的环回端口上），使用 `initTracing({ serviceName })` 而标准端点变量则将跟踪信息从头读到尾 Jaeger的HTTP API，并断言相同的树，然后删除容器。

## 常见问题

### @ddd-ts-kit/tracing 是什么？

非侵入式OpenTelemetry跟踪：适配器被封装在组合根，域永远不会知道。

### 如何安装 @ddd-ts-kit/tracing？

在项目目录运行 npm install @ddd-ts-kit/tracing，或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖：@opentelemetry/api（^1\.10\.0）。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖，Yarn 需要手动安装。

### @ddd-ts-kit/tracing 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @ddd-ts-kit/tracing 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @ddd-ts-kit/tracing 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js \>=22\.12\.0。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### @ddd-ts-kit/tracing 可以商用吗？

@ddd-ts-kit/tracing 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### @ddd-ts-kit/tracing 还在维护吗？

最新版本 0\.106\.0 发布于 2026/11/01（2 天前）；至今共发布 63 个版本，其中过去 12 个月发布了 63 个；首次发布于 2026/10/11。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

