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

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 +464 -185
  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,75 +34,33 @@ npm i pg@^8.16.3
34
34
 
35
35
  ---
36
36
 
37
- ## 🧱 Estructura del paquete
38
-
39
- ### 📝 Resumen rápido
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).
52
-
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.
58
-
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`).
62
-
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.
66
-
67
- > - **`src/index.ts`** — Barrel.
68
- > > - Re-exporta lo público: `createLogger`, `LogLevel`, tipos/contratos y `CompositeDatasource`.
69
-
70
- ### [LEER MAS](./ARQUITECTURA.md)
71
-
72
- ---
73
-
74
37
  ## 🧩 Configuración
75
38
 
76
39
  ### 🔐 Variables de Entorno (.env)
77
40
 
78
41
  ```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
84
-
85
-
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
91
-
92
- # --- FileSystem ---
93
- LOGGER_FS_PATH=./logs/app.log
94
- # Ruta local donde se almacenarán los logs en formato JSONL
95
-
96
- # --- Nivel de logging ---
97
- LOGGER_LEVEL=debug
98
- # Nivel mínimo de logs por entorno (dev: debug, prod: warn)
99
-
100
- # --- PII (enmascarado de datos sensibles) ---
101
- LOGGER_PII_ENABLED=true
102
- # Activa/desactiva el redactor de datos sensibles
103
-
104
- LOGGER_PII_INCLUDE_DEFAULTS=true
105
- # Incluye los patrones de PII por defecto además de los definidos por el cliente
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
106
64
 
107
65
  ```
108
66
 
@@ -110,77 +68,132 @@ LOGGER_PII_INCLUDE_DEFAULTS=true
110
68
 
111
69
  ### 🚀 Uso del paquete
112
70
 
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**.
71
+ #### 1) Estructura recomendada
114
72
 
115
- #### 1) Configuración de `pii.ts`
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
116
93
 
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`).
94
+ ```
119
95
 
120
- ```ts
121
- // src/config/logger/pii.ts
96
+ #### 2) Settings
122
97
 
123
- import type { IPiiPattern } from "@jmlq/logger";
98
+ #### 2.1) pii.settings.ts (patrones PII + merge)
124
99
 
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
- },
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;
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[] = [
132
120
  {
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]",
121
+ pattern: String.raw`[^@\n\r ]+@[^@\n\r ]+`,
122
+ replaceWith: "[EMAIL]",
123
+ flags: "g",
136
124
  },
137
125
  ];
138
126
 
139
- // Claves a redactar siempre (aunque no hagan match con regex)
140
- export const redactKeys = ["password", "secret", "token"];
141
-
142
- // Claves a preservar (no se enmascaran aunque coincidan con regex)
143
- export const preserveKeys = ["city"];
144
- ```
145
-
146
- #### 2) Inicialización de `logger/index.ts`
147
-
148
- El logger se ensambla en un archivo central, cargando variables de entorno y adaptadores según disponibilidad:
149
-
150
- ```tsx
151
- // src/config/logger/index.ts
152
-
153
- // Importa las funciones y tipos principales del core del logger
154
- import { createLogger, LogLevel, CompositeDatasource } from "@jmlq/logger";
155
-
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";
164
-
165
- // Configuración cargada desde variables de entorno (env-var + dotenv)
166
- import { envs } from "../plugins/envs.plugin";
127
+ // patrones “del cliente/proyecto”:
128
+ export const clientPiiPatterns: PiiReplacement[] = [
129
+ { pattern: String.raw`\b\d{10}\b`, flags: "g", replaceWith: "[EC_DNI]" },
130
+ ];
167
131
 
168
- // Patrones propios de PII definidos en src/config/logger/pii.ts
169
- import { clientPiiPatterns, redactKeys, preserveKeys } from "./pii";
132
+ // lista negra de nombres de clave que se siempre se ocultan
133
+ export const redactKeys: string[] = ["password", "secret"];
134
+ // lista blanca de nombres de clave que no se deben ocultar por clave
135
+ export const preserveKeys: string[] = ["city"];
136
+
137
+ // Helpers
138
+ // 1. Filtra valores no válidos: elimina null, undefined y "" (cadena vacía).
139
+ // 2. Elimina duplicados usando Set.
140
+ // 3. Conserva el primero de cada valor repetido (porque Set guarda la primera aparición).
141
+ export function dedupeStrings(arr: Array<string | undefined | null>): string[] {
142
+ return [...new Set(arr.filter((x): x is string => !!x && x.length > 0))];
143
+ }
170
144
 
171
- // Utilidades de Node.js para manejo de directorios y archivos
172
- import { mkdirSync, existsSync } from "node:fs";
173
- import { dirname } from "node:path";
145
+ // 1. Construye una clave de identidad por patrón: pattern + "__" + replaceWith + "__" + (flags || "").
146
+ // 2. Inserta cada elemento en un `Map` usando esa clave. Si la clave ya existe, sobrescribe el anterior (es decir, gana el último).
147
+ // 3. Devuelve los valores únicos del `Map`.
148
+ export function dedupePatterns(arr: PiiReplacement[]): PiiReplacement[] {
149
+ const m = new Map<string, PiiReplacement>();
150
+ for (const p of arr)
151
+ m.set(`${p.pattern}__${p.replaceWith}__${p.flags ?? ""}`, p);
152
+ return [...m.values()];
153
+ }
174
154
 
175
- // Cliente oficial de MongoDB
176
- import { MongoClient } from "mongodb";
155
+ // Builder unificado
156
+ export function buildPiiConfig(
157
+ opts?: PiiConfig
158
+ ): Required<Omit<PiiConfig, "includeDefaults">> {
159
+ const includeDefaults = opts?.includeDefaults ?? true;
160
+
161
+ const patterns = dedupePatterns([
162
+ ...(includeDefaults ? DEFAULT_PII_PATTERNS : []),
163
+ ...clientPiiPatterns,
164
+ ...(opts?.patterns ?? []),
165
+ ]);
166
+
167
+ const whitelistKeys = dedupeStrings([
168
+ ...(opts?.whitelistKeys ?? []),
169
+ ...preserveKeys,
170
+ ]);
171
+ const blacklistKeys = dedupeStrings([
172
+ ...(opts?.blacklistKeys ?? []),
173
+ ...redactKeys,
174
+ ]);
175
+
176
+ return {
177
+ enabled: !!opts?.enabled,
178
+ whitelistKeys,
179
+ blacklistKeys,
180
+ patterns,
181
+ deep: opts?.deep ?? true,
182
+ };
183
+ }
184
+ ```
177
185
 
178
- // Se mantiene una referencia global al cliente de MongoDB para cerrarlo en dispose
179
- let mongoClient: MongoClient | null = null;
186
+ ##### 2.2) loglevel.settings.ts (normalización de nivel)
180
187
 
181
- // Convierte un string (ej. "debug") al enum LogLevel
182
- function toMinLevel(level: string): LogLevel {
183
- switch (level.toLowerCase()) {
188
+ ```ts
189
+ // src/infrastructure/logger/settings/loglevel.settings.ts
190
+ import { LogLevel } from "@jmlq/logger";
191
+
192
+ export function toMinLevel(
193
+ level: LogLevel | keyof typeof LogLevel | string
194
+ ): LogLevel {
195
+ if (typeof level === "number") return level as LogLevel;
196
+ switch (String(level || "debug").toLowerCase()) {
184
197
  case "trace":
185
198
  return LogLevel.TRACE;
186
199
  case "debug":
@@ -194,103 +207,365 @@ function toMinLevel(level: string): LogLevel {
194
207
  case "fatal":
195
208
  return LogLevel.FATAL;
196
209
  default:
197
- return LogLevel.INFO; // Valor por defecto
210
+ return LogLevel.DEBUG;
198
211
  }
199
212
  }
213
+ ```
200
214
 
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 });
215
+ #### 3) Adapters (capa infrastructure)
216
+
217
+ Todos `devuelven`/`encapsulan` un `ILogDatasource` para evitar acoplar la app a clases concretas.
218
+
219
+ **NOTA**: Solo se implementan los que se necesiten, por ejemplo:
220
+
221
+ > - Si se necesita generar logs en archivos se crea adapter para `FS`.
222
+ > - Si se necesita guardar logs en una base de datos no relacional se crea adapter para `mongo`.
223
+ > - Si se necesita guardar logs en una base de datos relacional se crea adapter para `postgresql`.
224
+ > - También se pueden combinar.
225
+ > - Se debe implementar al menos un adapter.
226
+
227
+ ##### 3.1) FS (`fs.adapter.ts`)
228
+
229
+ ```ts
230
+ import { createFsDatasource } from "@jmlq/logger-plugin-fs";
231
+ import type { ILogDatasource } from "@jmlq/logger";
232
+
233
+ export interface IFsProps {
234
+ basePath: string;
235
+ fileNamePattern: string;
236
+ rotationPolicy: { by: "none" | "day" | "size"; maxSizeMB?: number };
205
237
  }
206
238
 
207
- // Inicializa el logger ensamblando los datasources configurados en .env
208
- async function initLogger() {
209
- const datasources = [];
239
+ export class FsAdapter {
240
+ private constructor(private readonly ds: ILogDatasource) {}
241
+ static create(opts: IFsProps): FsAdapter | undefined {
242
+ try {
243
+ const ds = createFsDatasource({
244
+ basePath: opts.basePath,
245
+ mkdir: true,
246
+ fileNamePattern: opts.fileNamePattern,
247
+ rotation: opts.rotationPolicy,
248
+ onRotate: (oldP, newP) =>
249
+ console.log("[fs] rotated:", oldP, "->", newP),
250
+ onError: (e) => console.error("[fs] error:", e),
251
+ });
252
+ console.log("[logger] Conectado a FS para logs");
253
+ return new FsAdapter(ds);
254
+ } catch (e: any) {
255
+ console.warn("[logger] FS deshabilitado:", e?.message ?? e);
256
+ }
257
+ }
258
+ get datasource(): ILogDatasource {
259
+ return this.ds;
260
+ }
261
+ }
262
+ ```
210
263
 
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
- );
264
+ ##### 3.2) Mongo (`mongo.adapter.ts`)
265
+
266
+ ```ts
267
+ import type { ILogDatasource } from "@jmlq/logger";
268
+ import { createMongoInfra, MongoDatasource } from "@jmlq/logger-plugin-mongo";
269
+
270
+ export interface IMongoProps {
271
+ url: string;
272
+ dbName: string;
273
+ collectionName?: string;
274
+ retentionDays?: number | null;
275
+ }
276
+
277
+ export class MongoAdapter {
278
+ private constructor(private readonly ds: ILogDatasource) {}
279
+ static async create(opts: IMongoProps): Promise<MongoAdapter | undefined> {
280
+ try {
281
+ const infra = await createMongoInfra({
282
+ url: opts.url,
283
+ dbName: opts.dbName,
284
+ collectionName: opts.collectionName ?? "logs",
285
+ createIfMissing: true,
286
+ ensureIndexes: true,
287
+ retentionDays: opts.retentionDays ?? 0,
288
+ extraIndexes: [{ key: { "meta.userId": 1 } }],
289
+ });
290
+ return new MongoAdapter(new MongoDatasource(infra.collection));
291
+ } catch (e: any) {
292
+ console.warn("[logger] Mongo deshabilitado:", e?.message ?? e);
293
+ }
294
+ }
295
+ get datasource(): ILogDatasource {
296
+ return this.ds;
297
+ }
298
+ }
299
+ ```
300
+
301
+ ##### 3.2) Postgresql (`postgresql.adapter.ts`)
302
+
303
+ ```ts
304
+ import type { ILogDatasource } from "@jmlq/logger";
305
+ import { createPostgresDatasource } from "@jmlq/logger-plugin-postgresql";
306
+
307
+ export interface IPostgresqlProps {
308
+ url: string;
309
+ schema: string;
310
+ table?: string;
311
+ retentionDays?: number | null;
312
+ }
313
+
314
+ export class PostgresqlAdapter {
315
+ private constructor(private readonly ds: ILogDatasource) {}
316
+ static async create(
317
+ opts: IPostgresqlProps
318
+ ): Promise<PostgresqlAdapter | undefined> {
319
+ try {
320
+ const ps = await createPostgresDatasource({
321
+ connectionString: opts.url,
322
+ schema: opts.schema,
323
+ table: opts.table ?? "logs",
324
+ createIfMissing: true,
325
+ retentionDays: opts.retentionDays ?? 0,
326
+ });
327
+ console.log("[logger] Conectado a PostgreSQL para logs");
328
+ return new PostgresqlAdapter(ps);
329
+ } catch (e: any) {
330
+ console.warn("[logger] Postgres deshabilitado:", e?.message ?? e);
331
+ }
332
+ }
333
+ get datasource(): ILogDatasource {
334
+ return this.ds;
218
335
  }
336
+ }
337
+ ```
338
+
339
+ #### 4) `LoggerBootstrap` (orquestador de adapters + PII)
340
+
341
+ ```ts
342
+ // src/infrastructure/logger/bootstrap.ts
343
+ import {
344
+ createLogger,
345
+ CompositeDatasource,
346
+ type ILogDatasource,
347
+ } from "@jmlq/logger";
348
+ import { buildPiiConfig } from "./settings/pii.settings";
349
+ import { toMinLevel } from "./settings/loglevel.settings";
350
+ // NOTA: Opcionales depende de las necesidades del cliente
351
+ import { FsAdapter, type IFsProps } from "./adapters/fs.adapter";
352
+ import { MongoAdapter, type IMongoProps } from "./adapters/mongo.adapter";
353
+ import {
354
+ PostgresqlAdapter,
355
+ type IPostgresqlProps,
356
+ } from "./adapters/postgresql.adapter";
357
+
358
+ export interface LoggerBootstrapOptions {
359
+ minLevel: string | number;
360
+ pii?: {
361
+ enabled?: boolean;
362
+ whitelistKeys?: string[];
363
+ blacklistKeys?: string[];
364
+ patterns?: any[];
365
+ deep?: boolean;
366
+ includeDefaults?: boolean;
367
+ };
368
+ adapters?: {
369
+ // NOTA: Opcionales depende de las necesidades del cliente
370
+ fs?: IFsProps;
371
+ mongo?: IMongoProps;
372
+ postgres?: IPostgresqlProps;
373
+ };
374
+ }
219
375
 
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));
376
+ export class LoggerBootstrap {
377
+ private constructor(
378
+ private readonly _logger: ReturnType<typeof createLogger>,
379
+ private readonly _ds: ILogDatasource
380
+ ) {}
381
+
382
+ static async create(opts: LoggerBootstrapOptions): Promise<LoggerBootstrap> {
383
+ const dsList: ILogDatasource[] = [];
384
+
385
+ // NOTA: Opcionales depende de las necesidades del cliente
386
+ if (opts.adapters?.fs) {
387
+ const fs = FsAdapter.create(opts.adapters.fs);
388
+ if (fs) dsList.push(fs.datasource);
389
+ }
390
+ if (opts.adapters?.mongo) {
391
+ const mg = await MongoAdapter.create(opts.adapters.mongo);
392
+ if (mg) dsList.push(mg.datasource);
393
+ }
394
+ if (opts.adapters?.postgres) {
395
+ const pg = await PostgresqlAdapter.create(opts.adapters.postgres);
396
+ if (pg) dsList.push(pg.datasource);
397
+ }
398
+ //----
399
+
400
+ if (dsList.length === 0)
401
+ throw new Error("[logger] No hay datasources válidos.");
402
+ const datasource =
403
+ dsList.length === 1 ? dsList[0] : new CompositeDatasource(dsList);
404
+
405
+ const pii = buildPiiConfig({
406
+ enabled: opts.pii?.enabled ?? false,
407
+ includeDefaults: opts.pii?.includeDefaults ?? true,
408
+ whitelistKeys: opts.pii?.whitelistKeys,
409
+ blacklistKeys: opts.pii?.blacklistKeys,
410
+ patterns: opts.pii?.patterns,
411
+ deep: opts.pii?.deep ?? true,
412
+ });
413
+
414
+ const logger = createLogger(datasource, {
415
+ minLevel: toMinLevel(opts.minLevel),
416
+ pii,
417
+ });
418
+ return new LoggerBootstrap(logger, datasource);
227
419
  }
228
420
 
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"));
421
+ get logger() {
422
+ return this._logger;
423
+ }
424
+ async flush() {
425
+ const any = this._logger as any;
426
+ if (typeof any.flush === "function") await any.flush();
427
+ }
428
+ async dispose() {
429
+ const any = this._logger as any;
430
+ if (typeof any.dispose === "function") await any.dispose();
235
431
  }
432
+ }
433
+ ```
434
+
435
+ #### 5) Configuración global del logger (singleton)
436
+
437
+ ```ts
438
+ // src/config/logger/index.ts
439
+ import { LoggerBootstrap } from "../../infrastructure/logger/bootstrap";
440
+ import { envs } from "../../infrastructure/plugins";
236
441
 
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);
442
+ declare global {
443
+ var __LOGGER_BOOT__: Promise<LoggerBootstrap> | undefined;
444
+ }
242
445
 
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),
446
+ async function init() {
447
+ return LoggerBootstrap.create({
448
+ minLevel: envs.logger.LOGGER_LEVEL ?? "debug",
246
449
  pii: {
247
450
  enabled: envs.logger.LOGGER_PII_ENABLED,
248
- includeDefaultPatterns: envs.logger.LOGGER_PII_INCLUDE_DEFAULTS,
249
- patterns: clientPiiPatterns,
250
- redactKeys,
251
- preserveKeys,
451
+ includeDefaults: envs.logger.LOGGER_PII_INCLUDE_DEFAULTS,
452
+ deep: true,
453
+ },
454
+ adapters: {
455
+ // NOTA: Opcionales depende de las necesidades del cliente
456
+ fs: envs.logger.LOGGER_FS_PATH
457
+ ? {
458
+ basePath: envs.logger.LOGGER_FS_PATH,
459
+ fileNamePattern: "app-{yyyy}{MM}{dd}.log",
460
+ rotationPolicy: { by: "day" },
461
+ }
462
+ : undefined,
463
+ mongo: envs.logger.MONGO_URL
464
+ ? {
465
+ url: envs.logger.MONGO_URL!,
466
+ dbName: envs.logger.MONGO_DB_NAME!,
467
+ collectionName: envs.logger.MONGO_COLLECTION ?? "logs",
468
+ retentionDays: Number(envs.logger.LOGGER_MONGO_RETENTION_DAYS) || 0,
469
+ }
470
+ : undefined,
471
+ postgres: envs.logger.POSTGRES_URL
472
+ ? {
473
+ url: envs.logger.POSTGRES_URL!,
474
+ schema: envs.logger.POSTGRES_SCHEMA ?? "public",
475
+ table: envs.logger.POSTGRES_TABLE ?? "logs",
476
+ retentionDays: Number(envs.logger.LOGGER_PG_RETENTION_DAYS) || 0,
477
+ }
478
+ : undefined,
479
+ // ---
252
480
  },
253
481
  });
254
482
  }
255
483
 
256
- // Exporta el logger como una Promise porque la inicialización es async
257
- export const loggerReady = initLogger();
484
+ // 1. Es una promesa singleton de LoggerBootstrap
485
+ // 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.
486
+ // 3. Garantiza una sola inicialización global del sistema de logging (adapters, datasources, PII, etc.) aunque el módulo se importe múltiples veces
487
+ export const bootReady: Promise<LoggerBootstrap> =
488
+ globalThis.__LOGGER_BOOT__ ?? (globalThis.__LOGGER_BOOT__ = init());
258
489
 
259
- // --- Funciones de utilidad ---
490
+ // 1. Es una promesa que resuelve directamente al logger
491
+ // 2. Hace un map de la promesa anterior: bootReady.then(b => b.logger).
492
+ export const loggerReady = bootReady.then((b) => b.logger);
260
493
 
261
- // Fuerza un flush() de todos los datasources, útil para apagar servicios con logs pendientes
494
+ // 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
495
  export async function flushLogs() {
263
- const logger: any = await loggerReady;
264
- if (typeof logger.flush === "function") await logger.flush();
496
+ const boot = await bootReady;
497
+ await boot.flush();
265
498
  }
266
499
 
267
- // Cierra conexiones abiertas (ej. MongoClient) y libera recursos
500
+ // 1. Espera a bootReady y llama boot.dispose(), que cierra recursos (conexiones a Mongo/Postgres, file handles, etc.)
268
501
  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();
502
+ const boot = await bootReady;
503
+ await boot.dispose();
272
504
  }
273
505
  ```
274
506
 
275
- #### 3) Uso desde cualquier aplicación Node.js
276
-
277
- En cualquier parte de la app se puede usar el logger así:
507
+ #### 6) Uso en la aplicación (Express)
278
508
 
279
509
  ```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 });
510
+ // src/presentation/server.ts
511
+ import express, { Request, Response, NextFunction } from "express";
512
+ import { loggerReady } from "../config/logger";
513
+
514
+ function attachLogger() {
515
+ const boot = loggerReady;
516
+ return async (req: Request, _res: Response, next: NextFunction) => {
517
+ // @ts-expect-error: extensión ad-hoc
518
+ req.logger = await boot;
519
+ // @ts-expect-error: idem
520
+ req.requestId = (req.headers["x-request-id"] as string) ?? randomUUID();
521
+ next();
522
+ };
523
+ }
286
524
 
287
- await logger.error("Error en proceso", {
288
- password: "123456", // será redactado
289
- city: "Quito", // se preserva
525
+ export function createServer() {
526
+ const app = express();
527
+ app.use(express.json());
528
+ app.use(attachLogger());
529
+
530
+ app.get("/health", (_req, res) =>
531
+ res
532
+ .status(200)
533
+ .json({ ok: true, service: "ml-dev-test", timestamp: Date.now() })
534
+ );
535
+
536
+ app.get("/debug/log-demo", async (req, res) => {
537
+ // @ts-expect-error: logger agregado por attachLogger
538
+ const logger = req.logger;
539
+ await logger.info(
540
+ "Pago con tarjeta 4111 1111 1111 1111 del email demo@correo.com",
541
+ {
542
+ password: "abc123",
543
+ phone: "0987654321",
544
+ city: "Quito",
545
+ }
546
+ );
547
+ res.json({ ok: true });
290
548
  });
291
- }
292
549
 
293
- main();
550
+ // Error handler con logging
551
+ app.use(
552
+ async (err: any, req: Request, res: Response, _next: NextFunction) => {
553
+ const status = err?.statusCode ?? 500;
554
+ // @ts-expect-error
555
+ const logger = req.logger ?? (await loggerReady);
556
+ await logger.error("http_error", {
557
+ message: err?.message,
558
+ stack: err?.stack,
559
+ status,
560
+ });
561
+ res
562
+ .status(status)
563
+ .json({ error: err?.message ?? "Internal Server Error" });
564
+ }
565
+ );
566
+
567
+ return app;
568
+ }
294
569
  ```
295
570
 
296
571
  ### 🔎 Notas importantes
@@ -342,6 +617,10 @@ test("logger redacta PII en FS", async () => {
342
617
 
343
618
  ---
344
619
 
620
+ ### [LEER MAS...](./ARQUITECTURA.md)
621
+
622
+ ---
623
+
345
624
  ## 📄 Licencia
346
625
 
347
626
  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.6",
4
4
  "author": "MLahuasi",
5
+ "license": "MIT",
5
6
  "main": "dist/index.js",
6
7
  "types": "dist/index.d.ts",
7
8
  "files": [