@ddd-ts-kit/winston 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.fr.md +369 -0
  2. package/README.md +75 -5
  3. package/package.json +3 -3
package/README.fr.md ADDED
@@ -0,0 +1,369 @@
1
+ [English](README.md) · **Français**
2
+ <!-- translation-of: README.md sha256:37676b9e126706c6 -->
3
+
4
+ # @ddd-ts-kit/winston
5
+
6
+ Le port `Logger` du noyau sur [winston](https://github.com/winstonjs/winston) : des lignes JSON
7
+ sur la sortie standard, les champs de corrélation du travail en cours sur chaque ligne, les
8
+ secrets d'authentification masqués, un vidage (*flush*) pour l'arrêt. Rien dans `domain/` ni dans
9
+ `application/` n'importe ce *package*, ni ne journalise quoi que ce soit : la racine de
10
+ composition lie l'adaptateur, et l'infrastructure, la présentation et les aspects écrivent par le
11
+ port.
12
+
13
+ ```
14
+ npm i @ddd-ts-kit/winston winston
15
+ ```
16
+
17
+ Écrit pour winston 3.19.
18
+
19
+ Le domaine et les gestionnaires (handlers) ne journalisent rien : ce qu'ils ont à dire est un
20
+ événement ou une erreur. Les adaptateurs de `infrastructure/`, les contrôleurs de
21
+ `presentation/` et les aspects (`logged`, `loggingMiddleware`, le planificateur de *jobs*)
22
+ reçoivent le `Logger` du noyau, et la racine de composition y lie `WinstonLogger` une fois. Le
23
+ code qui journalise est le même avec `@ddd-ts-kit/pino` ou `StdoutLogger` : voir
24
+ [Journaliser depuis un adaptateur](../pino/README.fr.md#journaliser-depuis-un-adaptateur).
25
+
26
+ ## Racine de composition
27
+
28
+ <!-- checked sample -->
29
+ ```ts
30
+ // platform/observability/create-logger.ts
31
+ import type { ExecutionScope, LogLevel } from '@ddd-ts-kit/kernel';
32
+ import { traceLogFields } from '@ddd-ts-kit/tracing';
33
+ import { createWinstonLogger, type WinstonLogger } from '@ddd-ts-kit/winston';
34
+
35
+ export interface LogSettings {
36
+ readonly level: LogLevel; // LOG_LEVEL
37
+ readonly format: 'json' | 'pretty'; // LOG_FORMAT
38
+ readonly service: string;
39
+ readonly environment: string;
40
+ readonly scrubMessages: boolean; // LOG_SCRUB_MESSAGES
41
+ }
42
+
43
+ export function createLogger(scope: ExecutionScope, settings: LogSettings): WinstonLogger {
44
+ return createWinstonLogger({ ...settings, fields: () => traceLogFields(scope) });
45
+ }
46
+ ```
47
+
48
+ ```ts
49
+ // main.ts
50
+ import { tracing } from '@/platform/tracing'; // en premier
51
+ import { createLogger } from '@/platform/observability/create-logger';
52
+ import { AsyncLocalStorageExecutionScope } from '@ddd-ts-kit/kernel';
53
+ import { createApplication, kernelModule } from '@ddd-ts-kit/di';
54
+
55
+ const scope = new AsyncLocalStorageExecutionScope();
56
+ const logger = createLogger(scope, config.log);
57
+
58
+ const app = createApplication({
59
+ modules: [kernelModule({ executionScope: scope, logger }), ...contexts],
60
+ });
61
+
62
+ process.once('SIGTERM', () => void shutdown());
63
+
64
+ async function shutdown(): Promise<void> {
65
+ await app.stop(); // vide le rapporteur d'erreurs, puis le logger, en dernier : cela clôt le logger winston
66
+ await tracing.shutdown();
67
+ }
68
+ ```
69
+
70
+ `init --logger winston` écrit ce `create-logger.ts` (il renvoie un `NoopLogger` quand il ne
71
+ reçoit aucun réglage : c'est ainsi que les outils et les tests restent silencieux) ;
72
+ `platform/lay-foundations.ts` l'appelle une fois et `composeApplication` passe le *logger* à
73
+ `kernelModule({ logger })`.
74
+
75
+ Une ligne de l'application, telle qu'elle est écrite :
76
+
77
+ ```json
78
+ {"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"}
79
+ ```
80
+
81
+ ![Le chemin d'un appel à logger.info à travers winston : sous le seuil, l'adaptateur s'arrête après une comparaison ; sinon il décrit les erreurs parmi les champs et en tient à l'écart level et message ; winston fusionne les champs de la ligne, les champs des enfants et son defaultMeta ; puis les formats s'exécutent dans l'ordre : correlationFields remplit depuis fields() les clés que personne n'a données, redactCredentials masque les clés de secret, isoTime écrit time, json ou la ligne lisible sérialise ; chaque transport écrit la ligne : la console par défaut, ou les fichiers, syslog ou le flux que vous donnez](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/winston-path-of-a-line.fr.svg)
82
+
83
+ **Corrélation.** `fields` est lu à chaque ligne écrite, par un format winston : aucun point
84
+ d'appel ne passe d'identifiant. `traceLogFields(scope)` renvoie les identifiants de trace et de
85
+ *span* à l'intérieur d'un *span*, et, à l'intérieur de la portée d'exécution, l'identifiant de
86
+ corrélation, l'identifiant de causalité, l'origine, l'acteur et le *tenant*. Ce *package* ne
87
+ dépend pas du *package* de traçage : toute fonction `() => LogFields` convient,
88
+ `() => logFieldsOf(scope.current())` comprise. Une fonction qui lève une exception ne donne aucun
89
+ champ, jamais une ligne perdue. Les champs de la ligne l'emportent sur ceux d'un enfant, qui
90
+ l'emportent sur `fields` : le format ne remplit que les clés que personne n'a données.
91
+ `StdoutLogger` et `@ddd-ts-kit/pino` diffèrent sur la deuxième marche (là, `fields` l'emporte sur
92
+ un enfant) : ne liez donc pas dans un enfant une clé que `fields` renvoie si le *logger* peut
93
+ être remplacé.
94
+
95
+ ![La valeur que prend une clé donnée deux fois, de la plus faible à la plus forte : avec pino et StdoutLogger, service, environment et base, puis les enfants, puis fields(), puis la ligne, qui l'emporte ; avec winston, fields(), puis les enfants, puis la ligne, puis service, environment et base, qui l'emportent](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/pino-which-value-wins.fr.svg)
96
+
97
+ **Une instance à vous.** Le projet qui configure déjà winston continue de le faire, et prend les
98
+ formats qu'il veut :
99
+
100
+ <!-- checked sample -->
101
+ ```ts
102
+ import { CREDENTIAL_KEYS, type ExecutionScope } from '@ddd-ts-kit/kernel';
103
+ import { traceLogFields } from '@ddd-ts-kit/tracing';
104
+ import winston from 'winston';
105
+ import { WinstonLogger, correlationFields, redactCredentials } from '@ddd-ts-kit/winston';
106
+
107
+ declare const scope: ExecutionScope; // celle que crée la racine de composition
108
+
109
+ const instance = winston.createLogger({
110
+ level: 'info',
111
+ format: winston.format.combine(
112
+ correlationFields(() => traceLogFields(scope)),
113
+ redactCredentials([...CREDENTIAL_KEYS, 'iban']), // après les formats qui ajoutent des champs
114
+ winston.format.timestamp(),
115
+ winston.format.json(),
116
+ ),
117
+ transports: [new winston.transports.Console()],
118
+ });
119
+
120
+ export const logger = new WinstonLogger(instance);
121
+ ```
122
+
123
+ `createWinstonLogger` écrit l'instant sous `time`, le niveau par son nom et le message sous
124
+ `message`, comme le `StdoutLogger` du *package* des aspects et comme `@ddd-ts-kit/pino` : un
125
+ même tableau de bord lit les trois. Les clés d'une ligne sortent triées : le format JSON de
126
+ winston sérialise avec un sérialiseur stable.
127
+
128
+ **Des transports à vous**, sans configurer winston vous-même : `transports` remplace la console,
129
+ nommez-la donc aussi quand les lignes doivent encore atteindre la sortie standard.
130
+
131
+ <!-- checked sample -->
132
+ ```ts
133
+ import { createWinstonLogger } from '@ddd-ts-kit/winston';
134
+ import winston from 'winston';
135
+
136
+ export const logger = createWinstonLogger({
137
+ service: 'shop-api',
138
+ transports: [
139
+ new winston.transports.Console(), // la sortie standard, comme par défaut
140
+ new winston.transports.File({ filename: 'logs/errors.log', level: 'error' }), // les erreurs seulement, gardées sur disque
141
+ ],
142
+ });
143
+ ```
144
+
145
+ Chaque transport filtre selon son propre `level` après l'exécution des formats : le fichier
146
+ ci-dessus reçoit les lignes `error`, la console chaque ligne à partir du seuil du *logger*.
147
+
148
+ ## Mode développement
149
+
150
+ ```ts
151
+ createWinstonLogger({ level: 'debug', format: 'pretty', fields: () => traceLogFields(scope) });
152
+ ```
153
+
154
+ ```
155
+ 10:00:00.000 info order shipped { context: 'shipping', service: 'shop-api', correlationId: '9f2c' }
156
+ ```
157
+
158
+ Le format `pretty` est fait des combinateurs de winston lui-même (`colorize`, `printf`), dans le
159
+ processus : aucun autre *package*, l'heure locale, le niveau en couleur, les champs tels que Node
160
+ affiche un objet, une pile sur plusieurs lignes. Choisissez le format par la configuration
161
+ (`LOG_FORMAT=pretty` dans `.env`, `json` partout ailleurs).
162
+
163
+ ## Confidentialité
164
+
165
+ Les lignes de journal sont l'endroit où les données personnelles fuient : elles sont écrites
166
+ partout, lues par tout le monde et conservées longtemps. Trois choses sont faites dans
167
+ l'adaptateur, pour qu'aucun point d'appel n'ait à s'en souvenir.
168
+
169
+ **Les secrets d'authentification sont masqués** par un format winston, dans les champs d'une
170
+ ligne, d'un enfant et de `fields` de la même façon. La valeur de ces clés est remplacée par
171
+ `[redacted]`, au premier niveau d'une ligne et un niveau en dessous (`headers.authorization`,
172
+ `body.password`) :
173
+
174
+ `password`, `authorization`, `cookie`, `set-cookie`, `token`, `secret`, `apiKey`, `api_key`,
175
+ `access_token`, `refresh_token`, `accessToken`, `refreshToken`, `client_secret`
176
+
177
+ `redact: ['iban', 'ssn']` complète la liste. Les noms sont comparés exactement, casse comprise,
178
+ et rien n'est cherché plus profond qu'un niveau : journalisez la partie que vous visez, pas une
179
+ requête HTTP entière. Les objets que vous passez ne sont jamais modifiés : un objet imbriqué est
180
+ copié quand une clé doit être masquée.
181
+
182
+ **Une erreur passée en champ** (`logger.error('payment failed', { error })` : la `LogValue` du
183
+ port admet une `Error`) est écrite telle que la décrit `describeError` du noyau, de la même façon
184
+ depuis tous les *loggers* du port : sa classe en `type`, son `code` quand c'est une
185
+ `DomainError`, son `message`, sa propre `stack`, puis sa `cause` de la même façon, jusqu'à huit
186
+ niveaux. Chaque erreur d'une chaîne est écrite une fois. Les erreurs sont cherchées parmi les
187
+ champs de la ligne, pas à l'intérieur d'objets imbriqués. `CREDENTIAL_KEYS`, `REDACTED` et
188
+ `SCRUBBED_MESSAGE` sont eux aussi ceux du noyau.
189
+
190
+ **Les messages des erreurs inattendues peuvent être retirés.** Le message d'une erreur est le
191
+ premier endroit où apparaissent des données personnelles : un pilote cite la ligne qu'il a
192
+ refusée. Par défaut, il est gardé, parce qu'une ligne de journal qui dit `[message removed]` coûte
193
+ à chaque investigation. Avec une option, seuls survivent les messages que le projet a écrits
194
+ lui-même, ceux de ses `DomainError` ; classe, code, pile et cause restent pour les autres, et la
195
+ pile perd sa première ligne, qui répète le message :
196
+
197
+ ```ts
198
+ createWinstonLogger({ scrubMessages: true }); // ou new WinstonLogger(instance, { scrubMessages: true })
199
+ ```
200
+
201
+ C'est le `scrubMessages` que prend chaque *logger* du port (`StdoutLogger`, pino, winston,
202
+ `InMemoryLogger`) et celui de `@ddd-ts-kit/sentry`, avec la même règle, le même
203
+ `SCRUBBED_MESSAGE` et le même compromis : activez-le quand les journaux sortent de vos mains ou
204
+ quand aucun opérateur ne doit y lire ces données. Ce que dit le message d'une ligne de journal
205
+ lui-même reste à votre charge : écrivez `order shipped` et mettez les identifiants dans des
206
+ champs.
207
+
208
+ Le kit lui-même ne confie jamais une erreur à un *logger* : l'aspect `logged`,
209
+ `loggingMiddleware` et le planificateur de *jobs* écrivent seulement `errorType` et `errorCode`
210
+ (`identifyError` du noyau), jamais un message, quoi que dise `scrubMessages`, et l'erreur va à
211
+ l'`ErrorReporter`. La description ci-dessus vaut pour l'erreur que votre propre code choisit de
212
+ journaliser : `logger.error('payment failed', { error })`.
213
+
214
+ ## Niveaux
215
+
216
+ | Port | winston (niveaux `npm`) | |
217
+ |---|---|---|
218
+ | `debug` | `debug` | en dessous, winston a aussi `silly` |
219
+ | `info` | `info` | le seuil par défaut ; `http` et `verbose` se placent entre `info` et `debug` |
220
+ | `warn` | `warn` | |
221
+ | `error` | `error` | |
222
+
223
+ Les noms sont les mêmes dans les niveaux `cli`. Une instance sur les niveaux `syslog` n'a pas de
224
+ `warn` : dites où il va, sinon la ligne est écartée, puisque winston n'écrit pas à un niveau qu'il
225
+ ne connaît pas.
226
+
227
+ <!-- checked sample -->
228
+ ```ts
229
+ import { WinstonLogger } from '@ddd-ts-kit/winston';
230
+ import winston from 'winston';
231
+
232
+ const instance = winston.createLogger({
233
+ levels: winston.config.syslog.levels, // emerg, alert, crit, error, warning, notice, info, debug
234
+ transports: [new winston.transports.Console()],
235
+ });
236
+
237
+ export const logger = new WinstonLogger(instance, { levels: { warn: 'warning' } }); // logger.warn() écrit au niveau 'warning'
238
+ ```
239
+
240
+ Une ligne sous le seuil coûte une comparaison : l'adaptateur interroge d'abord
241
+ `isLevelEnabled`, parce que winston seul exécuterait chaque format avant que ses transports
242
+ n'écartent la ligne. Le seuil d'une instance modifié en cours d'exécution
243
+ (`instance.level = 'debug'`) est suivi par l'adaptateur et par ses enfants. `isLevelEnabled(level)`
244
+ du port répond à la même question pour le niveau correspondant : interrogez-le avant de
245
+ construire des champs coûteux. Une fois le *logger* vidé, il répond `false` pour chaque niveau.
246
+
247
+ `child(fields)` est un enfant winston : les champs sont liés une fois, et winston les fusionne
248
+ sous les champs de chaque ligne. Trois choses diffèrent de ce que fait winston appelé
249
+ directement, toutes volontairement : un champ nommé `level` ou `message` ne remplace jamais le
250
+ niveau ou le message de la ligne ; le message n'est jamais lu comme une chaîne de format (`%s`) ;
251
+ et avec `createWinstonLogger`, un champ nommé `time` est écrasé par l'instant de la ligne. Une
252
+ chose relève de winston : `service`, `environment` et `base` sont son `defaultMeta`, qui
253
+ l'emporte sur un champ du même nom sur une ligne ou dans un enfant, là où les deux autres
254
+ *loggers* du kit laissent la ligne et l'enfant l'emporter.
255
+
256
+ ## Vidage
257
+
258
+ `flush(): Promise<void>` clôt le *logger* winston et se résout une fois que chacun de ses
259
+ transports a écrit ce qui lui a été confié (leur événement `finish`). Il ne rejette jamais, et un
260
+ second appel renvoie la même promesse. `app.stop()` l'appelle, en dernier, après l'arrêt des
261
+ modules et le vidage du rapporteur d'erreurs : l'arrêt journalise lui aussi. Sans l'application
262
+ du *package* di, appelez-le vous-même à cet endroit.
263
+
264
+ | Une fois `flush()` résolu | `@ddd-ts-kit/winston` | `@ddd-ts-kit/pino` |
265
+ |---|---|---|
266
+ | une ligne journalisée | écartée, sans erreur | écrite, comme avant |
267
+ | `isLevelEnabled(level)` | `false` pour chaque niveau | le seuil, comme avant |
268
+ | un second `flush()` | la même promesse | un nouveau vidage |
269
+
270
+ Le port appelle un vidage une fois, à l'arrêt, et ne précise pas ce qui est journalisé ensuite.
271
+ Ici le vidage est définitif : un *logger* winston ne se rouvre pas, donc ce qui est journalisé
272
+ ensuite, par ce *logger* ou l'un de ses enfants, est écarté sans erreur, et `isLevelEnabled`
273
+ répond `false`. Tout ce qui doit être journalisé à propos de l'arrêt l'est avant le retour de
274
+ `app.stop()`. Un transport sur un flux que vous avez ouvert vous-même (une *socket*, un flux de
275
+ fichier donné au transport `Stream`) a confié ses lignes à ce flux quand `flush()` se résout :
276
+ fermer ce flux vous revient.
277
+
278
+ ## Performances
279
+
280
+ winston écrit chaque ligne par un flux Node, une chaîne de formats et ses transports, le tout
281
+ dans le processus et dans l'appel : environ 7 microsecondes par ligne là où `@ddd-ts-kit/pino` en
282
+ prend 3 (Node 24, un enfant, quatre champs de corrélation, trois champs, un récepteur qui jette
283
+ tout), et davantage à chaque format ajouté. Chaque ligne coûte ici une copie de ses champs, une
284
+ fusion par niveau d'enfant, le format qui lit `fields`, le passage de masquage sur les valeurs de
285
+ premier niveau, l'instant, et `JSON.stringify` par un sérialiseur sûr. Ce n'est rien à quelques
286
+ centaines de lignes par seconde et c'est visible à des dizaines de milliers : là, baissez le
287
+ niveau ou prenez `@ddd-ts-kit/pino`.
288
+
289
+ Rien n'est mis en tampon par l'adaptateur, donc rien n'est perdu lors d'un plantage au-delà de ce
290
+ que le transport lui-même n'avait pas écrit. Les appels de journalisation ne lèvent jamais
291
+ d'exception : des champs qui ne peuvent être ni lus ni sérialisés (un accesseur qui lève une
292
+ exception) donnent la ligne sans eux, marquée `logError: "unserializable fields"`, et
293
+ `createWinstonLogger` tient ses formats à la même règle à l'intérieur du flux.
294
+
295
+ ## Lequel choisir
296
+
297
+ Prenez `StdoutLogger` du *package* des aspects quand des lignes JSON sur la sortie standard vous
298
+ suffisent : aucune dépendance, la même forme de ligne. Prenez `@ddd-ts-kit/pino` quand le volume
299
+ de journaux compte ou quand le *framework* parle déjà pino (Fastify). Prenez winston quand
300
+ l'équipe utilise déjà ses transports et ses formats (fichiers avec rotation, syslog, un transport
301
+ d'éditeur) et veut les garder. Les trois implémentent un même port et sont tenus par une même
302
+ suite de contrat : en changer tient en une ligne de la racine de composition.
303
+
304
+ ## NestJS
305
+
306
+ Nest a son propre `LoggerService`, utilisé par le *framework* pour ses propres messages et par
307
+ `new Logger(context)` dans les classes Nest. Le code applicatif du kit n'en a jamais besoin : les
308
+ gestionnaires et les adaptateurs reçoivent le `Logger` du noyau. Pour faire passer ce que Nest
309
+ journalise lui-même par la même instance, donnez-lui un pont de quelques lignes ; le kit n'en
310
+ livre aucun, car il lierait ce *package* à Nest :
311
+
312
+ ```ts
313
+ const nest = logger.child({ context: 'nest' });
314
+ const bridge: LoggerService = {
315
+ log: (message: unknown) => nest.info(String(message)),
316
+ warn: (message: unknown) => nest.warn(String(message)),
317
+ error: (message: unknown) => nest.error(String(message)),
318
+ debug: (message: unknown) => nest.debug(String(message)),
319
+ };
320
+
321
+ const app = await NestFactory.create(AppModule, { logger: bridge });
322
+ ```
323
+
324
+ `nest-winston` est l'alternative quand les classes Nest journalisent beaucoup : donnez-lui
325
+ l'instance winston que vous avez construite, et enveloppez la même instance dans
326
+ `new WinstonLogger(instance)` pour le kit.
327
+
328
+ ## Dans un test
329
+
330
+ Rien de ce *package* : l'application de test lie l'`InMemoryLogger` du noyau, et ses `entries`
331
+ sont des données. Pour tenir un autre adaptateur du port au même comportement, lancez le
332
+ contrat :
333
+
334
+ <!-- checked sample -->
335
+ ```ts
336
+ import { Writable } from 'node:stream';
337
+ import { beforeEach, describe, expect, it } from 'vitest';
338
+ import { LOG_LEVELS } from '@ddd-ts-kit/kernel';
339
+ import { loggerContract, type LoggedLine } from '@ddd-ts-kit/testing/contracts';
340
+ import { createWinstonLogger } from '@ddd-ts-kit/winston';
341
+ import winston from 'winston';
342
+
343
+ loggerContract({ describe, it, expect, beforeEach }, {
344
+ name: 'WinstonLogger',
345
+ createLogger: (setup) => { // { level, fields, scrubMessages }
346
+ const written: string[] = [];
347
+ const stream = new Writable({
348
+ write: (chunk, _encoding, done) => {
349
+ written.push(String(chunk));
350
+ done();
351
+ },
352
+ });
353
+ const logger = createWinstonLogger({ ...setup, transports: [new winston.transports.Stream({ stream })] });
354
+
355
+ return { logger, lines: () => written.map((line) => lineOf(JSON.parse(line))) };
356
+ },
357
+ });
358
+
359
+ /** Une ligne écrite telle que le contrat la lit : le niveau par son nom, le message, les autres champs. */
360
+ function lineOf({ level, message, time: _time, ...fields }: Record<string, unknown>): LoggedLine {
361
+ const known = LOG_LEVELS.find((one) => one === level);
362
+
363
+ if (known === undefined) {
364
+ throw new Error(`Not a level of the port: ${String(level)}`);
365
+ }
366
+
367
+ return { level: known, message: String(message), fields };
368
+ }
369
+ ```
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ **English** · [Français](README.fr.md)
2
+
1
3
  # @ddd-ts-kit/winston
2
4
 
3
5
  The kernel `Logger` port on [winston](https://github.com/winstonjs/winston): JSON lines on the
@@ -12,8 +14,15 @@ npm i @ddd-ts-kit/winston winston
12
14
 
13
15
  Written against winston 3.19.
14
16
 
17
+ The domain and the handlers log nothing: what they have to say is an event or an error. The
18
+ adapters of `infrastructure/`, the controllers of `presentation/` and the aspects (`logged`,
19
+ `loggingMiddleware`, the job scheduler) receive the kernel `Logger`, and the composition root
20
+ binds `WinstonLogger` to it once. The code that logs is the same with `@ddd-ts-kit/pino` or
21
+ `StdoutLogger`: see [Logging from an adapter](../pino/README.md#logging-from-an-adapter).
22
+
15
23
  ## Composition root
16
24
 
25
+ <!-- checked sample -->
17
26
  ```ts
18
27
  // platform/observability/create-logger.ts
19
28
  import type { ExecutionScope, LogLevel } from '@ddd-ts-kit/kernel';
@@ -36,6 +45,7 @@ export function createLogger(scope: ExecutionScope, settings: LogSettings): Wins
36
45
  ```ts
37
46
  // main.ts
38
47
  import { tracing } from '@/platform/tracing'; // first
48
+ import { createLogger } from '@/platform/observability/create-logger';
39
49
  import { AsyncLocalStorageExecutionScope } from '@ddd-ts-kit/kernel';
40
50
  import { createApplication, kernelModule } from '@ddd-ts-kit/di';
41
51
 
@@ -64,6 +74,8 @@ One line of the application, as it is written:
64
74
  {"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"}
65
75
  ```
66
76
 
77
+ ![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](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/winston-path-of-a-line.svg)
78
+
67
79
  **Correlation.** `fields` is read at each line that is written, by a winston format: no call
68
80
  site passes an id. `traceLogFields(scope)` answers the trace and span ids inside a span, and
69
81
  inside the execution scope the correlation id, the causation id, the origin, the actor and the
@@ -74,14 +86,20 @@ format only fills the keys nobody gave. `StdoutLogger` and `@ddd-ts-kit/pino` di
74
86
  second step (there `fields` win over a child), so do not bind in a child a key that `fields`
75
87
  answers if the logger may be swapped.
76
88
 
89
+ ![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](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/pino-which-value-wins.svg)
90
+
77
91
  **An instance of your own.** The project that already configures winston keeps doing so, and
78
92
  takes the formats it wants:
79
93
 
94
+ <!-- checked sample -->
80
95
  ```ts
81
- import { CREDENTIAL_KEYS } from '@ddd-ts-kit/kernel';
96
+ import { CREDENTIAL_KEYS, type ExecutionScope } from '@ddd-ts-kit/kernel';
97
+ import { traceLogFields } from '@ddd-ts-kit/tracing';
82
98
  import winston from 'winston';
83
99
  import { WinstonLogger, correlationFields, redactCredentials } from '@ddd-ts-kit/winston';
84
100
 
101
+ declare const scope: ExecutionScope; // the one the composition root creates
102
+
85
103
  const instance = winston.createLogger({
86
104
  level: 'info',
87
105
  format: winston.format.combine(
@@ -93,7 +111,7 @@ const instance = winston.createLogger({
93
111
  transports: [new winston.transports.Console()],
94
112
  });
95
113
 
96
- const logger = new WinstonLogger(instance);
114
+ export const logger = new WinstonLogger(instance);
97
115
  ```
98
116
 
99
117
  `createWinstonLogger` writes the instant under `time`, the level by its name and the message
@@ -101,6 +119,26 @@ under `message`, like the `StdoutLogger` of the aspects package and like `@ddd-t
101
119
  one dashboard reads the three. The keys of a line come out sorted: winston's JSON format
102
120
  serializes with a stable stringifier.
103
121
 
122
+ **Transports of your own** without configuring winston yourself: `transports` replaces the
123
+ console, so name it too when the lines must still reach the standard output.
124
+
125
+ <!-- checked sample -->
126
+ ```ts
127
+ import { createWinstonLogger } from '@ddd-ts-kit/winston';
128
+ import winston from 'winston';
129
+
130
+ export const logger = createWinstonLogger({
131
+ service: 'shop-api',
132
+ transports: [
133
+ new winston.transports.Console(), // the standard output, as by default
134
+ new winston.transports.File({ filename: 'logs/errors.log', level: 'error' }), // errors only, kept on disk
135
+ ],
136
+ });
137
+ ```
138
+
139
+ Each transport filters by its own `level` after the formats ran: the file above receives the
140
+ `error` lines, the console every line from the threshold of the logger.
141
+
104
142
  ## Development mode
105
143
 
106
144
  ```ts
@@ -175,8 +213,17 @@ The names are the same in the `cli` levels. An instance on the `syslog` levels h
175
213
  say where it goes, or the line is dropped, since winston does not write at a level it does
176
214
  not know.
177
215
 
216
+ <!-- checked sample -->
178
217
  ```ts
179
- new WinstonLogger(instance, { levels: { warn: 'warning' } });
218
+ import { WinstonLogger } from '@ddd-ts-kit/winston';
219
+ import winston from 'winston';
220
+
221
+ const instance = winston.createLogger({
222
+ levels: winston.config.syslog.levels, // emerg, alert, crit, error, warning, notice, info, debug
223
+ transports: [new winston.transports.Console()],
224
+ });
225
+
226
+ export const logger = new WinstonLogger(instance, { levels: { warn: 'warning' } }); // logger.warn() writes at 'warning'
180
227
  ```
181
228
 
182
229
  A line below the threshold costs one comparison: the adapter asks `isLevelEnabled` first,
@@ -203,6 +250,12 @@ the same promise. `app.stop()` calls it, last, after the modules stopped and the
203
250
  was flushed: stopping logs too. Without the application of the di package, call it yourself at
204
251
  that place.
205
252
 
253
+ | After `flush()` resolved | `@ddd-ts-kit/winston` | `@ddd-ts-kit/pino` |
254
+ |---|---|---|
255
+ | a line logged | dropped, without an error | written, like before |
256
+ | `isLevelEnabled(level)` | `false` for every level | the threshold, as before |
257
+ | a second `flush()` | the same promise | a new flush |
258
+
206
259
  The port calls a flush once, at shutdown, and leaves what is logged afterwards unspecified.
207
260
  Here the flush is final: a winston logger does not reopen, so what is logged afterwards,
208
261
  through this logger or one of its children, is dropped without an error, and `isLevelEnabled`
@@ -262,8 +315,14 @@ you built, and wrap the same instance in `new WinstonLogger(instance)` for the k
262
315
  Nothing of this package: the test application binds `InMemoryLogger` of the kernel, and its
263
316
  `entries` are data. To hold another adapter of the port to the same behaviour, run the contract:
264
317
 
318
+ <!-- checked sample -->
265
319
  ```ts
266
- import { loggerContract } from '@ddd-ts-kit/testing/contracts';
320
+ import { Writable } from 'node:stream';
321
+ import { beforeEach, describe, expect, it } from 'vitest';
322
+ import { LOG_LEVELS } from '@ddd-ts-kit/kernel';
323
+ import { loggerContract, type LoggedLine } from '@ddd-ts-kit/testing/contracts';
324
+ import { createWinstonLogger } from '@ddd-ts-kit/winston';
325
+ import winston from 'winston';
267
326
 
268
327
  loggerContract({ describe, it, expect, beforeEach }, {
269
328
  name: 'WinstonLogger',
@@ -277,7 +336,18 @@ loggerContract({ describe, it, expect, beforeEach }, {
277
336
  });
278
337
  const logger = createWinstonLogger({ ...setup, transports: [new winston.transports.Stream({ stream })] });
279
338
 
280
- return { logger, lines: () => written.map((line) => lineOf(JSON.parse(line))) }; // { level, message, fields }
339
+ return { logger, lines: () => written.map((line) => lineOf(JSON.parse(line))) };
281
340
  },
282
341
  });
342
+
343
+ /** A written line as the contract reads it: the level by its name, the message, the other fields. */
344
+ function lineOf({ level, message, time: _time, ...fields }: Record<string, unknown>): LoggedLine {
345
+ const known = LOG_LEVELS.find((one) => one === level);
346
+
347
+ if (known === undefined) {
348
+ throw new Error(`Not a level of the port: ${String(level)}`);
349
+ }
350
+
351
+ return { level: known, message: String(message), fields };
352
+ }
283
353
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddd-ts-kit/winston",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Adapter of the kernel Logger port on winston, with trace and execution correlation on every line.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,10 +26,10 @@
26
26
  "dist"
27
27
  ],
28
28
  "dependencies": {
29
- "@ddd-ts-kit/kernel": "0.16.0"
29
+ "@ddd-ts-kit/kernel": "0.16.1"
30
30
  },
31
31
  "devDependencies": {
32
- "@ddd-ts-kit/testing": "0.16.0",
32
+ "@ddd-ts-kit/testing": "0.16.1",
33
33
  "winston": "^3.19.0"
34
34
  },
35
35
  "peerDependencies": {