英语 · 法语
@ddd-ts-kit /温斯顿
内核 Logger 港口 温斯顿的JSON行
标准输出,每行正在进行的工作的相关字段,凭据
已编辑,关闭的冲洗。中没有 domain/ 或者 application/ 进口这
包,或记录任何东西:组合根绑定适配器和基础设施,
表示和方面通过端口编写。
npm i @ddd-ts-kit/winston winston
对温斯顿3.19写的。
域和处理程序不记录任何内容:它们必须记录的是事件或错误。这个
适配器的 infrastructure/的控制者 presentation/ 这些方面(logged,
loggingMiddleware(作业调度器)接收内核 Logger,和组成根
结合 WinstonLogger 一次。记录日志的代码与 @ddd-ts-kit/pino 或者
StdoutLogger:看 从适配器进行日志记录.
作文根
// platform/observability/create-logger.ts
import type { ExecutionScope, LogLevel } from '@ddd-ts-kit/kernel';
import { traceLogFields } from '@ddd-ts-kit/tracing';
import { createWinstonLogger, type WinstonLogger } from '@ddd-ts-kit/winston';
export interface LogSettings {
readonly level: LogLevel; // LOG_LEVEL
readonly format: 'json' | 'pretty'; // LOG_FORMAT
readonly service: string;
readonly environment: string;
readonly scrubMessages: boolean; // LOG_SCRUB_MESSAGES
}
export function createLogger(scope: ExecutionScope, settings: LogSettings): WinstonLogger {
return createWinstonLogger({ ...settings, fields: () => traceLogFields(scope) });
}
// main.ts
import { tracing } from '@/platform/observability/tracing'; // first
import { createLogger } from '@/platform/observability/create-logger';
import { AsyncLocalStorageExecutionScope } from '@ddd-ts-kit/kernel';
import { kernelModule } from '@ddd-ts-kit/di';
import { createApplication } from '@ddd-ts-kit/di/tsyringe';
const scope = new AsyncLocalStorageExecutionScope();
const logger = createLogger(scope, config.log);
const app = createApplication({
modules: [kernelModule({ executionScope: scope, logger }), ...contexts],
});
process.once('SIGTERM', () => void shutdown());
async function shutdown(): Promise<void> {
await app.stop(); // flushes the error reporter, then the logger, last: that ends the winston logger
await tracing.shutdown();
}
init --logger winston 写这个 create-logger.ts (它回答了。 NoopLogger 当给出no时
设置(这是工具和测试保持沉默的方式); platform/composition/lay-foundations.ts
调用一次,然后 composeApplication 将记录器交给 kernelModule({ logger }).
应用程序的一行,如下所示:
{"actorId":"user-1","context":"shipping","correlationId":"9f2c","environment":"production","level":"info","message":"order shipped","origin":"http","service":"shop-api","span_id":"00f067aa0ba902b7","tenantId":"acme","time":"2026-03-01T10:00:00.000Z","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736"}
相关性。 fields 按温斯顿格式在每一行读取:no call
站点传递一个id。 traceLogFields(scope) 回答跨度内的跟踪和跨度id,以及
在执行范围内,关联id、因果id、起源、参与者和
租户。这个包不依赖于跟踪包:any () => LogFields 合适,
() => logFieldsOf(scope.current()) 包括在内。抛出的函数不给出字段,永远不会
失去了线。这条线的田地赢得了孩子们的田地,孩子们赢得了 fields:
格式只能填满没人给的钥匙。 StdoutLogger 和 @ddd-ts-kit/pino 意见不同
第二步(这里) fields 赢得一个孩子),所以不要在孩子身上绑定一个键 fields
回答日志记录器是否可能被交换。
一个你自己的例子。 已经配置了winston的项目会继续这样做 接受它想要的格式:
import { CREDENTIAL_KEYS, type ExecutionScope } from '@ddd-ts-kit/kernel';
import { traceLogFields } from '@ddd-ts-kit/tracing';
import winston from 'winston';
import { WinstonLogger, correlationFields, redactCredentials } from '@ddd-ts-kit/winston';
declare const scope: ExecutionScope; // the one the composition root creates
const instance = winston.createLogger({
level: 'info',
format: winston.format.combine(
correlationFields(() => traceLogFields(scope)),
redactCredentials([...CREDENTIAL_KEYS, 'iban']), // after the formats that add fields
winston.format.timestamp(),
winston.format.json(),
),
transports: [new winston.transports.Console()],
});
export const logger = new WinstonLogger(instance);
createWinstonLogger 写下下面的瞬间 time,通过其名称和消息获取级别
下 message,比如 StdoutLogger 方面的包和喜欢 @ddd-ts-kit/pino:
一个仪表板上写着3。一行的键被排序出来:winston的JSON格式
使用稳定的stringifier进行序列化。
你自己的交通工具 不需要自己配置温斯顿: transports 取代了
控制台,所以当行仍然必须到达标准输出时也命名它。
import { createWinstonLogger } from '@ddd-ts-kit/winston';
import winston from 'winston';
export const logger = createWinstonLogger({
service: 'shop-api',
transports: [
new winston.transports.Console(), // the standard output, as by default
new winston.transports.File({ filename: 'logs/errors.log', level: 'error' }), // errors only, kept on disk
],
});
每个传输都有自己的过滤器 level 格式运行后:上面的文件接收到
error 行,控制台的每一行从日志记录器的阈值开始。
发展模式
createWinstonLogger({ level: 'debug', format: 'pretty', fields: () => traceLogFields(scope) });
10:00:00.000 info order shipped { context: 'shipping', service: 'shop-api', correlationId: '9f2c' }
这个 pretty 格式是由winston自己的组合子(colorize, printf),在…
处理:没有其他的包,当地的时间,在颜色的水平,领域作为节点
在几行上打印一个对象,一个堆栈。从配置中选择格式
(LOG_FORMAT=pretty 在 .env, json 其他地方)。
隐私
日志线是个人数据泄露的地方:它们被写在任何地方,每个人都可以阅读并保存 太久。在适配器中完成了三件事,因此调用站点不必记住它们。
凭据已编辑。 通过温斯顿格式,在行,子和的字段中
fields 一样的。这些键的值被替换为 [redacted],在一行的顶端
下一层(headers.authorization, body.password):
password, passwd, authorization, cookie, set-cookie, token, secret, apiKey,
api_key, x-api-key, access_token, refresh_token, id_token, accessToken,
refreshToken, idToken, client_secret, clientSecret, client-secret, private_key,
privateKey
redact: ['iban', 'ssn'] 添加到列表中。无论名字是什么情况,都是匹配的
(Authorization, X-Api-Key),如 isCredentialKey 果仁做了,却什么也没看
对于一个以上的层次,也不是在一个列表中:记录你想要的部分,而不是整个请求。这个
您传递的对象永远不会写入:当必须隐藏密钥时,将复制嵌套对象。
作为字段传递的错误 (logger.error('payment failed', { error }): LogValue 的
该港口允许船舶驶入。 Error)写为 describeError 内核的描述,是一样的
从端口的每个记录器中:它的类为 type,它的 code 当
这是一个 DomainError,它的 message,它自己 stack,那么…… cause 同样的方法,直到
八个深。链的每个错误都写一次。的字段中查找错误
行,不在对象内嵌套。 CREDENTIAL_KEYS, REDACTED 和 SCRUBBED_MESSAGE 是
内核的也一样。
意外错误的消息可以被丢弃。 错误的消息是第一位的
个人数据出现的地方:司机引用了他拒绝的那一行。默认保留它,因为
一条日志线上面写着 [message removed] 花费了所有的调查。只有一个选择
这个项目自己写的信息保存了下来,那些是它的 DomainError年代;类、代码、堆栈和
Cause为其他的保留,堆栈失去它的第一行,这重复了这个消息:
createWinstonLogger({ scrubMessages: true }); // or new WinstonLogger(instance, { scrubMessages: true })
这是 scrubMessages 端口的每个记录器(StdoutLogger皮诺,温斯顿,
InMemoryLogger)和的一个 @ddd-ts-kit/sentry,用同样的规则,同样的
SCRUBBED_MESSAGE 同样的权衡:当日志离开你的手或没有时打开它
操作符应该读取那里的数据。日志线本身的信息是你的:
写 order shipped 并将标识符放在字段中。
该工具包本身从不向记录器传递错误 logged 方面, loggingMiddleware 和
作业调度器写入 errorType 和 errorCode (identifyError (内核),永远不会
一个信息,随便什么 scrubMessages 说,错误会出现在 ErrorReporter。这个
上面的描述是针对您自己的代码选择记录的错误:
logger.error('payment failed', { error }).
水平
| 港口 | 温斯顿(npm 水平) | |
|---|---|---|
debug | debug | 在它下面,温斯顿也有 silly |
info | info | 默认阈值; http 和 verbose 站之间 info 和 debug |
warn | warn | |
error | error |
名字是一样的 cli 的水平。的实例。 syslog 水平没有 warn:
说出它去了哪里,否则这行就被删掉了,因为温斯顿的写作水平和它不一样
不知道。
import { WinstonLogger } from '@ddd-ts-kit/winston';
import winston from 'winston';
const instance = winston.createLogger({
levels: winston.config.syslog.levels, // emerg, alert, crit, error, warning, notice, info, debug
transports: [new winston.transports.Console()],
});
export const logger = new WinstonLogger(instance, { levels: { warn: 'warning' } }); // logger.warn() writes at 'warning'
低于阈值的一行需要进行一次比较:适配器请求 isLevelEnabled 首先,
因为温斯顿一个人就能在传送机下线前运行所有格式。这个
运行时更改实例的阈值(instance.level = 'debug')后面跟着the
适配器和它的孩子。 isLevelEnabled(level) 的端口回答了同样的问题
映射的关卡:在建造需要花费的领域之前询问它。一旦记录器被刷新
答案 false 对于每个关卡。
child(fields) 是温斯顿的孩子吗?田野被捆绑一次,温斯顿就把它们合并在一起
每行的字段。当温斯顿被直接召唤时,有三件事与他不同
目的:一个名为 level 或者 message 永远不要替换级别或消息的
行;消息永远不会作为格式字符串(%s); 和 createWinstonLogger 一个
字段命名 time 被该行的瞬间覆盖。有一件事是温斯顿的:
service, environment 和 base 是它的 defaultMeta,赢得了一场比赛
相同的名字在一条线上或在一个孩子,其中其他两个记录器的工具包让线和
孩子赢了。
冲洗
flush(): Promise<void> 结束Winston记录器,并在其每个传输完成后进行解析
写的是递给他们的 finish 事件)。它从不拒绝,第二个电话会接
同样的承诺。 app.stop() 最后在模块停止和错误报告器之后调用它
已被刷新:也在停止日志。没有应用di包,您可以在
那个地方。
后 flush() 解决 | @ddd-ts-kit/winston | @ddd-ts-kit/pino |
|---|---|---|
| 记录一行 | 删除,没有错误 | 写好了,像以前一样 |
isLevelEnabled(level) | false 对于每个关卡 | 门槛,和以前一样 |
第二个 flush() | 同样的承诺 | 新面孔 |
端口在关闭时调用一次刷新,并且不指定之后记录的内容。
这里的刷新是最终的:温斯顿记录器不会重新打开,所以之后记录的内容,
通过这个记录器或它的子记录器之一,在没有错误的情况下删除 isLevelEnabled
答案 false. 关于关机的任何必须记录的内容都在之前记录了 app.stop()
的回报。在你自己开的小溪上运输
(一个套接字,一个文件流给 Stream 运输部已经把自己的生产线交给了这条小溪
当 flush() 解析:结束该流是你的。
表演
winston通过Node流、格式链及其传输写入每一行
而在调用过程中:大约7微秒一行在哪里 @ddd-ts-kit/pino 需要3
(节点24,一个子节点,四个相关字段,三个字段,一个丢弃的接收器)等等
在这里,每行需要一个字段的副本,每个级别需要一个合并
子,格式阅读 fields,编校传递顶级值,即时,
和 JSON.stringify 通过安全序列化器。对于每行几百行,这算不了什么
第二,可以看到数以万计:在那里,降低水平或采取 @ddd-ts-kit/pino.
适配器不会缓冲任何东西,因此在传输之外的崩溃中不会丢失任何东西
它自己没有写。日志调用永远不会抛出无法读取或序列化的字段(a
Getter(抛出)给出没有它们的行,标记 logError: "unserializable fields",
和 createWinstonLogger 将其格式保存为流内相同的规则。
哪一个
采取 StdoutLogger 标准输出中的JSON行都是您的
需要:不依赖,线条形状相同。采取 @ddd-ts-kit/pino 当日志量很重要时
或者当框架已经说pino (fasttify)时。球队已经跑了,带上温斯顿
它的传输和格式(文件与旋转,syslog,一个供应商传输),并希望保持
他们。这三个实现一个端口,并由一个合约套件持有:交换是其中的一行
组成根。
NestJS
Nest也有自己的 LoggerService,由框架用于其自己的消息,并由
new Logger(context) 在Nest类中。该工具包的应用程序代码从不需要它:处理程序和
适配器接收内核 Logger. 要通过同一个实例发送Nest自身记录的内容,
给它一座几行字的桥;这个工具包没有发货,因为它会将这个包绑定到Nest;
const nest = logger.child({ context: 'nest' });
const bridge: LoggerService = {
log: (message: unknown) => nest.info(String(message)),
warn: (message: unknown) => nest.warn(String(message)),
error: (message: unknown) => nest.error(String(message)),
debug: (message: unknown) => nest.debug(String(message)),
};
const app = await NestFactory.create(AppModule, { logger: bridge });
nest-winston 当Nest类记录大量日志时,另一种选择是:给它一个winston实例
构建并封装同一个实例 new WinstonLogger(instance) 为了工具箱。
在测试中
这个包没有任何内容:测试应用程序绑定 InMemoryLogger 核函数的
entries 是数据。要使端口的另一个适配器具有相同的行为,请运行合约:
import { Writable } from 'node:stream';
import { beforeEach, describe, expect, it } from 'vitest';
import { LOG_LEVELS } from '@ddd-ts-kit/kernel';
import { loggerContract, type LoggedLine } from '@ddd-ts-kit/testing/contracts';
import { createWinstonLogger } from '@ddd-ts-kit/winston';
import winston from 'winston';
loggerContract({ describe, it, expect, beforeEach }, {
name: 'WinstonLogger',
createLogger: (setup) => { // { level, fields, scrubMessages }
const written: string[] = [];
const stream = new Writable({
write: (chunk, _encoding, done) => {
written.push(String(chunk));
done();
},
});
const logger = createWinstonLogger({ ...setup, transports: [new winston.transports.Stream({ stream })] });
return { logger, lines: () => written.map((line) => lineOf(JSON.parse(line))) };
},
});
/** A written line as the contract reads it: the level by its name, the message, the other fields. */
function lineOf({ level, message, time: _time, ...fields }: Record<string, unknown>): LoggedLine {
const known = LOG_LEVELS.find((one) => one === level);
if (known === undefined) {
throw new Error(`Not a level of the port: ${String(level)}`);
}
return { level: known, message: String(message), fields };
}