@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 +472 -80
- package/architecture.md +112 -90
- package/assets/mongo-log.png +0 -0
- package/assets/pg-log.png +0 -0
- package/dist/application/factory/logger.factory.d.ts +2 -1
- package/dist/application/types/index.d.ts +1 -0
- package/dist/application/types/index.js +17 -0
- package/dist/{domain/ports/logger-factory-config.port.d.ts → application/types/logger-factory-config.type.d.ts} +3 -3
- package/dist/domain/ports/index.d.ts +0 -1
- package/dist/domain/ports/index.js +0 -2
- package/dist/index.d.ts +2 -1
- package/package.json +2 -2
- package/install.md +0 -632
- /package/dist/{domain/ports/logger-factory-config.port.js → application/types/logger-factory-config.type.js} +0 -0
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
|
-
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Configuración @jmlq/logger
|
|
18
70
|
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
> - **
|
|
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
|
-

|
|
121
557
|
|
|
122
|
-
> - **
|
|
558
|
+
> - **PostgreSQL** (`@jmlq/logger-plugin-postgresql`)
|
|
123
559
|
|
|
124
|
-

|
|
125
561
|
|
|
126
562
|
### Filtros y Consultas
|
|
127
563
|
|
|
128
|
-
#### 1. Obtener todos los logs de nivel WARN o superior (
|
|
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)
|
|
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
|
|