@jmlq/logger 0.1.0-alpha.5 → 0.1.0-alpha.7

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/LICENSE +21 -0
  2. package/README.md +637 -169
  3. package/package.json +2 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2025 mlahuasi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the “Software”), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md CHANGED
@@ -34,153 +34,355 @@ npm i pg@^8.16.3
34
34
 
35
35
  ---
36
36
 
37
- ## 🧱 Estructura del paquete
37
+ ## 🧩 Configuración
38
38
 
39
- ### 📝 Resumen rápido
39
+ ### 🔐 Variables de Entorno (.env)
40
40
 
41
- > - **`src/domain/`** — Reglas del negocio (sin dependencias de frameworks).
42
- > > - **`value-objects/`**
43
- > > > - `log-level.ts` Define niveles (`TRACE…FATAL`) y `toLogLevel()` para convertir strings a nivel.
44
- > > - **`types/`**
45
- > > > - `log.types.ts` Tipos puros del dominio: `ILog`, `IGetLogsFilter`, `PiiOptions`, etc.
46
- > > - **`contracts/`**
47
- > > > - `log.datasource.ts` — Puerto que debe implementar cualquier destino de logs (`save/find/flush/dispose`).
48
- > > > - `logger.ts` — Contrato del logger público (`ILogger`, `ICreateLoggerOptions`).
49
- > > > - `pii.ts` — Contrato del redactor de PII.
50
- > > - **`services/`**
51
- > > > - `pii-redactor.ts` — Enmascarado de datos sensibles (whitelist/blacklist, patrones, modo profundo).
41
+ ```ini
42
+ # @jmlq/logger
43
+ # Nivel mínimo por entorno (dev: debug, prod: warn)
44
+ LOGGER_LEVEL=debug
45
+ # PII (Personally Identifiable Information)
46
+ LOGGER_PII_ENABLED=true
47
+ LOGGER_PII_INCLUDE_DEFAULTS=true
48
+
49
+ # MONGO DB @jmlq/logger-plugin-mongo
50
+ MONGO_URL=mongodb://<user>:<password>@localhost:<port>
51
+ MONGO_DB_NAME=<db-name>
52
+ MONGO_COLLECTION=<collection-name>
53
+ LOGGER_MONGO_RETENTION_DAYS=30
54
+
55
+ # POSTGRESQL @jmlq/logger-plugin-postgresql
56
+ POSTGRES_URL="postgresql://mlahuasi:123456@localhost:5432/NOC"
57
+ POSTGRES_DB=<db-name>
58
+ POSTGRES_SCHEMA=public
59
+ POSTGRES_TABLE=<table-name>
60
+ LOGGER_PG_RETENTION_DAYS=30
61
+
62
+ # FILESYSTEM @jmlq/logger-plugin-fs
63
+ LOGGER_FS_PATH=./logs
52
64
 
53
- > - **`src/application/`** — Orquestación de casos de uso (no depende de infraestructura).
54
- > > - **`use-cases/`**
55
- > > > - `save-log.ts` — Aplica `minLevel` + PII y delega a `datasource.save()`.
56
- > > > - `get-logs.ts` — Recupera logs con filtros/paginación si el datasource lo soporta.
57
- > > > - `flush-buffers.ts` — Ejecuta `flush()` en el datasource cuando exista.
65
+ ```
58
66
 
59
- > - **`src/infrastructure/`** — Adaptadores concretos (tecnología).
60
- > > - **`adapters/`**
61
- > > > - `composite.datasource.ts` — Fan-out: envía el log a varios datasources y no falla el todo si uno cae (avisa con `console.warn`).
67
+ ---
62
68
 
63
- > - **`src/presentation/`** — API pública y fábricas (cara del paquete).
64
- > > - **`factory/`**
65
- > > > - `create-logger.ts` — Crea el logger listo para usar (`trace…fatal`, `flush`, `dispose`) conectando casos de uso + PII.
69
+ ### 🚀 Uso del paquete
66
70
 
67
- > - **`src/index.ts`** — Barrel.
68
- > > - Re-exporta lo público: `createLogger`, `LogLevel`, tipos/contratos y `CompositeDatasource`.
71
+ #### 1) Estructura recomendada
69
72
 
70
- ### [LEER MAS](./ARQUITECTURA.md)
73
+ ```
74
+ src/
75
+ ├─ infrastructure/
76
+ │ ├─ logger/
77
+ │ │ ├─ adapters/
78
+ │ │ │ ├─ fs.adapter.ts
79
+ │ │ │ ├─ mongo.adapter.ts
80
+ │ │ │ └─ postgresql.adapter.ts
81
+ │ │ ├─ settings/
82
+ │ │ │ ├─ pii.settings.ts
83
+ │ │ │ └─ loglevel.settings.ts
84
+ │ │ └─ bootstrap.ts # LoggerBootstrap (orquesta adapters + PII)
85
+ │ └─ plugins/
86
+ │ ├─ env.plugin.ts
87
+ │ └─ index.ts
88
+ ├─ config/
89
+ │ └─ logger/
90
+ │ └─ index.ts # singleton global: loggerReady / flush / dispose
91
+ └─ presentation/
92
+ └─ server.ts # uso del logger en Express
71
93
 
72
- ---
94
+ ```
73
95
 
74
- ## 🧩 Configuración
96
+ #### 2) Settings
75
97
 
76
- ### 🔐 Variables de Entorno (.env)
98
+ #### 2.1) pii.settings.ts (patrones PII + merge)
77
99
 
78
- ```ini
79
- # --- MongoDB ---
80
- MONGO_URL=mongodb://<usuario>:<password>@localhost:27017
81
- # Dirección de conexión a MongoDB (usuario/contraseña opcionales)
82
- MONGO_DB_NAME=my_database
83
- # Nombre de la base de datos donde se guardarán los logs
100
+ ```ts
101
+ //Se definine reglas de redacción PII (buscar-y-reemplazar con regex) que el logger aplica antes de persistir/emitir un log.
102
+
103
+ export type PiiReplacement = {
104
+ pattern: string;
105
+ replaceWith: string;
106
+ flags?: string; // regex (g, i, m, s, u, y)
107
+ };
108
+
109
+ export interface PiiConfig {
110
+ enabled: boolean;
111
+ whitelistKeys?: string[];
112
+ blacklistKeys?: string[];
113
+ patterns?: PiiReplacement[];
114
+ deep?: boolean;
115
+ includeDefaults?: boolean;
116
+ }
117
+
118
+ // patrones “de fábrica” (del sistema):
119
+ export const DEFAULT_PII_PATTERNS: PiiReplacement[] = [
120
+ {
121
+ pattern: String.raw`[^@\n\r ]+@[^@\n\r ]+`,
122
+ replaceWith: "[EMAIL]",
123
+ flags: "g",
124
+ },
125
+ ];
126
+ // Ver abajo explicación flags
127
+
128
+ // patrones “del cliente/proyecto”:
129
+ export const clientPiiPatterns: PiiReplacement[] = [
130
+ { pattern: String.raw`\b\d{10}\b`, flags: "g", replaceWith: "[EC_DNI]" },
131
+ ];
132
+
133
+ // lista negra de nombres de clave que se siempre se ocultan
134
+ export const redactKeys: string[] = ["password", "secret"];
135
+ // lista blanca de nombres de clave que no se deben ocultar por clave
136
+ export const preserveKeys: string[] = ["city"];
137
+
138
+ // Helpers
139
+ // 1. Filtra valores no válidos: elimina null, undefined y "" (cadena vacía).
140
+ // 2. Elimina duplicados usando Set.
141
+ // 3. Conserva el primero de cada valor repetido (porque Set guarda la primera aparición).
142
+ export function dedupeStrings(arr: Array<string | undefined | null>): string[] {
143
+ return [...new Set(arr.filter((x): x is string => !!x && x.length > 0))];
144
+ }
145
+
146
+ // 1. Construye una clave de identidad por patrón: pattern + "__" + replaceWith + "__" + (flags || "").
147
+ // 2. Inserta cada elemento en un `Map` usando esa clave. Si la clave ya existe, sobrescribe el anterior (es decir, gana el último).
148
+ // 3. Devuelve los valores únicos del `Map`.
149
+ export function dedupePatterns(arr: PiiReplacement[]): PiiReplacement[] {
150
+ const m = new Map<string, PiiReplacement>();
151
+ for (const p of arr)
152
+ m.set(`${p.pattern}__${p.replaceWith}__${p.flags ?? ""}`, p);
153
+ return [...m.values()];
154
+ }
84
155
 
156
+ // Builder unificado
157
+ export function buildPiiConfig(
158
+ opts?: PiiConfig
159
+ ): Required<Omit<PiiConfig, "includeDefaults">> {
160
+ const includeDefaults = opts?.includeDefaults ?? true;
161
+
162
+ const patterns = dedupePatterns([
163
+ ...(includeDefaults ? DEFAULT_PII_PATTERNS : []),
164
+ ...clientPiiPatterns,
165
+ ...(opts?.patterns ?? []),
166
+ ]);
167
+
168
+ const whitelistKeys = dedupeStrings([
169
+ ...(opts?.whitelistKeys ?? []),
170
+ ...preserveKeys,
171
+ ]);
172
+ const blacklistKeys = dedupeStrings([
173
+ ...(opts?.blacklistKeys ?? []),
174
+ ...redactKeys,
175
+ ]);
176
+
177
+ return {
178
+ enabled: !!opts?.enabled,
179
+ whitelistKeys,
180
+ blacklistKeys,
181
+ patterns,
182
+ deep: opts?.deep ?? true,
183
+ };
184
+ }
185
+ ```
85
186
 
86
- # --- PostgreSQL ---
87
- POSTGRES_URL=postgresql://<usuario>:<password>@localhost:5432/my_database
88
- # URL de conexión a la base de datos principal
89
- POSTGRES_DB=my_database
90
- # Credenciales y nombre de la base de datos
187
+ > - **REGEX FLAGS**
91
188
 
92
- # --- FileSystem ---
93
- LOGGER_FS_PATH=./logs/app.log
94
- # Ruta local donde se almacenarán los logs en formato JSONL
189
+ > > - `g` (**global**): Encuentra `todas las coincidencias` en el texto, no solo la primera.
95
190
 
96
- # --- Nivel de logging ---
97
- LOGGER_LEVEL=debug
98
- # Nivel mínimo de logs por entorno (dev: debug, prod: warn)
191
+ ```ts
192
+ const regex = /\d{2}/g;
193
+ "123456".match(regex); // ["12", "34", "56"]
99
194
 
100
- # --- PII (enmascarado de datos sensibles) ---
101
- LOGGER_PII_ENABLED=true
102
- # Activa/desactiva el redactor de datos sensibles
195
+ // /\d{2}/ busca pares de dígitos.
196
+ // "12" en índices 0-1
197
+ // "34" en índices 2-3
198
+ // "56" en índices 4-5
199
+ ```
103
200
 
104
- LOGGER_PII_INCLUDE_DEFAULTS=true
105
- # Incluye los patrones de PII por defecto además de los definidos por el cliente
201
+ > > - `i` (**ignore case**): Ignora mayúsculas/minúsculas.
106
202
 
203
+ ```ts
204
+ // Sin "i" (sensible a mayúsculas/minúsculas)
205
+ const regexCaseSensitive = /secret/;
206
+ "SECRET".match(regexCaseSensitive); // null
207
+ "secret".match(regexCaseSensitive); // ["secret"]
208
+
209
+ // Con "i" (ignora mayúsculas/minúsculas)
210
+ const regexIgnoreCase = /secret/i;
211
+ "SECRET".match(regexIgnoreCase); // ["SECRET"]
212
+ "Secret".match(regexIgnoreCase); // ["Secret"]
213
+ "sEcReT".match(regexIgnoreCase); // ["sEcReT"]
107
214
  ```
108
215
 
109
- ---
216
+ > > - `m` (**multiline**): Permite que `^` y `$` funcionen en cada línea, no solo al inicio/fin del string completo.
110
217
 
111
- ### 🚀 Uso del paquete
218
+ ```ts
219
+ const texto = `uno
220
+ FOO
221
+ tres`;
112
222
 
113
- El cliente (la aplicación que usa `@jmlq/logger`) es responsable de inicializar el logger, ensamblar los **datasources** (`FS`, `Mongo`, `Postgres`) y configurar el **redactor de PII**.
223
+ // Sin "m": ^ solo reconoce el inicio de *todo* el string
224
+ const regexNormal = /^FOO/;
225
+ console.log(texto.match(regexNormal)); // null
114
226
 
115
- #### 1) Configuración de `pii.ts`
227
+ // Con "m": ^ reconoce también el inicio de cada línea
228
+ const regexMultiline = /^FOO/m;
229
+ console.log(texto.match(regexMultiline)); // ["FOO"]
230
+ ```
116
231
 
117
- Se definen los patrones propios de la aplicación para enmascarar datos sensibles.
118
- Estos se combinan con los patrones por defecto del core (`LOGGER_PII_INCLUDE_DEFAULTS=true`).
232
+ > > - `s` (**dotAll**): Permite que `.` coincida también con saltos de línea (`\n`).
119
233
 
120
234
  ```ts
121
- // src/config/logger/pii.ts
235
+ const texto = "a\nb";
122
236
 
123
- import type { IPiiPattern } from "@jmlq/logger";
237
+ // Sin "s": el punto (.) no captura saltos de línea
238
+ const regexNormal = /a.b/;
239
+ texto.match(regexNormal); // null
124
240
 
125
- // Patrones PII propios del cliente
126
- export const clientPiiPatterns: IPiiPattern[] = [
127
- {
128
- // Ejemplo: cédula ecuatoriana (10 dígitos con validaciones)
129
- regex: /\b\d{10}\b/g,
130
- replacement: "[REDACTED_CEDULA]",
131
- },
132
- {
133
- // Ejemplo: token JWT simulado
134
- regex: /\beyJ[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\b/g,
135
- replacement: "[REDACTED_JWT]",
136
- },
137
- ];
241
+ // Con "s": el punto (.) sí captura saltos de línea
242
+ const regexDotAll = /a.b/s;
243
+ texto.match(regexDotAll); // ["a\nb"]
244
+ ```
138
245
 
139
- // Claves a redactar siempre (aunque no hagan match con regex)
140
- export const redactKeys = ["password", "secret", "token"];
246
+ > > - `u` (**unicode**): Habilita soporte Unicode completo en regex (ejemplo, emojis o caracteres fuera del BMP).
141
247
 
142
- // Claves a preservar (no se enmascaran aunque coincidan con regex)
143
- export const preserveKeys = ["city"];
248
+ ```ts
249
+ const regex = /\u{1F600}/u; // 😀
250
+ "😀".match(regex); // ["😀"]
144
251
  ```
145
252
 
146
- #### 2) Inicialización de `logger/index.ts`
253
+ > > - `y` (**sticky**): Solo encuentra coincidencias en la posición exacta del índice actual (lastIndex).
147
254
 
148
- El logger se ensambla en un archivo central, cargando variables de entorno y adaptadores según disponibilidad:
255
+ ```ts
256
+ const regexSticky = /\d{2}/y;
149
257
 
150
- ```tsx
151
- // src/config/logger/index.ts
258
+ regexSticky.lastIndex = 0;
259
+ regexSticky.exec("123456");
260
+ // ["12"]
152
261
 
153
- // Importa las funciones y tipos principales del core del logger
154
- import { createLogger, LogLevel, CompositeDatasource } from "@jmlq/logger";
262
+ regexSticky.lastIndex = 2;
263
+ regexSticky.exec("123456");
264
+ // ["34"]
155
265
 
156
- // Importa los plugins disponibles para persistencia de logs
157
- import { FileSystemDatasource } from "@jmlq/logger-plugin-fs";
158
- import { MongoDatasource } from "@jmlq/logger-plugin-mongo";
159
- import {
160
- connectPostgres,
161
- ensurePostgresSchema,
162
- PostgresDatasource,
163
- } from "@jmlq/logger-plugin-postgresql";
266
+ regexSticky.lastIndex = 4;
267
+ regexSticky.exec("123456");
268
+ // ["56"]
269
+
270
+ regexSticky.lastIndex = 1;
271
+ regexSticky.exec("123456");
272
+ // null (porque en índice 1 hay "2", pero necesita empezar justo ahí y no hay 2 dígitos completos desde esa posición)
273
+ ```
274
+
275
+ Ejemplos:
276
+
277
+ ```ts
278
+ const text = "Usuario: 12345, Otro: 67890";
279
+ const rules: PiiReplacement[] = [
280
+ { pattern: "\\d{5}", replaceWith: "[ID]", flags: "g" },
281
+ ];
282
+ console.log(redact(text, rules));
283
+ // Usuario: [ID], Otro: [ID]
284
+
285
+ // Donde:
286
+ // \d → significa un dígito (0–9).
287
+ // {5} → significa exactamente 5 repeticiones seguidas.
288
+ ```
164
289
 
165
- // Configuración cargada desde variables de entorno (env-var + dotenv)
166
- import { envs } from "../plugins/envs.plugin";
290
+ ```ts
291
+ const text = "Password=1234; PASSWORD=5678; password=9999";
292
+ const rules: PiiReplacement[] = [
293
+ { pattern: "password=\\d+", replaceWith: "password=[REDACTED]", flags: "gi" },
294
+ ];
295
+ console.log(redact(text, rules));
296
+ // password=[REDACTED]; password=[REDACTED]; password=[REDACTED]
297
+
298
+ // Donde:
299
+ // \d → significa un dígito (0–9).
300
+ // + → uno o más dígitos consecutivos.
301
+ ```
167
302
 
168
- // Patrones propios de PII definidos en src/config/logger/pii.ts
169
- import { clientPiiPatterns, redactKeys, preserveKeys } from "./pii";
303
+ ```ts
304
+ const text = `
305
+ linea1: ok
306
+ secret=12345
307
+ linea3: done
308
+ `;
309
+
310
+ const rules: PiiReplacement[] = [
311
+ { pattern: "^secret=.*$", replaceWith: "secret=[REDACTED]", flags: "m" },
312
+ ];
313
+ console.log(redact(text, rules));
314
+ /*
315
+ linea1: ok
316
+ secret=[REDACTED]
317
+ linea3: done
318
+ */
319
+
320
+ // Donde
321
+ // . → cualquier carácter (excepto salto de línea, a menos que uses el flag s).
322
+ // * → cero o más repeticiones del carácter anterior (.).
323
+ // $ → final de la línea o final de la cadena (dependiendo si usas flag m).
324
+ ```
170
325
 
171
- // Utilidades de Node.js para manejo de directorios y archivos
172
- import { mkdirSync, existsSync } from "node:fs";
173
- import { dirname } from "node:path";
326
+ ```ts
327
+ const text = "BEGIN\n12345\nEND";
328
+ const rules: PiiReplacement[] = [
329
+ { pattern: "BEGIN.*END", replaceWith: "[BLOCK REDACTED]", flags: "s" },
330
+ ];
331
+ console.log(redact(text, rules));
332
+ // [BLOCK REDACTED]
174
333
 
175
- // Cliente oficial de MongoDB
176
- import { MongoClient } from "mongodb";
334
+ // Donde
335
+ // . cualquier carácter (excepto salto de línea, a menos que uses el flag s).
336
+ // * → cero o más repeticiones del carácter anterior (.).
337
+ ```
177
338
 
178
- // Se mantiene una referencia global al cliente de MongoDB para cerrarlo en dispose
179
- let mongoClient: MongoClient | null = null;
339
+ ```ts
340
+ const text = "Cliente: 😀 secreto=123";
341
+ const rules: PiiReplacement[] = [
342
+ { pattern: "\\p{Emoji}", replaceWith: "[EMOJI]", flags: "gu" },
343
+ ];
344
+ console.log(redact(text, rules));
345
+ // Cliente: [EMOJI] secreto=123
180
346
 
181
- // Convierte un string (ej. "debug") al enum LogLevel
182
- function toMinLevel(level: string): LogLevel {
183
- switch (level.toLowerCase()) {
347
+ // Donde
348
+ // \p{...} → en regex con flag u (unicode), permite usar propiedades Unicode.
349
+ // \p{Emoji} → coincide con cualquier carácter que esté clasificado en Unicode como un emoji.
350
+ ```
351
+
352
+ ```ts
353
+ const text = "ID=1234 ID=5678";
354
+ const regexRule: PiiReplacement = {
355
+ pattern: "ID=\\d{4}",
356
+ replaceWith: "ID=[REDACTED]",
357
+ flags: "y",
358
+ };
359
+
360
+ const regex = new RegExp(regexRule.pattern, regexRule.flags);
361
+ regex.lastIndex = 0;
362
+ console.log(regex.exec(text)); // ["ID=1234"]
363
+
364
+ regex.lastIndex = 7;
365
+ console.log(regex.exec(text)); // ["ID=5678"]
366
+
367
+ regex.lastIndex = 3;
368
+ console.log(regex.exec(text)); // null (porque no empieza justo ahí)
369
+
370
+ // Donde:
371
+ // \d → significa un dígito (0–9).
372
+ // {4} → significa exactamente 4 repeticiones consecutivas.
373
+ ```
374
+
375
+ ##### 2.2) loglevel.settings.ts (normalización de nivel)
376
+
377
+ ```ts
378
+ // src/infrastructure/logger/settings/loglevel.settings.ts
379
+ import { LogLevel } from "@jmlq/logger";
380
+
381
+ export function toMinLevel(
382
+ level: LogLevel | keyof typeof LogLevel | string
383
+ ): LogLevel {
384
+ if (typeof level === "number") return level as LogLevel;
385
+ switch (String(level || "debug").toLowerCase()) {
184
386
  case "trace":
185
387
  return LogLevel.TRACE;
186
388
  case "debug":
@@ -194,103 +396,365 @@ function toMinLevel(level: string): LogLevel {
194
396
  case "fatal":
195
397
  return LogLevel.FATAL;
196
398
  default:
197
- return LogLevel.INFO; // Valor por defecto
399
+ return LogLevel.DEBUG;
198
400
  }
199
401
  }
402
+ ```
403
+
404
+ #### 3) Adapters (capa infrastructure)
405
+
406
+ Todos `devuelven`/`encapsulan` un `ILogDatasource` para evitar acoplar la app a clases concretas.
407
+
408
+ **NOTA**: Solo se implementan los que se necesiten, por ejemplo:
409
+
410
+ > - Si se necesita generar logs en archivos se crea adapter para `FS`.
411
+ > - Si se necesita guardar logs en una base de datos no relacional se crea adapter para `mongo`.
412
+ > - Si se necesita guardar logs en una base de datos relacional se crea adapter para `postgresql`.
413
+ > - También se pueden combinar.
414
+ > - Se debe implementar al menos un adapter.
415
+
416
+ ##### 3.1) FS (`fs.adapter.ts`)
417
+
418
+ ```ts
419
+ import { createFsDatasource } from "@jmlq/logger-plugin-fs";
420
+ import type { ILogDatasource } from "@jmlq/logger";
421
+
422
+ export interface IFsProps {
423
+ basePath: string;
424
+ fileNamePattern: string;
425
+ rotationPolicy: { by: "none" | "day" | "size"; maxSizeMB?: number };
426
+ }
200
427
 
201
- // Asegura que exista el directorio de logs para el datasource de FileSystem
202
- function ensureDirFor(filePath: string) {
203
- const dir = dirname(filePath);
204
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
428
+ export class FsAdapter {
429
+ private constructor(private readonly ds: ILogDatasource) {}
430
+ static create(opts: IFsProps): FsAdapter | undefined {
431
+ try {
432
+ const ds = createFsDatasource({
433
+ basePath: opts.basePath,
434
+ mkdir: true,
435
+ fileNamePattern: opts.fileNamePattern,
436
+ rotation: opts.rotationPolicy,
437
+ onRotate: (oldP, newP) =>
438
+ console.log("[fs] rotated:", oldP, "->", newP),
439
+ onError: (e) => console.error("[fs] error:", e),
440
+ });
441
+ console.log("[logger] Conectado a FS para logs");
442
+ return new FsAdapter(ds);
443
+ } catch (e: any) {
444
+ console.warn("[logger] FS deshabilitado:", e?.message ?? e);
445
+ }
446
+ }
447
+ get datasource(): ILogDatasource {
448
+ return this.ds;
449
+ }
205
450
  }
451
+ ```
206
452
 
207
- // Inicializa el logger ensamblando los datasources configurados en .env
208
- async function initLogger() {
209
- const datasources = [];
453
+ ##### 3.2) Mongo (`mongo.adapter.ts`)
210
454
 
211
- // --- FileSystem ---
212
- // Si está definido LOGGER_FS_PATH, se usa un datasource local de archivos
213
- if (envs.logger.LOGGER_FS_PATH) {
214
- ensureDirFor(envs.logger.LOGGER_FS_PATH);
215
- datasources.push(
216
- new FileSystemDatasource({ filePath: envs.logger.LOGGER_FS_PATH })
217
- );
455
+ ```ts
456
+ import type { ILogDatasource } from "@jmlq/logger";
457
+ import { createMongoInfra, MongoDatasource } from "@jmlq/logger-plugin-mongo";
458
+
459
+ export interface IMongoProps {
460
+ url: string;
461
+ dbName: string;
462
+ collectionName?: string;
463
+ retentionDays?: number | null;
464
+ }
465
+
466
+ export class MongoAdapter {
467
+ private constructor(private readonly ds: ILogDatasource) {}
468
+ static async create(opts: IMongoProps): Promise<MongoAdapter | undefined> {
469
+ try {
470
+ const infra = await createMongoInfra({
471
+ url: opts.url,
472
+ dbName: opts.dbName,
473
+ collectionName: opts.collectionName ?? "logs",
474
+ createIfMissing: true,
475
+ ensureIndexes: true,
476
+ retentionDays: opts.retentionDays ?? 0,
477
+ extraIndexes: [{ key: { "meta.userId": 1 } }],
478
+ });
479
+ return new MongoAdapter(new MongoDatasource(infra.collection));
480
+ } catch (e: any) {
481
+ console.warn("[logger] Mongo deshabilitado:", e?.message ?? e);
482
+ }
483
+ }
484
+ get datasource(): ILogDatasource {
485
+ return this.ds;
218
486
  }
487
+ }
488
+ ```
489
+
490
+ ##### 3.2) Postgresql (`postgresql.adapter.ts`)
491
+
492
+ ```ts
493
+ import type { ILogDatasource } from "@jmlq/logger";
494
+ import { createPostgresDatasource } from "@jmlq/logger-plugin-postgresql";
495
+
496
+ export interface IPostgresqlProps {
497
+ url: string;
498
+ schema: string;
499
+ table?: string;
500
+ retentionDays?: number | null;
501
+ }
219
502
 
220
- // --- MongoDB ---
221
- // Si están configurados MONGO_URL y MONGO_DB_NAME, se conecta y usa la colección logs
222
- if (envs.logger.MONGO_URL && envs.logger.MONGO_DB_NAME) {
223
- mongoClient = new MongoClient(envs.logger.MONGO_URL);
224
- await mongoClient.connect();
225
- const coll = mongoClient.db(envs.logger.MONGO_DB_NAME).collection("logs");
226
- datasources.push(new MongoDatasource(coll));
503
+ export class PostgresqlAdapter {
504
+ private constructor(private readonly ds: ILogDatasource) {}
505
+ static async create(
506
+ opts: IPostgresqlProps
507
+ ): Promise<PostgresqlAdapter | undefined> {
508
+ try {
509
+ const ps = await createPostgresDatasource({
510
+ connectionString: opts.url,
511
+ schema: opts.schema,
512
+ table: opts.table ?? "logs",
513
+ createIfMissing: true,
514
+ retentionDays: opts.retentionDays ?? 0,
515
+ });
516
+ console.log("[logger] Conectado a PostgreSQL para logs");
517
+ return new PostgresqlAdapter(ps);
518
+ } catch (e: any) {
519
+ console.warn("[logger] Postgres deshabilitado:", e?.message ?? e);
520
+ }
521
+ }
522
+ get datasource(): ILogDatasource {
523
+ return this.ds;
524
+ }
525
+ }
526
+ ```
527
+
528
+ #### 4) `LoggerBootstrap` (orquestador de adapters + PII)
529
+
530
+ ```ts
531
+ // src/infrastructure/logger/bootstrap.ts
532
+ import {
533
+ createLogger,
534
+ CompositeDatasource,
535
+ type ILogDatasource,
536
+ } from "@jmlq/logger";
537
+ import { buildPiiConfig } from "./settings/pii.settings";
538
+ import { toMinLevel } from "./settings/loglevel.settings";
539
+ // NOTA: Opcionales depende de las necesidades del cliente
540
+ import { FsAdapter, type IFsProps } from "./adapters/fs.adapter";
541
+ import { MongoAdapter, type IMongoProps } from "./adapters/mongo.adapter";
542
+ import {
543
+ PostgresqlAdapter,
544
+ type IPostgresqlProps,
545
+ } from "./adapters/postgresql.adapter";
546
+
547
+ export interface LoggerBootstrapOptions {
548
+ minLevel: string | number;
549
+ pii?: {
550
+ enabled?: boolean;
551
+ whitelistKeys?: string[];
552
+ blacklistKeys?: string[];
553
+ patterns?: any[];
554
+ deep?: boolean;
555
+ includeDefaults?: boolean;
556
+ };
557
+ adapters?: {
558
+ // NOTA: Opcionales depende de las necesidades del cliente
559
+ fs?: IFsProps;
560
+ mongo?: IMongoProps;
561
+ postgres?: IPostgresqlProps;
562
+ };
563
+ }
564
+
565
+ export class LoggerBootstrap {
566
+ private constructor(
567
+ private readonly _logger: ReturnType<typeof createLogger>,
568
+ private readonly _ds: ILogDatasource
569
+ ) {}
570
+
571
+ static async create(opts: LoggerBootstrapOptions): Promise<LoggerBootstrap> {
572
+ const dsList: ILogDatasource[] = [];
573
+
574
+ // NOTA: Opcionales depende de las necesidades del cliente
575
+ if (opts.adapters?.fs) {
576
+ const fs = FsAdapter.create(opts.adapters.fs);
577
+ if (fs) dsList.push(fs.datasource);
578
+ }
579
+ if (opts.adapters?.mongo) {
580
+ const mg = await MongoAdapter.create(opts.adapters.mongo);
581
+ if (mg) dsList.push(mg.datasource);
582
+ }
583
+ if (opts.adapters?.postgres) {
584
+ const pg = await PostgresqlAdapter.create(opts.adapters.postgres);
585
+ if (pg) dsList.push(pg.datasource);
586
+ }
587
+ //----
588
+
589
+ if (dsList.length === 0)
590
+ throw new Error("[logger] No hay datasources válidos.");
591
+ const datasource =
592
+ dsList.length === 1 ? dsList[0] : new CompositeDatasource(dsList);
593
+
594
+ const pii = buildPiiConfig({
595
+ enabled: opts.pii?.enabled ?? false,
596
+ includeDefaults: opts.pii?.includeDefaults ?? true,
597
+ whitelistKeys: opts.pii?.whitelistKeys,
598
+ blacklistKeys: opts.pii?.blacklistKeys,
599
+ patterns: opts.pii?.patterns,
600
+ deep: opts.pii?.deep ?? true,
601
+ });
602
+
603
+ const logger = createLogger(datasource, {
604
+ minLevel: toMinLevel(opts.minLevel),
605
+ pii,
606
+ });
607
+ return new LoggerBootstrap(logger, datasource);
227
608
  }
228
609
 
229
- // --- PostgreSQL ---
230
- // Si está configurado POSTGRES_URL, se conecta, asegura la tabla y crea el datasource
231
- if (envs.logger.POSTGRES_URL) {
232
- await connectPostgres(envs.logger.POSTGRES_URL);
233
- await ensurePostgresSchema();
234
- datasources.push(new PostgresDatasource("logs"));
610
+ get logger() {
611
+ return this._logger;
612
+ }
613
+ async flush() {
614
+ const any = this._logger as any;
615
+ if (typeof any.flush === "function") await any.flush();
235
616
  }
617
+ async dispose() {
618
+ const any = this._logger as any;
619
+ if (typeof any.dispose === "function") await any.dispose();
620
+ }
621
+ }
622
+ ```
236
623
 
237
- // Si hay más de un datasource, se compone con CompositeDatasource (fan-out)
238
- const datasource =
239
- datasources.length === 1
240
- ? datasources[0]
241
- : new CompositeDatasource(datasources);
624
+ #### 5) Configuración global del logger (singleton)
242
625
 
243
- // Crea y retorna el logger con nivel mínimo y configuración de PII
244
- return createLogger(datasource, {
245
- minLevel: toMinLevel(envs.logger.LOGGER_LEVEL),
626
+ ```ts
627
+ // src/config/logger/index.ts
628
+ import { LoggerBootstrap } from "../../infrastructure/logger/bootstrap";
629
+ import { envs } from "../../infrastructure/plugins";
630
+
631
+ declare global {
632
+ var __LOGGER_BOOT__: Promise<LoggerBootstrap> | undefined;
633
+ }
634
+
635
+ async function init() {
636
+ return LoggerBootstrap.create({
637
+ minLevel: envs.logger.LOGGER_LEVEL ?? "debug",
246
638
  pii: {
247
639
  enabled: envs.logger.LOGGER_PII_ENABLED,
248
- includeDefaultPatterns: envs.logger.LOGGER_PII_INCLUDE_DEFAULTS,
249
- patterns: clientPiiPatterns,
250
- redactKeys,
251
- preserveKeys,
640
+ includeDefaults: envs.logger.LOGGER_PII_INCLUDE_DEFAULTS,
641
+ deep: true,
642
+ },
643
+ adapters: {
644
+ // NOTA: Opcionales depende de las necesidades del cliente
645
+ fs: envs.logger.LOGGER_FS_PATH
646
+ ? {
647
+ basePath: envs.logger.LOGGER_FS_PATH,
648
+ fileNamePattern: "app-{yyyy}{MM}{dd}.log",
649
+ rotationPolicy: { by: "day" },
650
+ }
651
+ : undefined,
652
+ mongo: envs.logger.MONGO_URL
653
+ ? {
654
+ url: envs.logger.MONGO_URL!,
655
+ dbName: envs.logger.MONGO_DB_NAME!,
656
+ collectionName: envs.logger.MONGO_COLLECTION ?? "logs",
657
+ retentionDays: Number(envs.logger.LOGGER_MONGO_RETENTION_DAYS) || 0,
658
+ }
659
+ : undefined,
660
+ postgres: envs.logger.POSTGRES_URL
661
+ ? {
662
+ url: envs.logger.POSTGRES_URL!,
663
+ schema: envs.logger.POSTGRES_SCHEMA ?? "public",
664
+ table: envs.logger.POSTGRES_TABLE ?? "logs",
665
+ retentionDays: Number(envs.logger.LOGGER_PG_RETENTION_DAYS) || 0,
666
+ }
667
+ : undefined,
668
+ // ---
252
669
  },
253
670
  });
254
671
  }
255
672
 
256
- // Exporta el logger como una Promise porque la inicialización es async
257
- export const loggerReady = initLogger();
673
+ // 1. Es una promesa singleton de LoggerBootstrap
674
+ // 2. usa el operador nullish-coalescing (??) para: Reusar globalThis.__LOGGER_BOOT__ si ya existe. En caso contrario crea y memoriza (= init()) la promesa si no existe aún.
675
+ // 3. Garantiza una sola inicialización global del sistema de logging (adapters, datasources, PII, etc.) aunque el módulo se importe múltiples veces
676
+ export const bootReady: Promise<LoggerBootstrap> =
677
+ globalThis.__LOGGER_BOOT__ ?? (globalThis.__LOGGER_BOOT__ = init());
258
678
 
259
- // --- Funciones de utilidad ---
679
+ // 1. Es una promesa que resuelve directamente al logger
680
+ // 2. Hace un map de la promesa anterior: bootReady.then(b => b.logger).
681
+ export const loggerReady = bootReady.then((b) => b.logger);
260
682
 
261
- // Fuerza un flush() de todos los datasources, útil para apagar servicios con logs pendientes
683
+ // 1. Espera a bootReady y llama boot.flush(), que a su vez pide al logger/datasources que vacíen buffers pendientes (útil antes de apagar el proceso o en tests).
262
684
  export async function flushLogs() {
263
- const logger: any = await loggerReady;
264
- if (typeof logger.flush === "function") await logger.flush();
685
+ const boot = await bootReady;
686
+ await boot.flush();
265
687
  }
266
688
 
267
- // Cierra conexiones abiertas (ej. MongoClient) y libera recursos
689
+ // 1. Espera a bootReady y llama boot.dispose(), que cierra recursos (conexiones a Mongo/Postgres, file handles, etc.)
268
690
  export async function disposeLogs() {
269
- const logger: any = await loggerReady;
270
- if (typeof logger.dispose === "function") await logger.dispose();
271
- if (mongoClient) await mongoClient.close();
691
+ const boot = await bootReady;
692
+ await boot.dispose();
272
693
  }
273
694
  ```
274
695
 
275
- #### 3) Uso desde cualquier aplicación Node.js
276
-
277
- En cualquier parte de la app se puede usar el logger así:
696
+ #### 6) Uso en la aplicación (Express)
278
697
 
279
698
  ```ts
280
- import { loggerReady } from "./config/logger";
281
-
282
- async function main() {
283
- const logger = await loggerReady;
284
-
285
- await logger.info("Aplicación iniciada", { pid: process.pid });
699
+ // src/presentation/server.ts
700
+ import express, { Request, Response, NextFunction } from "express";
701
+ import { loggerReady } from "../config/logger";
702
+
703
+ function attachLogger() {
704
+ const boot = loggerReady;
705
+ return async (req: Request, _res: Response, next: NextFunction) => {
706
+ // @ts-expect-error: extensión ad-hoc
707
+ req.logger = await boot;
708
+ // @ts-expect-error: idem
709
+ req.requestId = (req.headers["x-request-id"] as string) ?? randomUUID();
710
+ next();
711
+ };
712
+ }
286
713
 
287
- await logger.error("Error en proceso", {
288
- password: "123456", // será redactado
289
- city: "Quito", // se preserva
714
+ export function createServer() {
715
+ const app = express();
716
+ app.use(express.json());
717
+ app.use(attachLogger());
718
+
719
+ app.get("/health", (_req, res) =>
720
+ res
721
+ .status(200)
722
+ .json({ ok: true, service: "ml-dev-test", timestamp: Date.now() })
723
+ );
724
+
725
+ app.get("/debug/log-demo", async (req, res) => {
726
+ // @ts-expect-error: logger agregado por attachLogger
727
+ const logger = req.logger;
728
+ await logger.info(
729
+ "Pago con tarjeta 4111 1111 1111 1111 del email demo@correo.com",
730
+ {
731
+ password: "abc123",
732
+ phone: "0987654321",
733
+ city: "Quito",
734
+ }
735
+ );
736
+ res.json({ ok: true });
290
737
  });
291
- }
292
738
 
293
- main();
739
+ // Error handler con logging
740
+ app.use(
741
+ async (err: any, req: Request, res: Response, _next: NextFunction) => {
742
+ const status = err?.statusCode ?? 500;
743
+ // @ts-expect-error
744
+ const logger = req.logger ?? (await loggerReady);
745
+ await logger.error("http_error", {
746
+ message: err?.message,
747
+ stack: err?.stack,
748
+ status,
749
+ });
750
+ res
751
+ .status(status)
752
+ .json({ error: err?.message ?? "Internal Server Error" });
753
+ }
754
+ );
755
+
756
+ return app;
757
+ }
294
758
  ```
295
759
 
296
760
  ### 🔎 Notas importantes
@@ -342,6 +806,10 @@ test("logger redacta PII en FS", async () => {
342
806
 
343
807
  ---
344
808
 
809
+ ### [LEER MAS...](./ARQUITECTURA.md)
810
+
811
+ ---
812
+
345
813
  ## 📄 Licencia
346
814
 
347
815
  MIT © Mauricio Lahuasi
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@jmlq/logger",
3
- "version": "0.1.0-alpha.5",
3
+ "version": "0.1.0-alpha.7",
4
4
  "author": "MLahuasi",
5
+ "license": "MIT",
5
6
  "main": "dist/index.js",
6
7
  "types": "dist/index.d.ts",
7
8
  "files": [