@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.
- package/README.fr.md +369 -0
- package/README.md +75 -5
- 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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
-
|
|
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 {
|
|
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))) };
|
|
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.
|
|
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.
|
|
29
|
+
"@ddd-ts-kit/kernel": "0.16.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@ddd-ts-kit/testing": "0.16.
|
|
32
|
+
"@ddd-ts-kit/testing": "0.16.1",
|
|
33
33
|
"winston": "^3.19.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|