英语 · 法语
@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
轨迹的每一个跨度都是由这几个地方中的一个打开的,它们都在站台上。这个
域,上下文的应用程序和适配器保持纯类,这是规则
tracing/platform-only 持有:的导入 @ddd-ts-kit/tracing 或 @opentelemetry/*
在源代码根目录下的其他任何地方都会被报告。而不是在适配器中打开一个span:
// 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
和其他适配器一样。
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');
设置
// platform/observability/tracing.ts: imported before everything else
import { initTracing } from '@ddd-ts-kit/tracing/node';
export const tracing = initTracing({ serviceName: 'clinic-api' });
// 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之间没有任何东西:
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) 同步压力表
这将保留最后一个值集,每个值为每个名称创建一次。的属性
测量时,遗漏了一个未定义的标签。
// 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,而不是错误。任何其他设置状态ERRORerror.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。
没有的事件 traceparent (在配置跟踪之前存储)继续活动
跨度。
继续或链接。 OpenTelemetry的消息传递约定允许消费者继续
生产者的跟踪或启动一个链接到它的新生产者。默认是继续,
traceApplication({ consumers: 'continue' }):端到端跟着一个动作就是开口
一个跟踪,Jaeger只显示一个链接作为参考来点击,一个消费者
时间。选择 consumers: 'link' 对于一个将一项活动推广到众多消费者的系统来说,
或者重试的时间太长,以至于跟踪永远不会结束:每次传递都是它的根
自己的跟踪,链接到出版物。
交付。 提供者告诉每个处理程序这是哪个交付(EventDelivery 的
内核:尝试,最后一次尝试,传输,源,消费者),并且每次尝试都是其
自己的: app.messaging.attempt 从1开始计数, app.messaging.redelivered 标记后面的
第一种,失败的尝试是抛出的代码和尝试的错误
之后,提供者放弃事件说 app.outcome: dead-lettered. RabbitMQ,卡夫卡,
NATS、Redis Streams和内存总线都这么说;他们的 traceparent 在旅行中
信封和真正的标头(traceparent)。
碎片,没有 kernelModule
traceApplication() 对于一篇手写的作文,每位成员一人:
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) 处理的时候。这就是发件箱的全部功能
按原样运行:
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() 叫它为
每个绑定都用一个类声明;用手,用测试绳固定;
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 }), 最外层的第一个:thedecorate当有一个时,应用程序的钩子,跨度, 域事件的移交(dispatchingOnSave的内核),然后是方面的 绑定(retry,timeout). 所以repository X.save包括重试和重试保存 宣布它的事件一次。在一个工作单元内部repository X.save不包含domain-eventSpan:保存只将事件排成队列,然后在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 变量应用,默认情况下保留每个跟踪。
保留每个以错误结束的跟踪,无论比率如何,都不能在跟踪时确定 就开始了。这是尾部采样,它属于一个收集器,它可以看到整个痕迹。样本 应用程序中的所有内容,并在那里决定:
# 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:试剂盒的相关系数,每个试剂盒都相同
动作到达的有界上下文。从那里:
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属性:演员和
租户留在日志字段,在那里保留是你的。
断言项目中的跟踪
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,并断言相同的树,然后删除容器。