@ddd-ts-kit/winston

中文文档
0.106.0发布于 2026/11/01MITTypeScriptES Module构建来源已验证

Adapter of the kernel Logger port on winston, with trace and execution correlation on every line.

原始使用文档
文档目录9 节
  1. Composition root
  2. Development mode
  3. Privacy
  4. Levels
  5. Flush
  6. Performance
  7. Which one
  8. NestJS
  9. In a test

English · Français

@ddd-ts-kit/winston

The kernel Logger port on winston: JSON lines on the standard output, the correlation fields of the work in progress on every line, credentials redacted, a flush for the shutdown. Nothing in domain/ or application/ imports this package, or logs anything: the composition root binds the adapter, and infrastructure, presentation and the aspects write through the port.

npm i @ddd-ts-kit/winston winston

Written against winston 3.19.

The domain and the handlers log nothing: what they have to say is an event or an error. The adapters of infrastructure/, the controllers of presentation/ and the aspects (logged, loggingMiddleware, the job scheduler) receive the kernel Logger, and the composition root binds WinstonLogger to it once. The code that logs is the same with @ddd-ts-kit/pino or StdoutLogger: see Logging from an adapter.

Composition root

// 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 writes this create-logger.ts (it answers a NoopLogger when given no settings, which is how the tools and the tests stay silent); platform/composition/lay-foundations.ts calls it once and composeApplication hands the logger to kernelModule({ logger }).

One line of the application, as it is written:

{"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"}

The path of one call to logger.info through winston: below the threshold the adapter stops after one comparison; otherwise it describes the errors among the fields and keeps level and message out of them; winston merges the fields of the line, the fields of the children and its defaultMeta; then the formats run in order: correlationFields fills the keys nobody gave from fields(), redactCredentials hides the credential keys, isoTime writes time, json or the pretty line serializes; each transport writes the line: the console by default, or the files, syslog or stream you give

Correlation. fields is read at each line that is written, by a winston format: no call site passes an id. traceLogFields(scope) answers the trace and span ids inside a span, and inside the execution scope the correlation id, the causation id, the origin, the actor and the tenant. This package does not depend on the tracing package: any () => LogFields fits, () => logFieldsOf(scope.current()) included. A function that throws gives no field, never a lost line. The fields of the line win over those of a child, which win over fields: the format only fills the keys nobody gave. StdoutLogger and @ddd-ts-kit/pino differ on the second step (there fields win over a child), so do not bind in a child a key that fields answers if the logger may be swapped.

Which value a key gets when it is given twice, from the weakest to the strongest: with pino and StdoutLogger, service, environment and base, then the children, then fields(), then the line, which wins; with winston, fields(), then the children, then the line, then service, environment and base, which win

An instance of your own. The project that already configures winston keeps doing so, and takes the formats it wants:

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 writes the instant under time, the level by its name and the message under message, like the StdoutLogger of the aspects package and like @ddd-ts-kit/pino: one dashboard reads the three. The keys of a line come out sorted: winston's JSON format serializes with a stable stringifier.

Transports of your own without configuring winston yourself: transports replaces the console, so name it too when the lines must still reach the standard output.

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
  ],
});

Each transport filters by its own level after the formats ran: the file above receives the error lines, the console every line from the threshold of the logger.

Development mode

createWinstonLogger({ level: 'debug', format: 'pretty', fields: () => traceLogFields(scope) });
10:00:00.000 info order shipped { context: 'shipping', service: 'shop-api', correlationId: '9f2c' }

The pretty format is made of winston's own combinators (colorize, printf), in the process: no other package, the local time of day, the level in colour, the fields as Node prints an object, a stack over several lines. Choose the format from configuration (LOG_FORMAT=pretty in .env, json everywhere else).

Privacy

Log lines are where personal data leaks: they are written everywhere, read by everyone and kept for long. Three things are done in the adapter, so that no call site has to remember them.

Credentials are redacted by a winston format, in the fields of a line, of a child and of fields alike. The value of these keys is replaced by [redacted], at the top of a line and one level below (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'] adds to the list. Names are matched whatever their case (Authorization, X-Api-Key), as isCredentialKey of the kernel does, and nothing is looked for deeper than one level, nor inside a list: log the part you mean, not a whole request. The objects you pass are never written to: a nested one is copied when a key has to be hidden.

An error passed as a field (logger.error('payment failed', { error }): the LogValue of the port admits an Error) is written as describeError of the kernel describes it, the same from every logger of the port: its class as type, its code when it is a DomainError, its message, its own stack, then its cause the same way, up to eight deep. Each error of a chain is written once. Errors are looked for among the fields of the line, not inside nested objects. CREDENTIAL_KEYS, REDACTED and SCRUBBED_MESSAGE are those of the kernel too.

Messages of unexpected errors can be dropped. The message of an error is the first place where personal data shows up: a driver quotes the row it refused. The default keeps it, because a log line that says [message removed] costs every investigation. With one option only the messages the project wrote itself survive, those of its DomainErrors; class, code, stack and cause remain for the others, and the stack loses its first line, which repeats the message:

createWinstonLogger({ scrubMessages: true }); // or new WinstonLogger(instance, { scrubMessages: true })

This is the scrubMessages every logger of the port takes (StdoutLogger, pino, winston, InMemoryLogger) and the one of @ddd-ts-kit/sentry, with the same rule, the same SCRUBBED_MESSAGE and the same trade-off: turn it on when the logs leave your hands or when no operator should read that data there. What the message of a log line itself says stays yours: write order shipped and put the identifiers in fields.

The kit itself never hands an error to a logger: the logged aspect, loggingMiddleware and the job scheduler write errorType and errorCode only (identifyError of the kernel), never a message, whatever scrubMessages says, and the error goes to the ErrorReporter. The description above is for the error your own code chooses to log: logger.error('payment failed', { error }).

Levels

Portwinston (npm levels)
debugdebugbelow it, winston also has silly
infoinfothe default threshold; http and verbose stand between info and debug
warnwarn
errorerror

The names are the same in the cli levels. An instance on the syslog levels has no warn: say where it goes, or the line is dropped, since winston does not write at a level it does not know.

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'

A line below the threshold costs one comparison: the adapter asks isLevelEnabled first, because winston alone would run every format before its transports drop the line. The threshold of an instance changed while running (instance.level = 'debug') is followed by the adapter and by its children. isLevelEnabled(level) of the port answers the same question for the mapped level: ask it before building fields that cost. Once the logger was flushed it answers false for every level.

child(fields) is a winston child: the fields are bound once, and winston merges them under the fields of each line. Three things differ from what winston does when called directly, all on purpose: a field named level or message never replaces the level or the message of the line; the message is never read as a format string (%s); and with createWinstonLogger a field named time is overwritten by the instant of the line. One thing is winston's: service, environment and base are its defaultMeta, which wins over a field of the same name on a line or in a child, where the other two loggers of the kit let the line and the child win.

Flush

flush(): Promise<void> ends the winston logger and resolves once each of its transports has written what it was handed (their finish event). It never rejects, and a second call answers the same promise. app.stop() calls it, last, after the modules stopped and the error reporter was flushed: stopping logs too. Without the application of the di package, call it yourself at that place.

After flush() resolved@ddd-ts-kit/winston@ddd-ts-kit/pino
a line loggeddropped, without an errorwritten, like before
isLevelEnabled(level)false for every levelthe threshold, as before
a second flush()the same promisea new flush

The port calls a flush once, at shutdown, and leaves what is logged afterwards unspecified. Here the flush is final: a winston logger does not reopen, so what is logged afterwards, through this logger or one of its children, is dropped without an error, and isLevelEnabled answers false. Whatever must be logged about the shutdown is logged before app.stop() returns. A transport over a stream you opened yourself (a socket, a file stream given to the Stream transport) has handed its lines to that stream when flush() resolves: ending that stream is yours.

Performance

winston writes each line through a Node stream, a chain of formats and its transports, all in the process and in the call: about 7 microseconds a line where @ddd-ts-kit/pino takes 3 (Node 24, a child, four correlation fields, three fields, a sink that discards), and more with each format you add. Each line costs here one copy of its fields, one merge per level of child, the format reading fields, the redaction pass over the top-level values, the instant, and JSON.stringify through a safe serializer. That is nothing at a few hundred lines per second and visible at tens of thousands: there, lower the level or take @ddd-ts-kit/pino.

Nothing is buffered by the adapter, so nothing is lost on a crash beyond what the transport itself had not written. Log calls never throw: fields that cannot be read or serialized (a getter that throws) give the line without them, marked logError: "unserializable fields", and createWinstonLogger holds its formats to the same rule inside the stream.

Which one

Take StdoutLogger of the aspects package when JSON lines on the standard output are all you need: no dependency, the same line shape. Take @ddd-ts-kit/pino when logging volume matters or when the framework already speaks pino (Fastify). Take winston when the team already runs its transports and formats (files with rotation, syslog, a vendor transport) and wants to keep them. The three implement one port and are held by one contract suite: swapping is one line of the composition root.

NestJS

Nest has its own LoggerService, used by the framework for its own messages and by new Logger(context) in Nest classes. Application code of the kit never needs it: handlers and adapters receive the kernel Logger. To send what Nest itself logs through the same instance, hand it a bridge of a few lines; the kit ships none, since it would bind this package to 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 is the alternative when Nest classes log a lot: give it the winston instance you built, and wrap the same instance in new WinstonLogger(instance) for the kit.

In a test

Nothing of this package: the test application binds InMemoryLogger of the kernel, and its entries are data. To hold another adapter of the port to the same behaviour, run the contract:

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 };
}

版本与发布记录

@ddd-ts-kit/winston 共发布 63 个版本,首次发布于 2026/10/11;过去 12 个月发布了 63 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.106.0latest2026/11/012 天前
0.105.02026/10/312 天前
0.104.02026/10/313 天前
0.103.02026/10/313 天前
0.102.02026/10/313 天前
0.101.02026/10/303 天前

关于 @ddd-ts-kit/winston 的常见问题

@ddd-ts-kit/winston 是什么?

温斯顿内核记录器端口的适配器,每行都有跟踪和执行关联。

如何安装 @ddd-ts-kit/winston?

在项目目录运行 npm install @ddd-ts-kit/winston,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:winston(^3.19.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@ddd-ts-kit/winston 支持 TypeScript 吗?

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

@ddd-ts-kit/winston 支持 ESM 还是 CommonJS?

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

@ddd-ts-kit/winston 需要什么版本的 Node.js?

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

@ddd-ts-kit/winston 可以商用吗?

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

@ddd-ts-kit/winston 还在维护吗?

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