@jmlq/logger 0.1.0-alpha.20 → 0.1.0-alpha.21

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.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  Sistema de logging modular y extensible con **Arquitectura Limpia**. Soporta múltiples destinos (archivos, MongoDB, PostgreSQL) y enmascarado automático de datos sensibles (PII).
4
4
 
5
+ Se encarga de:
6
+
7
+ - Persistencia de logs en múltiples datasources
8
+ - Enmascarado automático de datos sensibles (PII)
9
+ - Filtros y búsquedas avanzadas
10
+ - Arquitectura extensible y testeable
11
+
12
+ ## Componentes principales expuestos
13
+
14
+ ### Factory
15
+
16
+ - **[`createLogger`](src/application/factory/logger.factory.ts)**
17
+ Factory de alto nivel que construye un `ILogger` compatible con `@jmlq/logger`, resolviendo internamente:
18
+
19
+ - Configuración de datasources múltiples (fan-out)
20
+ - Inicialización del redactor PII
21
+ - Composición de casos de uso
22
+ - Nivel mínimo de logging
23
+
24
+ > Es el **único punto recomendado de entrada** para consumidores del paquete.
25
+
26
+ ---
27
+
28
+ ### Tipos de configuración
29
+
30
+ - **[`ILoggerFactoryConfig`](src/application/types/logger-factory-config.type.ts)**
31
+ Define la configuración necesaria:
32
+ - `datasources`: Uno o múltiples datasources donde persistir logs
33
+ - `minLevel`: Nivel mínimo de logging (por defecto INFO)
34
+ - `redactor`: Instancia personalizada de redactor PII
35
+ - `redactorOptions`: Configuración para redactor interno
36
+
37
+ ---
38
+
39
+ ## Contratos (Types / Ports)
40
+
41
+ Estos tipos existen para **desacoplar el dominio y la aplicación**.
42
+ Son útiles para testing, extensiones avanzadas o integraciones personalizadas.
43
+
44
+ - **[`ILogger`](src/domain/ports/logger.port.ts)**: Interfaz principal del logger con métodos por nivel
45
+ - **[`ILogDatasource`](src/domain/ports/log-datasource.port.ts)**: Contrato para implementar datasources personalizados
46
+ - **[`LogLevel`](src/domain/value-objects/log-level.vo.ts)**: Enumeración de niveles de logging
47
+ - **[`LogFilterRequest`](src/domain/request/log-filter.request.ts)**: Filtros para consultas de logs
48
+ - **[`ILogResponse`](src/domain/response/log.response.ts)**: Formato de respuesta de logs consultados
49
+ - **[`PiiOptions`](src/domain/model/pii-options.model.ts)**: Configuración para enmascarado PII
50
+ - **[`LogEntry`](src/domain/model/log-entry.model.ts)**: Modelo de entrada de log
51
+ - **[`PiiRedactor`](src/domain/services/pii-redactor.service.ts)**: Servicio de enmascarado PII
52
+
53
+ ---
54
+
5
55
  ## 📦 Instalación
6
56
 
7
57
  ```bash
@@ -14,18 +64,406 @@ npm install @jmlq/logger-plugin-mongo # Para MongoDB
14
64
  npm install @jmlq/logger-plugin-postgresql # Para PostgreSQL
15
65
  ```
16
66
 
17
- ## 🚀 Uso Básico
67
+ ---
68
+
69
+ ## Configuración @jmlq/logger
18
70
 
19
- Con esta configuración se busca:
71
+ ### Variables de entorno
72
+
73
+ Ver cómo se configuran los adapters de `@jmlq/logger-plugin-fs`, `@jmlq/logger-plugin-mongo` y `@jmlq/logger-plugin-postgresql`.
74
+
75
+ | Plugin | Descripción | NPM |
76
+ | -------------------------------- | -------------------------- | --------------------------------------------------------------- |
77
+ | `@jmlq/logger-plugin-fs` | Persistencia en archivos | [npm](https://npmjs.com/package/@jmlq/logger-plugin-fs) |
78
+ | `@jmlq/logger-plugin-mongo` | Persistencia en MongoDB | [npm](https://npmjs.com/package/@jmlq/logger-plugin-mongo) |
79
+ | `@jmlq/logger-plugin-postgresql` | Persistencia en PostgreSQL | [npm](https://npmjs.com/package/@jmlq/logger-plugin-postgresql) |
80
+
81
+ El paquete no gestiona variables de entorno internamente, pero puedes implementar tu propia capa de configuración. Estas son las variables de entorno que se usan para configurar todos los plugins.
82
+
83
+ ```ini
84
+ # Settings @jmlq/logger
85
+ # LOGGER Plugins Configuration
86
+ # Nivel mínimo de logs
87
+ # Valores: TRACE, DEBUG, INFO, WARN, ERROR, FATAL
88
+ LOG_LEVEL=INFO
89
+ # PII Protection
90
+ LOGGER_PII_ENABLED=true
91
+ LOGGER_PII_INCLUDE_DEFAULTS=false
92
+ # FS Plugin Settings
93
+ LOGGER_FS_PATH=./logs/
94
+ # MongoDB Plugin Settings
95
+ LOGGER_MONGO_DB_URL=mongodb://root:secret@localhost:27018/?authSource=admin
96
+ LOGGER_MONGO_DB_NAME=logger_example_db
97
+ LOGGER_MONGO_COLLECTION_NAME=logs_datasource_factory_example
98
+ # PostgreSQL Plugin Settings
99
+ LOGGER_PG_CONNECTION_STRING=postgres://admin:secret@localhost:5436/logger_tests
100
+ LOGGER_PG_TABLE_NAME=logs_example_factory
101
+ LOGGER_PG_SCHEMA=public
102
+ ```
20
103
 
21
- - Tener un **logger centralizado** basado en `@jmlq/logger`.
22
- - Enviar logs a uno o varios de estos destinos (**opcionales** y combinables):
23
- - Sistema de archivos (`@jmlq/logger-plugin-fs`)
24
- - MongoDB (`@jmlq/logger-plugin-mongo`)
25
- - PostgreSQL (`@jmlq/logger-plugin-postgresql`)
26
- - Integrar el logger con:
27
- - Express (por request) mediante un middleware (`req.logger` + `req.requestId`).
28
- - El ciclo de vida de la app: arranque, apagado limpio y fallos no controlados.
104
+ ### Interfaz de configuración del paquete
105
+
106
+ ```ts
107
+ // Path Ejemplo: src/infrastructure/logger/core/logger.bootstrap.options.ts
108
+
109
+ import { LogLevel } from "@jmlq/logger";
110
+ import { IFilesystemDatasourceOptions } from "@jmlq/logger-plugin-fs";
111
+ import { IMongoDatasourceOptions } from "@jmlq/logger-plugin-mongo";
112
+ import { IPostgresDatasourceOptions } from "@jmlq/logger-plugin-postgresql";
113
+
114
+ export interface LoggerBootstrapOptions {
115
+ minLevel: LogLevel;
116
+ pii?: {
117
+ enabled?: boolean;
118
+ whitelistKeys?: string[];
119
+ blacklistKeys?: string[];
120
+ patterns?: any[];
121
+ deep?: boolean;
122
+ includeDefaults?: boolean;
123
+ };
124
+ adapters?: {
125
+ // NOTA: Opcional, depende de las necesidades del cliente
126
+ fs?: IFilesystemDatasourceOptions;
127
+ mongo?: IMongoDatasourceOptions;
128
+ pg?: IPostgresDatasourceOptions;
129
+ };
130
+ }
131
+ ```
132
+
133
+ ### Implementación del Logger
134
+
135
+ ```ts
136
+ // Path Ejemplo: src/infrastructure/logger/core/logger.bootstrap.ts
137
+
138
+ import { type ILogDatasource, createLogger } from "@jmlq/logger";
139
+ import { FsAdapter } from "../fs/fs.adapter";
140
+ import { MongoAdapter } from "../mongo/mongo.adapter";
141
+ import { PgAdapter } from "../pg/pg.adapter";
142
+ import { LoggerBootstrapOptions } from "./logger.bootstrap.options";
143
+
144
+ export class LoggerBootstrap {
145
+ private constructor(
146
+ private readonly _logger: ReturnType<typeof createLogger>
147
+ ) {}
148
+
149
+ static async create(opts: LoggerBootstrapOptions): Promise<LoggerBootstrap> {
150
+ const dsList: ILogDatasource[] = [];
151
+
152
+ // NOTA: Opcional, depende de las necesidades del cliente
153
+ if (opts.adapters?.fs) {
154
+ const fs = FsAdapter.create(opts.adapters.fs);
155
+ if (fs) dsList.push(fs.datasource);
156
+ }
157
+ if (opts.adapters?.mongo) {
158
+ const mongo = await MongoAdapter.create(opts.adapters.mongo);
159
+ if (mongo) dsList.push(mongo.datasource);
160
+ }
161
+ if (opts.adapters?.pg) {
162
+ const pg = await PgAdapter.create(opts.adapters.pg);
163
+ if (pg) dsList.push(pg.datasource);
164
+ }
165
+ //----
166
+
167
+ if (dsList.length === 0)
168
+ throw new Error("[logger] No hay datasources válidos.");
169
+
170
+ const logger = createLogger({
171
+ datasources: dsList,
172
+ minLevel: opts.minLevel,
173
+ redactorOptions: {
174
+ enabled: opts.pii?.enabled ?? false,
175
+ deep: opts.pii?.deep ?? true,
176
+ patterns: opts.pii?.patterns ?? [],
177
+ },
178
+ });
179
+
180
+ console.log("✅ Logger creado correctamente.\n");
181
+
182
+ return new LoggerBootstrap(logger);
183
+ }
184
+
185
+ get logger() {
186
+ return this._logger;
187
+ }
188
+
189
+ // Limpia buffers pendientes antes del cierre
190
+ async flush() {
191
+ const logs = this._logger as any;
192
+ if (typeof logs.flush === "function") await logs.flush();
193
+ }
194
+
195
+ // Cierra recursos (conexiones, handles, etc.)
196
+ async dispose() {
197
+ const logs = this._logger as any;
198
+ if (typeof logs.dispose === "function") await logs.dispose();
199
+ }
200
+ }
201
+ ```
202
+
203
+ ### Inicializador del Logger
204
+
205
+ ```ts
206
+ // Path Ejemplo: src/infrastructure/logger/index.ts
207
+
208
+ import { envs } from "../../../../config/plugins";
209
+ // NOTA: envs es un archivo donde se leen variables de entorno .env
210
+ import { LoggerBootstrap } from "./core/logger.bootstrap";
211
+ import { parseLogLevel } from "./helper";
212
+ // NOTA: parseLogLevel hace un match con los valores del objeto LogLevel de @jmlq/logger
213
+
214
+ declare global {
215
+ var __LOGGER_BOOT__: Promise<LoggerBootstrap> | undefined;
216
+ }
217
+
218
+ async function init() {
219
+ return LoggerBootstrap.create({
220
+ minLevel: parseLogLevel(envs.logger.LOGGER_LEVEL),
221
+ pii: {
222
+ enabled: envs.logger.LOGGER_PII_ENABLED,
223
+ includeDefaults: envs.logger.LOGGER_PII_INCLUDE_DEFAULTS,
224
+ deep: true,
225
+ patterns: [
226
+ {
227
+ pattern: "\\b\\d{4}-\\d{4}-\\d{4}-\\d{4}\\b",
228
+ replaceWith: "****-****-****-****",
229
+ },
230
+ {
231
+ pattern: "[\\w.-]+@[\\w.-]+",
232
+ replaceWith: "***@***",
233
+ },
234
+ ],
235
+ },
236
+ adapters: {
237
+ // NOTA: Opcional, depende de las necesidades del cliente
238
+ fs: envs.logger.LOGGER_FS_PATH
239
+ ? {
240
+ basePath: envs.logger.LOGGER_FS_PATH,
241
+ fileNamePattern: "app-{yyyy}{MM}{dd}.log",
242
+ rotation: { by: "day" },
243
+ mkdir: true,
244
+ onRotate: (oldPath, newPath) => {
245
+ console.log(
246
+ ` [Rotate] Rotación completada: ${oldPath.absolutePath} → ${newPath.absolutePath}`
247
+ );
248
+ },
249
+ onError: (err) => {
250
+ console.error(" [Error Handler]", err.message);
251
+ },
252
+ }
253
+ : undefined,
254
+ mongo: envs.logger.LOGGER_MONGO_DB_URL
255
+ ? ({
256
+ dbName: envs.logger.LOGGER_MONGO_DB_NAME ?? "logs",
257
+ collectionName:
258
+ envs.logger.LOGGER_MONGO_COLLECTION_NAME ?? "app_logs",
259
+ ensureIndexes: true,
260
+ retentionDays: 7,
261
+ extraIndexes: [{ key: { level: 1 } }],
262
+ createIfMissing: true,
263
+ } as any)
264
+ : undefined,
265
+ pg: envs.logger.LOGGER_PG_CONNECTION_STRING
266
+ ? ({
267
+ schema: envs.logger.LOGGER_PG_SCHEMA ?? "public",
268
+ table: envs.logger.LOGGER_PG_TABLE_NAME ?? "logs",
269
+ createIfMissing: true,
270
+ enablePrune: true,
271
+ } as any)
272
+ : undefined,
273
+ },
274
+ });
275
+ }
276
+
277
+ // 1. Es una promesa singleton de LoggerBootstrap
278
+ // 2. Usa el operador nullish-coalescing (??) para: Reutilizar globalThis.__LOGGER_BOOT__ si ya existe.
279
+ // En caso contrario crea y memoriza (= init()) la promesa si no existe aún.
280
+ // 3. Garantiza una sola inicialización global del sistema de logging (adapters, datasources, PII, etc.)
281
+ // aunque el módulo se importe múltiples veces
282
+ export const bootReady: Promise<LoggerBootstrap> =
283
+ globalThis.__LOGGER_BOOT__ ?? (globalThis.__LOGGER_BOOT__ = init());
284
+
285
+ // 1. Es una promesa que resuelve directamente al logger
286
+ // 2. Hace un map de la promesa anterior: bootReady.then(b => b.logger).
287
+ export const loggerReady = bootReady.then((b) => b.logger);
288
+
289
+ // 1. Espera a bootReady y llama boot.flush(), que a su vez pide al logger/datasources
290
+ // que vacíen buffers pendientes (útil antes de apagar el proceso o en tests).
291
+ export async function flushLogs() {
292
+ const boot = await bootReady;
293
+ await boot.flush();
294
+ }
295
+
296
+ // 1. Espera a bootReady y llama boot.dispose(), que cierra recursos
297
+ // (conexiones a MongoDB/Postgres, file handles, etc.)
298
+ export async function disposeLogs() {
299
+ const boot = await bootReady;
300
+ await boot.dispose();
301
+ }
302
+ ```
303
+
304
+ ---
305
+
306
+ ## Configuración en Frameworks
307
+
308
+ **NOTA**: Se sugiere realizarlo en la capa `presentation` de la API que implementa `@jmlq/logger` y/o sus plugins.
309
+
310
+ ### Express
311
+
312
+ #### Crear middleware (opcional):
313
+
314
+ Se puede realizar de la forma que el usuario considere conveniente
315
+
316
+ ```ts
317
+ // Path Ejemplo: src/presentation/middlewares/attach-logger.middleware.ts
318
+
319
+ import { ILogger } from "@jmlq/logger";
320
+ import { randomUUID } from "crypto";
321
+ import { Request, Response, NextFunction } from "express";
322
+
323
+ /**
324
+ * Middleware factory que adjunta contexto de logging al request.
325
+ *
326
+ * Responsabilidades:
327
+ * 1. Inyectar un logger base accesible desde cualquier controller/middleware.
328
+ * 2. Garantizar un requestId único para trazabilidad y correlación de logs.
329
+ *
330
+ * No configura el logger ni emite logs:
331
+ * solo prepara el contexto por request.
332
+ */
333
+ export function attachLogger(base: ILogger) {
334
+ /**
335
+ * Middleware de Express ejecutado en cada request HTTP.
336
+ */
337
+ return async (req: Request, _res: Response, next: NextFunction) => {
338
+ /**
339
+ * Se adjunta el logger al objeto Request para evitar:
340
+ * - imports globales
341
+ * - singletons
342
+ * - acoplamiento directo en controllers
343
+ */
344
+ req.logger = base;
345
+
346
+ /**
347
+ * Se asigna un identificador único por request.
348
+ *
349
+ * - Si el cliente ya envía "x-request-id" (ej. API Gateway, Load Balancer),
350
+ * se reutiliza para mantener trazabilidad entre servicios.
351
+ * - Si no existe, se genera un UUID local.
352
+ */
353
+ req.requestId = (req.headers["x-request-id"] as string) ?? randomUUID();
354
+
355
+ /**
356
+ * Continúa el pipeline normal de Express.
357
+ */
358
+ next();
359
+ };
360
+ }
361
+ ```
362
+
363
+ #### Adjuntar middleware en el servidor
364
+
365
+ ```ts
366
+ // Path Ejemplo: src/presentation/server.ts
367
+
368
+ import { ILogger } from "@jmlq/logger";
369
+ import express from "express";
370
+ import { attachLogger } from "./middlewares";
371
+
372
+ export function createServer(base: ILogger) {
373
+ const app = express();
374
+ app.use(express.json()); // <-- necesario para POST JSON
375
+ app.use(attachLogger(base));
376
+
377
+ app.get("/health", (_req, res) => res.json({ ok: true }));
378
+
379
+ return app;
380
+ }
381
+ ```
382
+
383
+ #### Iniciar servidor
384
+
385
+ **NOTA**: Esto se realiza en la raíz de la API
386
+
387
+ ```ts
388
+ // Path Ejemplo: src/app.ts
389
+
390
+ import { envs } from "./config/plugins";
391
+ import { disposeLogs, flushLogs, loggerReady } from "./infrastructure/logger";
392
+ import { createServer } from "./presentation/server";
393
+
394
+ async function bootstrap() {
395
+ const logger = await loggerReady;
396
+
397
+ const app = createServer(logger);
398
+ const server = app.listen(envs.PORT, envs.HOST, () => {
399
+ console.log(`🚀 Server running at http://${envs.HOST}:${envs.PORT}`);
400
+ logger.info("http.start", { host: envs.HOST, port: envs.PORT });
401
+ });
402
+
403
+ server.on("error", async (err: any) => {
404
+ logger.error("http.listen.error", {
405
+ message: err?.message,
406
+ stack: err?.stack,
407
+ });
408
+ await flushLogs().catch(() => {});
409
+ await disposeLogs().catch(() => {});
410
+ process.exit(1);
411
+ });
412
+
413
+ // 3) Señales de apagado limpio
414
+ const shutdown = async (reason: string, code = 0) => {
415
+ logger.info("app.shutdown.begin", { reason });
416
+ server.close(async () => {
417
+ try {
418
+ await flushLogs();
419
+ } catch {}
420
+ try {
421
+ await disposeLogs();
422
+ } catch {}
423
+ logger.info("app.shutdown.end", { code });
424
+ process.exit(code);
425
+ });
426
+ };
427
+
428
+ process.on("SIGINT", () => shutdown("SIGINT"));
429
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
430
+
431
+ // Fallos no controlados
432
+ process.on("unhandledRejection", async (err) => {
433
+ const e = err as any;
434
+ logger.error("unhandled.rejection", {
435
+ message: e?.message,
436
+ stack: e?.stack,
437
+ });
438
+ await shutdown("unhandledRejection", 1);
439
+ });
440
+
441
+ process.on("uncaughtException", async (err) => {
442
+ logger.error("uncaught.exception", {
443
+ message: err.message,
444
+ stack: err.stack,
445
+ });
446
+ await shutdown("uncaughtException", 1);
447
+ });
448
+ }
449
+
450
+ bootstrap().catch(async (err) => {
451
+ // Falla durante el bootstrap
452
+ const logger = await loggerReady.catch(() => null);
453
+ if (logger) {
454
+ logger.error("bootstrap.fatal", {
455
+ message: (err as Error)?.message,
456
+ stack: (err as Error)?.stack,
457
+ });
458
+ await flushLogs().catch(() => {});
459
+ await disposeLogs().catch(() => {});
460
+ } else {
461
+ // último recurso si el logger no llegó a inicializar
462
+ console.error("Fatal error (no logger):", err);
463
+ }
464
+ process.exit(1);
465
+ });
466
+ ```
29
467
 
30
468
  ---
31
469
 
@@ -45,28 +483,26 @@ Con esta configuración se busca:
45
483
  El logger detecta y enmascara automáticamente datos sensibles utilizando patrones personalizados:
46
484
 
47
485
  ```js
48
-
49
- patterns: [
50
- // Tarjetas de crédito
51
- {
52
- pattern: "\\b\\d{4}[-\\s]?\\d{4}[-\\s]?\\d{4}[-\\s]?\\d{4}\\b",
53
- replaceWith: "****-****-****-****",
54
- },
55
- // Números de cuenta bancaria
56
- {
57
- pattern: "\\b\\d{10,20}\\b",
58
- replaceWith: "****ACCOUNT****",
59
- },
60
- // Tokens JWT
61
- {
62
- pattern: "eyJ[A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.?[A-Za-z0-9-_.+/=]*",
63
- replaceWith: "****JWT****",
64
- },
65
- ],
66
-
486
+ patterns: [
487
+ // Tarjetas de crédito
488
+ {
489
+ pattern: "\\b\\d{4}[-\\s]?\\d{4}[-\\s]?\\d{4}[-\\s]?\\d{4}\\b",
490
+ replaceWith: "****-****-****-****",
491
+ },
492
+ // Números de cuenta bancaria
493
+ {
494
+ pattern: "\\b\\d{10,20}\\b",
495
+ replaceWith: "****ACCOUNT****",
496
+ },
497
+ // Tokens JWT
498
+ {
499
+ pattern: "eyJ[A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.?[A-Za-z0-9-_.+/=]*",
500
+ replaceWith: "****JWT****",
501
+ },
502
+ ],
67
503
  ```
68
504
 
69
- Por ejemplo se tiene la siguiente información:
505
+ Por ejemplo, si se tiene la siguiente información:
70
506
 
71
507
  ```js
72
508
  [
@@ -87,7 +523,7 @@ Por ejemplo se tiene la siguiente información:
87
523
 
88
524
  Los logs registrarán lo siguiente:
89
525
 
90
- > - **File .log** (`@jmlq/logger-plugin-fs`)
526
+ > - **Archivo .log** (`@jmlq/logger-plugin-fs`)
91
527
 
92
528
  ```js
93
529
  {
@@ -117,15 +553,15 @@ Los logs registrarán lo siguiente:
117
553
 
118
554
  > - **MongoDB** (`@jmlq/logger-plugin-mongo`)
119
555
 
120
- ![MongoDB log](./assets/mongo-log.png)
556
+ ![MongoDB log](assets/mongo-log.png)
121
557
 
122
- > - **MongoDB** (`@jmlq/logger-plugin-mongo`)
558
+ > - **PostgreSQL** (`@jmlq/logger-plugin-postgresql`)
123
559
 
124
- ![Postgresql log](./assets/pg-log.png)
560
+ ![PostgreSQL log](assets/pg-log.png)
125
561
 
126
562
  ### Filtros y Consultas
127
563
 
128
- #### 1. Obtener todos los logs de nivel WARN o superior (en este caso retorna WARN, ERROR y FATAL)
564
+ #### 1. Obtener todos los logs de nivel WARN o superior (retorna WARN, ERROR y FATAL)
129
565
 
130
566
  ```ts
131
567
  const errors = await _req.logger?.getLogs({
@@ -138,8 +574,6 @@ Resultado:
138
574
  ```ini
139
575
  [2025-12-10T21:19:58.804Z] [50] app.error {"name":"AppError","code":"INTERNAL","message":"Unhandled exception","retryable":false,"meta":null,"cause":{"cause":{}},"stack":"AppError: Unhandled exception\n at AppError.internal (D:\\Fuentes\\Core\\ml-dev-rest-api\\dist\\shared\\errors\\app-error.js:61:16)\n at D:\\Fuentes\\Core\\ml-dev-rest-api\\dist\\presentation\\middlewares\\http\\error.middleware.js:36:40\n at Layer.handleError (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\lib\\layer.js:116:17)\n
140
576
  at trimPrefix (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:340:13)\n at D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:297:9\n at processParams (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:582:12)\n at Immediate.next (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:291:5)\n at Immediate._onImmediate (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:688:15)\n at process.processImmediate (node:internal/timers:485:21)"}
141
- [2025-12-10T21:19:58.804Z] [50] app.error {"code":"INTERNAL","name":"AppError","cause":{"cause":{}},"stack":"AppError: Unhandled exception\n at AppError.internal (D:\\Fuentes\\Core\\ml-dev-rest-api\\dist\\shared\\errors\\app-error.js:61:16)\n at D:\\Fuentes\\Core\\ml-dev-rest-api\\dist\\presentation\\middlewares\\http\\error.middleware.js:36:40\n at Layer.handleError (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\lib\\layer.js:116:17)\n at trimPrefix (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:340:13)\n at D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:297:9\n at processParams (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:582:12)\n at Immediate.next (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:291:5)\n at Immediate._onImmediate (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:688:15)\n at process.processImmediate (node:internal/timers:485:21)","message":"Unhandled exception","retryable":false}
142
-
143
577
  ```
144
578
 
145
579
  **NOTA**: `parseLogLevel` es un helper para convertir un `string` a `LogLevel`.
@@ -158,9 +592,8 @@ Resultado:
158
592
 
159
593
  ```ini
160
594
  [2025-12-10T22:21:58.500Z] [30] app.shutdown.begin {"reason":"SIGINT"}
161
- [2025-12-10T21:12:59.216Z] [50] app.error {"name":"AppError","code":"INTERNAL","message":"Unhandled exception","retryable":false,"meta":null,"cause":{"cause":{}},"stack":"AppError: Unhandled exception\n at AppError.internal (D:\\Fuentes\\Core\\ml-dev-rest-api\\dist\\shared\\errors\\app-error.js:61:16)\n at D:\\Fuentes\\Core\\ml-dev-rest-api\\dist\\presentation\\middlewares\\http\\error.middleware.js:36:40\n at Layer.handleError (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\lib\\layer.js:116:17)\n at trimPrefix (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:340:13)\n at D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:297:9\n at processParams (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:582:12)\n at Immediate.next (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:291:5)\n at Immediate._onImmediate (D:\\Fuentes\\Core\\ml-dev-rest-api\\node_modules\\router\\index.js:688:15)\n at process.processImmediate (node:internal/timers:485:21)"}
595
+ [2025-12-10T21:12:59.216Z] [50] app.error {"name":"AppError","code":"INTERNAL","message":"Unhandled exception","retryable":false,"meta":null,"cause":{"cause":{}},"stack":"AppError: Unhandled exception\n at AppError.internal (D:\\Fuentes\\Core\\ml-dev-rest-api\\dist\\shared\\errors\\app-error.js:61:16)"}
162
596
  [2025-12-10T21:04:21.565Z] [30] Users listed successfully {"count":2,"result":[{"id":"1","name":"John Doe","email":"***@***","card":"****-****-****-****"},{"id":"2","name":"Jane Doe","email":"***@***","card":"****-****-****-****"}]}
163
- [2025-12-10T21:04:21.565Z] [30] Users listed successfully {"count":2,"result":[{"id":"1","card":"****-****-****-****","name":"John Doe","email":"***@***"},{"id":"2","card":"****-****-****-****","name":"Jane Doe","email":"***@***"}]}
164
597
  [2025-12-10T21:03:32.910Z] [30] http.start {"host":"127.0.0.1","port":3000}
165
598
  ```
166
599
 
@@ -197,50 +630,9 @@ Resultado:
197
630
  [2025-12-10T22:58:09.874Z] [30] Users listed successfully {"count":2,"result":[{"id":"1","name":"John Doe","email":"***@***","card":"****-****-****-****"},{"id":"2","name":"Jane Doe","email":"***@***","card":"****-****-****-****"}]}
198
631
  ```
199
632
 
200
- ## 🏗️ Arquitectura
201
-
202
- El paquete sigue **Arquitectura Limpia**:
203
-
204
- ```
205
- src/
206
- ├─ domain/ # Reglas de negocio puras
207
- │ ├─ entities/
208
- │ ├─ value-objects/ # LogLevel
209
- │ ├─ services/ # PiiRedactor, MessageNormalizer
210
- │ ├─ ports/ # Interfaces/contratos
211
- │ └─ types/
212
- ├─ application/ # Casos de uso
213
- │ ├─ use-cases/ # SaveLog, GetLogs, FlushBuffers
214
- │ └─ factory/ # LoggerFactory
215
- └─ infrastructure/ # Implementaciones concretas
216
- └─ services/ # DataSourceService (composite)
217
- ```
218
-
219
- ### Casos de Uso Principales
220
-
221
- - **[`SaveLogUseCase`](src/application/use-cases/save-log.use-case.ts)** - Guarda logs aplicando filtros de nivel y PII
222
- - **[`GetLogsUseCase`](src/application/use-cases/get-logs.use-case.ts)** - Recupera logs con filtros
223
- - **[`FlushBuffersUseCase`](src/application/use-cases/flush-buffers.use-case.ts)** - Vacía buffers de datasources
224
-
225
- ### Servicios de Dominio
226
-
227
- - **[`PiiRedactor`](src/domain/services/pii-redactor.service.ts)** - Enmascara datos sensibles
228
- - **[`MessageNormalizer`](src/domain/services/message-normalizer.service.ts)** - Normaliza mensajes de log
229
- - **[`LogLevelService`](src/domain/services/log-level.service.ts)** - Maneja niveles de log
230
-
231
- ## 🔌 Plugins Disponibles
232
-
233
- | Plugin | Descripción | NPM |
234
- | -------------------------------- | -------------------------- | --------------------------------------------------------------- |
235
- | `@jmlq/logger-plugin-fs` | Persistencia en archivos | [npm](https://npmjs.com/package/@jmlq/logger-plugin-fs) |
236
- | `@jmlq/logger-plugin-mongo` | Persistencia en MongoDB | [npm](https://npmjs.com/package/@jmlq/logger-plugin-mongo) |
237
- | `@jmlq/logger-plugin-postgresql` | Persistencia en PostgreSQL | [npm](https://npmjs.com/package/@jmlq/logger-plugin-postgresql) |
238
-
239
633
  ## 📄 Más Información
240
634
 
241
635
  - **[Arquitectura Detallada](./architecture.md)** - Documentación técnica completa
242
- - **[Guía de Instalación](./install.md)** - Configuración paso a paso
243
- - **[Ejemplos](./examples/)** - Códigos de ejemplo funcionales
244
636
 
245
637
  ## 📝 Licencia
246
638