英语 · 法语
@ddd-ts-kit /哨兵
内核 ErrorReporter 在Sentry或GlitchTip上,它们使用相同的协议:一个适配器,
只有DSN不同。只有意想不到的失败,每一次失败都留下了痕迹
离开流程。中没有 domain/ 或者 application/ 导入此包或报告
处理程序抛出的任何东西,以及它们周围的东西都会报告。
npm i @ddd-ts-kit/sentry @sentry/node @opentelemetry/api
写对 @sentry/node 11:它所依赖的选项enableOpenTelemetrySetup,
dataCollection)在以前的专业中不存在。
用例的代码永远不会看到报告器。它扔;它周围的东西决定它是否存在
失败是一个事件,用正在进行的工作和手的标识符来描述它
它变成了内核 ErrorReporter这个包实现的唯一端口。
作文根
// platform/observability/errors.ts: imported second, right after platform/observability/tracing.ts
import { reportProcessFailures } from '@ddd-ts-kit/kernel';
import { initSentry } from '@ddd-ts-kit/sentry';
import { loadConfig } from '@/platform/config/load-config';
// { dsn?, environment, release?, scrubMessages } of the validated configuration.
// Without a DSN nothing starts and nothing is reported.
export const errors = initSentry(loadConfig(process.env).errorReporting);
reportProcessFailures(errors); // uncaughtException and unhandledRejection: reported, then exit 1
// main.ts
import { tracing } from '@/platform/observability/tracing'; // first
import { errors } from '@/platform/observability/errors'; // second
import { DeadLetterReports, ExecutionScope } from '@ddd-ts-kit/kernel';
import { kernelModule } from '@ddd-ts-kit/di';
import { createApplication } from '@ddd-ts-kit/di/tsyringe';
import { reportHttpErrors } from '@ddd-ts-kit/http';
import { RabbitMqEventBus } from '@ddd-ts-kit/rabbitmq';
const deadLetters = new DeadLetterReports(errors);
const bus = new RabbitMqEventBus({ url, onFailure: (failure) => deadLetters.onFailure(failure) });
const app = createApplication({
modules: [
kernelModule({
errorReporter: errors, // buses and jobs report through it, app.stop() flushes it
eventBus: bus,
eventSubscribers: { forContext: (name) => deadLetters.observing(bus.forConsumer(name)) },
}),
...contexts,
],
});
const http = { onError: reportHttpErrors(errors), executionScope: app.resolve(ExecutionScope) };
process.on('SIGTERM', () => void app.stop().then(() => tracing.shutdown()));
它有三行字: initSentry, errorReporter: errors, onError: reportHttpErrors(errors).
流程处理程序和死信各占一行,供需要它们的用户使用。
init --errors sentry 在它的文件中精确地写入这些片段: platform/observability/errors.ts 作为
以上, SENTRY_DSN, SENTRY_ENVIRONMENT (默认值: NODE_ENV), SENTRY_RELEASE 和
SENTRY_SCRUB_MESSAGES 读到 platform/config/load-config.ts; errors 交的 main.ts
来 composeApplication,它的值是 kernelModule({ errorReporter });
reportHttpErrors 在 platform/auth/authentication/http-boundary.ts; DeadLetterReports 在
platform/events/create-event-provider.ts (经纪人)进
platform/events/start-outbox-relay.ts (被宣布死亡的出版物)。
加载顺序。 platform/observability/tracing.ts 保留第一个导入:OpenTelemetry补丁 http 和
驱动程序加载时,所以它必须在它们之前运行。 initSentry 没有补丁,所以
它的位置对于跟踪没有改变,无论两个顺序中哪个先开始(两个顺序都是)
测试)。它是第二个,所以当应用程序的其余部分加载时已经出现了故障
报道了 reportProcessFailures.
报道了什么
只是没人计划的事。一个 DomainError 是生意上的拒绝,是回答,而不是事件。
| 捕捉点 | 附加的 | 报告 | 不报告 |
|---|---|---|---|
| 命令和查询总线 | kernelModule (默认的公共汽车运载 errorReportingMiddleware) | 命令或查询失败,但没有 DomainError | 一个 DomainError;事件被消耗时的任何东西 |
| HTTP | onError: reportHttpErrors(errors) | 每个请求都得到了5xx, a DomainError 映射到一个5xx包含 | 4 xx |
| 安排工作 | kernelModule (默认调度器观察 reportFailedRuns) | 赛跑的结果是 failed,包括超时 | 一个 DomainError跳过或错过的事件 |
| 集成事件 | DeadLetterReports 在 onFailure 提供者的钩子 | 事件放弃:重试已用尽,有毒消息,发件箱中继的发布被宣布为死亡 | 传递失败并重新尝试 |
| 过程 | reportProcessFailures(errors) | uncaughtException 和 unhandledRejection,因为 fatal |
一次失败,一次报告。 看到错误对象的第一个捕获点报告它和
标志着它;它越过的边界后来承认它,也被称为 cause 一个错误
就这样吧。因此,在请求中失败的命令将由总线报告一次,
在命令的名称下,回答500的路由保持沉默。报告仍然
说明了它的来源: origin 是执行上下文的起源(http, schedule),
关联id和跟踪会指向请求。
事件。 消费者被重新尝试:它的第一次失败是预料之中的,报告它们也是预料之中的
掩盖真实的事件。因此,总线中间件在使用事件时什么也不说,
和 DeadLetterReports 一旦其提供者放弃该事件,就报告该事件。鱼钩
提供者只说代码,所以 observing(subscriber) 记住每个处理程序抛出了什么:
报告携带真正的错误及其堆栈。没有它,或者是一个有毒的信息,报告
是一个 DeadLetterError 命名代码。事件隐藏在 DomainError 被报道为
warning它还是输给了它的消费者。
同样的步骤,用手对内核的双子棋,作为对线路的测试:
import {
DeadLetterReports,
InMemoryErrorReporter,
type IntegrationEvent,
type IntegrationEventCallback,
type IntegrationEventSubscriber,
} from '@ddd-ts-kit/kernel';
// A provider reduced to what matters here: it keeps the handler it is given.
const handlers = new Map<string, IntegrationEventCallback>();
const provider: IntegrationEventSubscriber = {
subscribe: (type, handler) => {
handlers.set(type, handler);
return { unsubscribe: () => handlers.delete(type) };
},
};
const reporter = new InMemoryErrorReporter();
const deadLetters = new DeadLetterReports(reporter);
deadLetters.observing(provider).subscribe('ordering.order.placed', async () => {
throw new Error('ledger unavailable');
});
const event: IntegrationEvent = {
id: 'evt-1',
type: 'ordering.order.placed',
version: 1,
occurredAt: '2026-03-01T10:00:00.000Z',
payload: {},
};
const deliver = handlers.get('ordering.order.placed');
// What the provider says of each failed delivery: codes and identifiers only.
const failure = { kind: 'handler', code: 'Error', eventId: 'evt-1', eventType: 'ordering.order.placed', consumer: 'billing' };
await deliver?.(event).catch(() => undefined); // attempt 1 fails: observing remembers the error
deadLetters.onFailure({ ...failure, attempt: 1 }); // retried: no report
await deliver?.(event).catch(() => undefined); // attempt 2, the last one, fails
deadLetters.onFailure({ ...failure, attempt: 2, dead: true }); // given up: one report
// One report: the Error('ledger unavailable') itself, origin 'event', operation 'ordering.order.placed billing'.
export const reports = reporter.reports;
在处理程序中。 没有什么要写的:处理程序抛出,它周围的捕获点报告。
import { DomainRefusal, type ErrorReporter } from '@ddd-ts-kit/kernel';
interface PaymentGateway {
capture(paymentId: string): Promise<'captured' | 'declined'>;
}
interface CapturePaymentCommand {
readonly paymentId: string;
}
// payments.codes.ts
export const PaymentsCodes = {
PaymentDeclined: 'PAYMENT_DECLINED',
} as const;
// domain/errors/payment-declined.error.ts
export class PaymentDeclined extends DomainRefusal {
readonly code = PaymentsCodes.PaymentDeclined;
static forPayment(paymentId: string) {
return PaymentDeclined.refusal(`Payment ${paymentId} was declined.`, { paymentId });
}
}
// Instead of: a handler that reports by itself, and hides the failure from its caller.
export class CapturePaymentReportingItself {
constructor(
private readonly gateway: PaymentGateway,
private readonly errors: ErrorReporter,
) {}
async execute(command: CapturePaymentCommand): Promise<void> {
try {
await this.gateway.capture(command.paymentId);
} catch (error) {
this.errors.report(error, { origin: 'command', operation: 'CapturePayment', severity: 'error' });
}
}
}
// Write: a handler that throws. A refusal is a DomainError, answered and never reported;
// anything else goes up to the bus, which reports it once, with the actor, the tenant and the trace.
export class CapturePaymentHandler {
constructor(private readonly gateway: PaymentGateway) {}
async execute(command: CapturePaymentCommand): Promise<void> {
if ((await this.gateway.capture(command.paymentId)) === 'declined') {
throw PaymentDeclined.forPayment(command.paymentId);
}
}
}
与OpenTelemetry
@ddd-ts-kit/tracing 拥有跟踪程序提供程序、上下文管理器、传播程序和
auto-instrumentations。 initSentry 让他们独自一人:
enableOpenTelemetrySetup: false: SDK不注册任何跟踪程序提供程序和传播程序;- 没有SDK的默认集成,并且
enableRuntimeChannelInjection: false没有第二 仪器的http或指司机; - 否
tracesSampleRate除非给定:否则不会将性能数据发送到跟踪器。
将两者联系起来的是:当报告失败时,报告程序获取跟踪id和
活动OpenTelemetry span的span id,并将它们放入 contexts.trace 事件的。
从Sentry或GlitchTip中的一个问题,跟踪id在跟踪后端打开跟踪。这个
跨度本身被导出一次,由工具包的出口商。
隐私
| 默认的 | 选择在 | |
|---|---|---|
| 请求 | 从来没有收集。如果其他东西添加了一个:方法、不带查询字符串的URL、既不携带凭据也不携带地址的标头。没有尸体,就没有饼干 | 一个项目 beforeSend |
| 用户 | 演员id,没有其他信息:没有电子邮件,没有姓名,没有地址 | |
| 租户 | 不发送 | tagTenant: true |
| 局部变量 | 从来没有 | |
| 面包屑 | 无自动记录:无话务台,无呼出 | Sentry.addBreadcrumb 项目决定的地方 |
| 异常消息 | 发送后,失败语句引用的值被编校 | scrubMessages: true 为每个非 DomainError |
| 为失败语句赋值引号 | 永远不要在消息中说明错误的原因或集成复制的内容(extra(上下文):语句保留,包括占位符 |
消息。 消息是错误跟踪器中首先读取的内容,也是第一个读取的内容
个人数据泄露的地方:司机引用了他拒绝的那一行。默认保留它,因为
问题标题 [message removed] 浪费了所有的调查,因为追踪者是个
项目控制其访问和保留的内部工具。与 scrubMessages: true 只有
这个项目自己写的信息保存了下来,那些是它的 DomainError年代;输入,编码,堆栈
并为其他人留下痕迹。当跟踪器由第三方托管或
当数据足够敏感,操作人员不应该读取它时。的记录器
工具包(StdoutLogger, @ddd-ts-kit/pino, @ddd-ts-kit/winston)选择同一项下的选项
名字,同样的规则,同样的标记, SCRUBBED_MESSAGE 核的。
失败语句的值。 无论 scrubMessages 说, scrubEvent 替换的
[redacted] 驱动程序引用每个值,并保留语句,这是信号:
Drizzle写后 params: 在一个 DrizzleQueryError;被侵犯的唯一的键
或外键约束(Key (email)=([redacted]) already exists.,在短信或邮件中
detail); 行不为空或检查约束被拒绝(Failing row contains ([redacted]).); PostgreSQL无法读取的值(invalid input syntax for type integer: "[redacted]"); Prisma对它拒绝的请求所提出的论据;一个名为 parameters,
params 或者 bindings 这是对误差的积分。了解一下PostgreSQL的引号 $1,
TypeORM是服务器的消息,其表单如上面所示。
beforeSend 在擦除的事件上最后运行,并可能通过回答而丢弃它 null.
DSN留在SDK客户端中。一个 SentryErrorReporter 这是记录,传播或
连载显示 SentryErrorReporter [client concealed];SDK的客户端,如果您登录
它自己显示了它的选项。
分组和标记
指纹从不包含消息,因此消息中的标识符不会解决一个问题
分成成千上万个问题 DomainError 是它的类和代码;任何其他错误都是
类、它的代码(如果有的话)以及它的第一帧的函数和文件(没有行)
数字。
标签: origin, operation, correlation_id, bounded_context, error_code, tenant_id
在请求。项目添加到的字段 ErrorReportExtensions 作为 report
上下文。
选项
| 选项 | 默认的 | |
|---|---|---|
dsn | 无 | 没有它 initSentry 回答一个 NoopErrorReporter 什么都没有开始。 SENTRY_DSN 是不是读取后面的配置 |
environment, release | 无 | |
enabled | true | false 什么也没报告,不管深空网络是什么 |
sampleRate | 1 | 发送的错误事件的共享 |
tracesSampleRate | 无 | 仅适用于通过Sentry API启动的跨度;套件的跨度从来没有到过那里 |
scrubMessages | false | 看到“隐私” |
tagTenant | false | |
beforeSend | 无 | 在试剂盒洗涤器之后运行 |
integrations | 无 | 添加到链接错误和源上下文行 |
flush(timeoutMs) 答案 true 当没有东西可以送的时候。 app.stop() 用2来调用
秒; reportProcessFailures 退出前做。
开发小提示
GlitchTip 6作为PostgreSQL(14或更高版本)旁边的一个服务运行,并且由其推荐 文档,Valkey。开发映像引导用户、组织和项目 并打印DSN:
services:
glitchtip:
image: glitchtip/glitchtip:6.2.6
profiles: [errors]
depends_on:
glitchtip-postgres: { condition: service_healthy }
glitchtip-valkey: { condition: service_started }
ports: ['127.0.0.1:8000:8000']
environment:
SERVER_ROLE: all_in_one # web and worker in one process
DATABASE_URL: postgres://postgres:postgres@glitchtip-postgres:5432/glitchtip
VALKEY_URL: redis://glitchtip-valkey:6379
SECRET_KEY: local-development-only-not-a-secret # never in production: openssl rand -hex 32
GLITCHTIP_DOMAIN: http://localhost:8000
EMAIL_URL: consolemail://
DEFAULT_FROM_EMAIL: glitchtip@localhost
DEBUG: 'true' # development only: required by the bootstrap below
GLITCHTIP_BOOTSTRAP_DEV: 'true' # user [email protected] / admin_pass, organization, project, DSN
healthcheck:
test: [CMD, python, healthcheck.py]
interval: 10s
timeout: 6s
retries: 6
start_period: 180s # the first start migrates the database
glitchtip-postgres:
image: postgres:18-alpine
profiles: [errors]
environment: { POSTGRES_DB: glitchtip, POSTGRES_PASSWORD: postgres }
volumes: ['glitchtip-postgres:/var/lib/postgresql']
healthcheck:
test: [CMD-SHELL, pg_isready -U postgres -d glitchtip]
interval: 5s
retries: 12
glitchtip-valkey:
image: valkey/valkey:9-alpine
profiles: [errors]
volumes:
glitchtip-postgres:
docker compose --profile errors up -d --wait
docker compose logs glitchtip | grep 'DSN:'
把DSN放进去 SENTRY_DSN;从堆栈的另一个容器中替换 localhost 与
glitchtip. 使用上述用户登录http://localhost:8000。
在测试中
这个包没有任何内容:测试应用程序绑定 InMemoryErrorReporter,
app.errors.reports 列出总线和作业报告的内容,作为数据:每个条目是
{ error, report },抛出时的错误和 ErrorReport 就是这样。