@jmlq/logger 0.1.0-alpha.13 → 0.1.0-alpha.16
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 +141 -201
- package/dist/infrastructure/services/datasource.service.js +28 -0
- package/install.md +556 -291
- package/package.json +1 -1
package/install.md
CHANGED
|
@@ -1,367 +1,632 @@
|
|
|
1
|
-
#
|
|
1
|
+
# GUÍA DE CONFIGURACIÓN @jmlq/logger Y SUS PLUGINS
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## PASO 1 – BOOTSTRAP DEL LOGGER CORE
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
### 1.1. OPCIONES DE BOOTSTRAP
|
|
6
|
+
|
|
7
|
+
Path sugerido: `src/infrastructure/adapters/jmlq/logger/core/logger.bootstrap.options.ts`
|
|
8
|
+
|
|
9
|
+
`LoggerBootstrapOptions` es un **contrato de configuración** usado al momento de inicializar el logger. Define:
|
|
10
|
+
|
|
11
|
+
- El **nivel mínimo de log** permitido.
|
|
12
|
+
- Configuración de **redacción de PII** (datos sensibles).
|
|
13
|
+
- Los **datasources** del logger que se deben habilitar (FS, Mongo, PostgreSQL).
|
|
14
|
+
|
|
15
|
+
Describe cómo debe construirse el logger, qué debe censurar, y a dónde deben enviarse los logs.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { LogLevel } from "@jmlq/logger";
|
|
19
|
+
import { IFilesystemDatasourceOptions } from "@jmlq/logger-plugin-fs";
|
|
20
|
+
import { MongoPluginConfig } from "@jmlq/logger-plugin-mongo";
|
|
21
|
+
import { PostgresPluginConfig } from "@jmlq/logger-plugin-postgresql";
|
|
22
|
+
|
|
23
|
+
export interface LoggerBootstrapOptions {
|
|
24
|
+
minLevel: LogLevel;
|
|
25
|
+
pii?: {
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
whitelistKeys?: string[];
|
|
28
|
+
blacklistKeys?: string[];
|
|
29
|
+
patterns?: any[];
|
|
30
|
+
deep?: boolean;
|
|
31
|
+
includeDefaults?: boolean;
|
|
32
|
+
};
|
|
33
|
+
adapters?: {
|
|
34
|
+
// NOTA: Opcionales depende de las necesidades del cliente
|
|
35
|
+
fs?: IFilesystemDatasourceOptions;
|
|
36
|
+
mongo?: MongoPluginConfig;
|
|
37
|
+
pg?: PostgresPluginConfig;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
7
40
|
```
|
|
8
41
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
- `minLevel`: nivel mínimo de log (ss de tipo `LogLevel` de `@jmlq/logger`).
|
|
43
|
+
- `pii`: configuración de redacción (redaction) de datos sensibles.
|
|
44
|
+
- `adapters`: configuración específica para cada plugin (**todos opcionales**).
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### 1.2. IMPLEMENTACIÓN DEL BOOTSTRAP
|
|
49
|
+
|
|
50
|
+
Path sugerido: `src/infrastructure/adapters/jmlq/logger/core/logger.bootstrap.ts`
|
|
51
|
+
|
|
52
|
+
La clase `LoggerBootstrap` es un componente de infraestructura cuyo objetivo es:
|
|
53
|
+
|
|
54
|
+
- Crear e inicializar el **logger principal** de la aplicación.
|
|
55
|
+
- Construir la lista de **datasources** (FS, Mongo, PostgreSQL) según la configuración del usuario.
|
|
56
|
+
- Exponer funciones para **flush** y **dispose**, importantes para apagado limpio.
|
|
57
|
+
- Encapsular la instancia del logger para no exponer detalles internos.
|
|
58
|
+
|
|
59
|
+
La clase completa se encarga de arrancar correctamente el sistema de logging.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { type ILogDatasource, LoggerFactory } from "@jmlq/logger";
|
|
63
|
+
import { FsAdapter } from "../fs/fs.adapter";
|
|
64
|
+
import { MongoAdapter } from "../mongo/mongo.adapter";
|
|
65
|
+
import { PgAdapter } from "../pg/pg.adapter";
|
|
66
|
+
import { LoggerBootstrapOptions } from "./logger.bootstrap.options";
|
|
67
|
+
|
|
68
|
+
export class LoggerBootstrap {
|
|
69
|
+
private constructor(
|
|
70
|
+
private readonly _logger: ReturnType<typeof LoggerFactory.create>
|
|
71
|
+
) {}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Este es el punto de entrada para construir el logger.
|
|
75
|
+
*/
|
|
76
|
+
static async create(opts: LoggerBootstrapOptions): Promise<LoggerBootstrap> {
|
|
77
|
+
// Almacena los plugins inicializados.
|
|
78
|
+
const dsList: ILogDatasource[] = [];
|
|
79
|
+
|
|
80
|
+
// Inyectar datasources
|
|
81
|
+
// NOTA: Opcionales depende de las necesidades del cliente
|
|
82
|
+
// FS si está configurado
|
|
83
|
+
if (opts.adapters?.fs) {
|
|
84
|
+
const fs = FsAdapter.create(opts.adapters.fs);
|
|
85
|
+
if (fs) dsList.push(fs.datasource);
|
|
86
|
+
}
|
|
87
|
+
// MongoDB si está configurado
|
|
88
|
+
if (opts.adapters?.mongo) {
|
|
89
|
+
const mongo = await MongoAdapter.create(opts.adapters.mongo);
|
|
90
|
+
if (mongo) dsList.push(mongo.datasource);
|
|
91
|
+
}
|
|
92
|
+
// Postgresql si está configurado
|
|
93
|
+
if (opts.adapters?.pg) {
|
|
94
|
+
const pg = await PgAdapter.create(opts.adapters.pg);
|
|
95
|
+
if (pg) dsList.push(pg.datasource);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (dsList.length === 0)
|
|
99
|
+
throw new Error("[logger] No hay datasources válidos.");
|
|
100
|
+
|
|
101
|
+
// Crear el logger central
|
|
102
|
+
const logger = LoggerFactory.create({
|
|
103
|
+
datasources: dsList,
|
|
104
|
+
minLevel: opts.minLevel,
|
|
105
|
+
redactorOptions: {
|
|
106
|
+
enabled: opts.pii?.enabled ?? false,
|
|
107
|
+
deep: opts.pii?.deep ?? true,
|
|
108
|
+
patterns: opts.pii?.patterns ?? [],
|
|
52
109
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
console.log("✅ Logger creado correctamente.\n");
|
|
113
|
+
|
|
114
|
+
// Retornar la instancia final
|
|
115
|
+
return new LoggerBootstrap(logger);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Permite acceder al logger real pero manteniendo encapsulación.
|
|
119
|
+
get logger() {
|
|
120
|
+
return this._logger;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fuerza a que todos los logs pendientes se escriban en los datasources.
|
|
124
|
+
// Lo usa el ciclo de vida del servidor (shutdown, errores críticos, etc.).
|
|
125
|
+
async flush() {
|
|
126
|
+
const logs = this._logger as any;
|
|
127
|
+
if (typeof logs.flush === "function") await logs.flush();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Cierra conexiones y limpia recursos (como conexiones a BD).
|
|
131
|
+
// Asegura un apagado ordenado.
|
|
132
|
+
async dispose() {
|
|
133
|
+
const logs = this._logger as any;
|
|
134
|
+
if (typeof logs.dispose === "function") await logs.dispose();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
59
137
|
```
|
|
60
138
|
|
|
61
|
-
|
|
139
|
+
---
|
|
62
140
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
141
|
+
## PASO 2 – CONFIGURAR PLUGINS / DATASOURCES
|
|
142
|
+
|
|
143
|
+
### 2.1. Plugin FileSystem (`@jmlq/logger-plugin-fs`)
|
|
144
|
+
|
|
145
|
+
Path sugerido: `src/infrastructure/adapters/jmlq/logger/fs/fs.adapter.ts`
|
|
67
146
|
|
|
68
|
-
|
|
69
|
-
export class UsersService {
|
|
70
|
-
constructor(@Inject("LOGGER") private readonly logger: ILogger) {}
|
|
147
|
+
`FsAdapter` es un **adaptador de infraestructura** cuyo propósito es `crear`, `inicializar` y `exponer` un `datasource` de logs basado en archivos (FileSystem) usando el plugin `@jmlq/logger-plugin-fs`.
|
|
71
148
|
|
|
72
|
-
|
|
149
|
+
```ts
|
|
150
|
+
import {
|
|
151
|
+
// Función del plugin que crea realmente el datasource de FileSystem.
|
|
152
|
+
createFsDatasource,
|
|
153
|
+
// Tipo de configuración que el plugin requiere
|
|
154
|
+
IFilesystemDatasourceOptions,
|
|
155
|
+
} from "@jmlq/logger-plugin-fs";
|
|
156
|
+
// Interfaz estándar que todos los datasources deben implementar para funcionar dentro de @jmlq/logger
|
|
157
|
+
import type { ILogDatasource } from "@jmlq/logger";
|
|
158
|
+
|
|
159
|
+
export class FsAdapter {
|
|
160
|
+
private constructor(private readonly ds: ILogDatasource) {}
|
|
161
|
+
|
|
162
|
+
// Este es el punto central del adaptador.
|
|
163
|
+
static create(opts: IFilesystemDatasourceOptions): FsAdapter | undefined {
|
|
164
|
+
try {
|
|
165
|
+
// Se inicializa el plugin.
|
|
166
|
+
// Se configuran rutas, políticas de rotación, serializer, etc.
|
|
167
|
+
// El datasource queda listo para recibir logs.
|
|
168
|
+
const ds = createFsDatasource(opts);
|
|
169
|
+
console.log("[logger] Conectado a FS para logs");
|
|
170
|
+
return new FsAdapter(ds);
|
|
171
|
+
} catch (e: any) {
|
|
172
|
+
console.warn("[logger] FS deshabilitado:", e?.message ?? e);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Permite a otras capas (especialmente LoggerBootstrap) obtener el datasource real que necesita LoggerFactory.create
|
|
176
|
+
get datasource(): ILogDatasource {
|
|
177
|
+
return this.ds;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### 2.2. Plugin MongoDB (`@jmlq/logger-plugin-mongo`)
|
|
185
|
+
|
|
186
|
+
Path sugerido: `src/infrastructure/adapters/jmlq/logger/mongo/mongo.adapter.ts`
|
|
187
|
+
|
|
188
|
+
`MongoAdapter` es un **adaptador de infraestructura** cuyo propósito es `crear`, `inicializar` y `exponer` un `datasource` de los logs almacenados en `MongoDB` usando el plugin `@jmlq/logger-plugin-mongo`.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// Interfaz estándar que todos los datasources deben implementar para funcionar dentro de @jmlq/logger
|
|
192
|
+
import type { ILogDatasource } from "@jmlq/logger";
|
|
193
|
+
import {
|
|
194
|
+
// Función del plugin que crea realmente el datasource de MongoDb.
|
|
195
|
+
createMongoDatasource,
|
|
196
|
+
// Tipo de configuración que el plugin requiere
|
|
197
|
+
MongoPluginConfig,
|
|
198
|
+
} from "@jmlq/logger-plugin-mongo";
|
|
199
|
+
|
|
200
|
+
export class MongoAdapter {
|
|
201
|
+
private constructor(private readonly ds: ILogDatasource) {}
|
|
202
|
+
|
|
203
|
+
// Este es el punto central del adaptador.
|
|
204
|
+
static async create(
|
|
205
|
+
opts: MongoPluginConfig
|
|
206
|
+
): Promise<MongoAdapter | undefined> {
|
|
73
207
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return result;
|
|
80
|
-
} catch (error) {
|
|
81
|
-
await this.logger.error("Error al crear usuario", {
|
|
82
|
-
error: error.message,
|
|
83
|
-
userData,
|
|
84
|
-
});
|
|
85
|
-
throw error;
|
|
208
|
+
const ds = await createMongoDatasource(opts);
|
|
209
|
+
console.log("[logger] Conectado a MongoDB para logs");
|
|
210
|
+
return new MongoAdapter(ds);
|
|
211
|
+
} catch (e: any) {
|
|
212
|
+
console.warn("[logger] MongoDB deshabilitado:", e?.message ?? e);
|
|
86
213
|
}
|
|
87
214
|
}
|
|
215
|
+
get datasource(): ILogDatasource {
|
|
216
|
+
return this.ds;
|
|
217
|
+
}
|
|
88
218
|
}
|
|
89
219
|
```
|
|
90
220
|
|
|
91
|
-
|
|
221
|
+
---
|
|
92
222
|
|
|
93
|
-
|
|
223
|
+
### 2.3. Plugin PostgreSQL (`@jmlq/logger-plugin-postgresql`)
|
|
224
|
+
|
|
225
|
+
Path sugerido: `src/infrastructure/adapters/jmlq/logger/pg/pg.adapter.ts`
|
|
226
|
+
|
|
227
|
+
`PgAdapter` es un **adaptador de infraestructura** cuyo propósito es `crear`, `inicializar` y `exponer` un `datasource` de los logs almacenados en `Postgresal` usando el plugin `@jmlq/logger-plugin-postgresql`.
|
|
94
228
|
|
|
95
229
|
```ts
|
|
96
|
-
//
|
|
97
|
-
import {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
230
|
+
// Interfaz estándar que todos los datasources deben implementar para funcionar dentro de @jmlq/logger
|
|
231
|
+
import type { ILogDatasource } from "@jmlq/logger";
|
|
232
|
+
import {
|
|
233
|
+
// Función del plugin que crea realmente el datasource de Postgresql.
|
|
234
|
+
createPostgresDatasource,
|
|
235
|
+
// Tipo de configuración que el plugin requiere
|
|
236
|
+
PostgresPluginConfig,
|
|
237
|
+
} from "@jmlq/logger-plugin-postgresql";
|
|
238
|
+
|
|
239
|
+
export class PgAdapter {
|
|
240
|
+
private constructor(private readonly ds: ILogDatasource) {}
|
|
241
|
+
|
|
242
|
+
// Este es el punto central del adaptador.
|
|
243
|
+
static async create(
|
|
244
|
+
opts: PostgresPluginConfig
|
|
245
|
+
): Promise<PgAdapter | undefined> {
|
|
246
|
+
try {
|
|
247
|
+
const ds = await createPostgresDatasource(opts);
|
|
248
|
+
console.log("[logger] Conectado a PostgreSQL para logs");
|
|
249
|
+
return new PgAdapter(ds);
|
|
250
|
+
} catch (e: any) {
|
|
251
|
+
console.warn("[logger] PostgreSQL deshabilitado:", e?.message ?? e);
|
|
252
|
+
}
|
|
110
253
|
}
|
|
111
254
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
255
|
+
get datasource(): ILogDatasource {
|
|
256
|
+
return this.ds;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 2.4. Configuración Global @jmlq/Logger
|
|
262
|
+
|
|
263
|
+
Path sugerido: `src/infrastructure/adapters/jmlq/logger/index.ts`
|
|
264
|
+
|
|
265
|
+
Este archivo implementa el **bootstrap global** y singleton del sistema de logging de tu proyecto.
|
|
266
|
+
En términos simples: garantiza que **el logger, sus adapters y sus reglas de PII se inicialicen una sola vez**, sin importar cuántas veces se importe este archivo en distintos módulos.
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import { envs } from "../../../../config/plugins";
|
|
270
|
+
import { LoggerBootstrap } from "./core/logger.bootstrap";
|
|
271
|
+
import { parseLogLevel } from "./helper";
|
|
272
|
+
|
|
273
|
+
// Promesa Singleton
|
|
274
|
+
declare global {
|
|
275
|
+
var __LOGGER_BOOT__: Promise<LoggerBootstrap> | undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Construye toda la infraestructura del logger
|
|
279
|
+
async function init() {
|
|
280
|
+
// Envía la configuración del logger en envs (se puede usar .env)
|
|
281
|
+
return LoggerBootstrap.create({
|
|
282
|
+
// Nivel mínimo de log
|
|
283
|
+
minLevel: parseLogLevel(envs.logger.LOGGER_LEVEL),
|
|
284
|
+
// Configuración de PII (ocultamiento de datos sensibles)
|
|
285
|
+
pii: {
|
|
286
|
+
// Activación global
|
|
287
|
+
enabled: envs.logger.LOGGER_PII_ENABLED,
|
|
288
|
+
// Incluir reglas de validación declaradas en @jmlq/logger
|
|
289
|
+
includeDefaults: envs.logger.LOGGER_PII_INCLUDE_DEFAULTS,
|
|
290
|
+
// Incluye recursión profunda
|
|
117
291
|
deep: true,
|
|
118
|
-
includeDefaults: true,
|
|
119
292
|
patterns: [
|
|
293
|
+
// Patrón para ocultar tarjetas
|
|
120
294
|
{
|
|
121
295
|
pattern: "\\b\\d{4}-\\d{4}-\\d{4}-\\d{4}\\b",
|
|
122
296
|
replaceWith: "****-****-****-****",
|
|
123
297
|
},
|
|
298
|
+
// Patrón para ocultar correos
|
|
299
|
+
{
|
|
300
|
+
pattern: "[\\w.-]+@[\\w.-]+",
|
|
301
|
+
replaceWith: "***@***",
|
|
302
|
+
},
|
|
303
|
+
// NOTA: Se pueden agregar mas reglas
|
|
124
304
|
],
|
|
125
305
|
},
|
|
306
|
+
adapters: {
|
|
307
|
+
// NOTA: Opcionales depende de las necesidades del cliente
|
|
308
|
+
// Adapter FS
|
|
309
|
+
fs: envs.logger.LOGGER_FS_PATH
|
|
310
|
+
? {
|
|
311
|
+
// Path donde se guarda el log
|
|
312
|
+
basePath: envs.logger.LOGGER_FS_PATH,
|
|
313
|
+
// Patrón de archivo por fecha
|
|
314
|
+
fileNamePattern: "app-{yyyy}{MM}{dd}.log",
|
|
315
|
+
// Tipo de Rotacion (Generación de un nuevo archivo puede ser por día, peso, etc)
|
|
316
|
+
rotation: { by: "day" },
|
|
317
|
+
// Crear directorio nuevo
|
|
318
|
+
mkdir: true,
|
|
319
|
+
// Darle formato a la trama
|
|
320
|
+
serializer: {
|
|
321
|
+
serialize(log: any) {
|
|
322
|
+
// Serializer personalizado
|
|
323
|
+
return JSON.stringify(log, null, 2);
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
// Se ejecuta acción cuando se genera un nuevo archivo (Se podría enviar un email por ejempo)
|
|
327
|
+
onRotate: (oldPath, newPath) => {
|
|
328
|
+
console.log(
|
|
329
|
+
` [Rotate] Rotación completada: ${oldPath.absolutePath} → ${newPath.absolutePath}`
|
|
330
|
+
);
|
|
331
|
+
},
|
|
332
|
+
// Se ejecuta acción cuando se presenta un error
|
|
333
|
+
onError: (err) => {
|
|
334
|
+
console.error(" [Error Handler]", err.message);
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
: undefined,
|
|
338
|
+
// Adapter MongoDB
|
|
339
|
+
mongo: envs.logger.LOGGER_MONGO_DB_URL
|
|
340
|
+
? {
|
|
341
|
+
url: envs.logger.LOGGER_MONGO_DB_URL,
|
|
342
|
+
dbName: envs.logger.LOGGER_MONGO_DB_NAME ?? "logs",
|
|
343
|
+
collectionName:
|
|
344
|
+
envs.logger.LOGGER_MONGO_COLLECTION_NAME ?? "app_logs",
|
|
345
|
+
ensureIndexes: true,
|
|
346
|
+
// Tiempo de retención del log
|
|
347
|
+
retentionDays: 7,
|
|
348
|
+
// Crear Index extra (opcional)
|
|
349
|
+
extraIndexes: [{ key: { level: 1 } }],
|
|
350
|
+
// writeOrdered: true,
|
|
351
|
+
createIfMissing: true,
|
|
352
|
+
}
|
|
353
|
+
: undefined,
|
|
354
|
+
// Adapter Postgresql
|
|
355
|
+
pg: envs.logger.LOGGER_PG_CONNECTION_STRING
|
|
356
|
+
? {
|
|
357
|
+
connectionString: envs.logger.LOGGER_PG_CONNECTION_STRING,
|
|
358
|
+
table: envs.logger.LOGGER_PG_TABLE_NAME ?? "app_logs",
|
|
359
|
+
schema: envs.logger.LOGGER_PG_SCHEMA ?? "public",
|
|
360
|
+
createIfMissing: true,
|
|
361
|
+
// Tiempo de retención del log
|
|
362
|
+
retentionDays: 7,
|
|
363
|
+
}
|
|
364
|
+
: undefined,
|
|
365
|
+
//----
|
|
366
|
+
},
|
|
126
367
|
});
|
|
127
|
-
}
|
|
128
|
-
```
|
|
368
|
+
}
|
|
129
369
|
|
|
130
|
-
|
|
370
|
+
// Inicializar promesa Singleton
|
|
371
|
+
export const bootReady: Promise<LoggerBootstrap> =
|
|
372
|
+
globalThis.__LOGGER_BOOT__ ?? (globalThis.__LOGGER_BOOT__ = init());
|
|
131
373
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
import { Request, Response, NextFunction } from "express";
|
|
135
|
-
import { ILogger } from "@jmlq/logger";
|
|
374
|
+
// Acceso directo al logger
|
|
375
|
+
export const loggerReady = bootReady.then((b) => b.logger);
|
|
136
376
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
377
|
+
// Vaciar buffers antes de terminar el proceso
|
|
378
|
+
export async function flushLogs() {
|
|
379
|
+
const boot = await bootReady;
|
|
380
|
+
await boot.flush();
|
|
381
|
+
}
|
|
140
382
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
383
|
+
// Cerrar recursos
|
|
384
|
+
export async function disposeLogs() {
|
|
385
|
+
const boot = await bootReady;
|
|
386
|
+
await boot.dispose();
|
|
387
|
+
}
|
|
388
|
+
```
|
|
147
389
|
|
|
148
|
-
|
|
149
|
-
const duration = Date.now() - startTime;
|
|
150
|
-
const level = res.statusCode >= 400 ? "error" : "info";
|
|
390
|
+
**NOTA**: `parseLogLevel` es un helper para convertir un `string` en `LogLevel`:
|
|
151
391
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
url: req.url,
|
|
155
|
-
statusCode: res.statusCode,
|
|
156
|
-
duration,
|
|
157
|
-
});
|
|
158
|
-
});
|
|
392
|
+
```ts
|
|
393
|
+
import { LogLevel } from "@jmlq/logger";
|
|
159
394
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
};
|
|
163
|
-
```
|
|
395
|
+
export function parseLogLevel(value?: string): LogLevel {
|
|
396
|
+
if (!value) return LogLevel.DEBUG; // fallback seguro
|
|
164
397
|
|
|
165
|
-
|
|
398
|
+
const normalized = value.toUpperCase();
|
|
166
399
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
app.get("/users/:id", async (req, res) => {
|
|
179
|
-
try {
|
|
180
|
-
await logger.info("Obteniendo usuario", { userId: req.params.id });
|
|
181
|
-
// ... lógica
|
|
182
|
-
res.json(user);
|
|
183
|
-
} catch (error) {
|
|
184
|
-
await logger.error("Error al obtener usuario", {
|
|
185
|
-
userId: req.params.id,
|
|
186
|
-
error: error.message,
|
|
187
|
-
});
|
|
188
|
-
res.status(500).json({ error: "Internal server error" });
|
|
189
|
-
}
|
|
190
|
-
});
|
|
400
|
+
const map: Record<string, LogLevel> = {
|
|
401
|
+
TRACE: LogLevel.TRACE,
|
|
402
|
+
DEBUG: LogLevel.DEBUG,
|
|
403
|
+
INFO: LogLevel.INFO,
|
|
404
|
+
WARN: LogLevel.WARN,
|
|
405
|
+
ERROR: LogLevel.ERROR,
|
|
406
|
+
FATAL: LogLevel.FATAL,
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
return map[normalized] ?? LogLevel.DEBUG; // fallback en caso de valor inválido
|
|
410
|
+
}
|
|
191
411
|
```
|
|
192
412
|
|
|
193
|
-
|
|
413
|
+
---
|
|
194
414
|
|
|
195
|
-
|
|
415
|
+
## PASO 3 – INTEGRACIÓN CON EXPRESS
|
|
196
416
|
|
|
197
|
-
|
|
417
|
+
### 3.1. Extender `Request` con logger y requestId
|
|
418
|
+
|
|
419
|
+
Path sugerido: `src/types/express.d.ts`
|
|
198
420
|
|
|
199
421
|
```ts
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
422
|
+
import "express";
|
|
423
|
+
import type { ILogger } from "@jmlq/logger";
|
|
424
|
+
|
|
425
|
+
// Cada req trae un logger y un requestId.
|
|
426
|
+
declare module "express-serve-static-core" {
|
|
427
|
+
interface Request {
|
|
428
|
+
logger?: ILogger;
|
|
429
|
+
requestId?: string;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
208
432
|
```
|
|
209
433
|
|
|
210
|
-
|
|
434
|
+
Esto permite escribir `req.logger` y `req.requestId` con type safety.
|
|
435
|
+
|
|
436
|
+
### 3.2. Crear el servidor y adjuntar el logger
|
|
437
|
+
|
|
438
|
+
Path sugerido: `src/presentation/server.ts`
|
|
211
439
|
|
|
212
440
|
```ts
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
{
|
|
227
|
-
pattern: "[A-Z]{4}\\d{6}[HM][A-Z]{5}[A-Z0-9]\\d",
|
|
228
|
-
replaceWith: "****CURP****",
|
|
229
|
-
},
|
|
230
|
-
// Números de cuenta bancaria
|
|
231
|
-
{
|
|
232
|
-
pattern: "\\b\\d{10,20}\\b",
|
|
233
|
-
replaceWith: "****ACCOUNT****",
|
|
234
|
-
},
|
|
235
|
-
// Tokens JWT
|
|
236
|
-
{
|
|
237
|
-
pattern: "eyJ[A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.?[A-Za-z0-9-_.+/=]*",
|
|
238
|
-
replaceWith: "****JWT****",
|
|
239
|
-
},
|
|
240
|
-
],
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
```
|
|
441
|
+
import { ILogger } from "@jmlq/logger";
|
|
442
|
+
import express, { Request, Response, NextFunction } from "express";
|
|
443
|
+
import router from "./routes";
|
|
444
|
+
import { randomUUID } from "crypto";
|
|
445
|
+
|
|
446
|
+
// Middleware para adjuntar logger por request
|
|
447
|
+
function attachLogger(base: ILogger) {
|
|
448
|
+
return async (req: Request, _res: Response, next: NextFunction) => {
|
|
449
|
+
req.logger = base;
|
|
450
|
+
req.requestId = (req.headers["x-request-id"] as string) ?? randomUUID();
|
|
451
|
+
next();
|
|
452
|
+
};
|
|
453
|
+
}
|
|
244
454
|
|
|
245
|
-
|
|
455
|
+
export function createServer(base: ILogger) {
|
|
456
|
+
const app = express();
|
|
457
|
+
app.use(express.json());
|
|
458
|
+
app.use(attachLogger(base));
|
|
246
459
|
|
|
247
|
-
|
|
460
|
+
// Aquí se montan las rutas que llamarán a tus casos de uso
|
|
461
|
+
// <-- los handlers pueden usar req.logger
|
|
248
462
|
|
|
249
|
-
|
|
250
|
-
// Obtener todos los logs de nivel ERROR o superior
|
|
251
|
-
const errors = await logger.getLogs({
|
|
252
|
-
levelMin: LogLevel.ERROR,
|
|
253
|
-
});
|
|
463
|
+
app.get("/health", (_req, res) => res.json({ ok: true }));
|
|
254
464
|
|
|
255
|
-
//
|
|
256
|
-
const yesterday = Date.now() - 24 * 60 * 60 * 1000;
|
|
257
|
-
const recentLogs = await logger.getLogs({
|
|
258
|
-
since: yesterday,
|
|
259
|
-
until: Date.now(),
|
|
260
|
-
});
|
|
465
|
+
// IMPORTANTE: Siempre colocar un errorHandler al final
|
|
261
466
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
query: "usuario",
|
|
265
|
-
limit: 50,
|
|
266
|
-
});
|
|
467
|
+
return app;
|
|
468
|
+
}
|
|
267
469
|
```
|
|
268
470
|
|
|
269
|
-
|
|
471
|
+
En tus controladores, la idea es:
|
|
270
472
|
|
|
271
473
|
```ts
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
query: "database connection",
|
|
277
|
-
limit: 20,
|
|
278
|
-
offset: 0,
|
|
474
|
+
// controlador de ejemplo (no incluido en este archivo)
|
|
475
|
+
req.logger?.info("user.create.request", {
|
|
476
|
+
requestId: req.requestId,
|
|
477
|
+
// aquí podrías pasar info de la request (sin PII sensible)
|
|
279
478
|
});
|
|
280
479
|
|
|
281
|
-
//
|
|
282
|
-
const page1 = await logger.getLogs({ limit: 10, offset: 0 });
|
|
283
|
-
const page2 = await logger.getLogs({ limit: 10, offset: 10 });
|
|
480
|
+
// aquí llamarías al caso de uso correspondiente (no se implementa en este doc)
|
|
284
481
|
```
|
|
285
482
|
|
|
286
|
-
|
|
483
|
+
---
|
|
287
484
|
|
|
288
|
-
|
|
289
|
-
# Nivel mínimo de logs
|
|
290
|
-
LOG_LEVEL=info
|
|
485
|
+
## PASO 4 – BOOTSTRAP DE LA APLICACIÓN (Express + logger)
|
|
291
486
|
|
|
292
|
-
|
|
293
|
-
LOG_FS_ENABLED=true
|
|
294
|
-
LOG_FS_PATH=./logs
|
|
487
|
+
Path sugerido: `src/app.ts`
|
|
295
488
|
|
|
296
|
-
|
|
297
|
-
LOG_MONGO_ENABLED=true
|
|
298
|
-
LOG_MONGO_URL=mongodb://localhost:27017
|
|
299
|
-
LOG_MONGO_DB=logs
|
|
489
|
+
Aquí se orquesta todo:
|
|
300
490
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
491
|
+
```ts
|
|
492
|
+
import { envs } from "./config/plugins";
|
|
493
|
+
import {
|
|
494
|
+
disposeLogs,
|
|
495
|
+
flushLogs,
|
|
496
|
+
loggerReady,
|
|
497
|
+
} from "./infrastructure/adapters/jmlq/logger";
|
|
498
|
+
import { createServer } from "./presentation/server";
|
|
499
|
+
|
|
500
|
+
async function bootstrap() {
|
|
501
|
+
const logger = await loggerReady;
|
|
502
|
+
|
|
503
|
+
const app = createServer(logger);
|
|
504
|
+
const server = app.listen(envs.PORT, envs.HOST, () => {
|
|
505
|
+
console.log(`🚀 Server running at http://${envs.HOST}:${envs.PORT}`);
|
|
506
|
+
logger.info("http.start", { host: envs.HOST, port: envs.PORT });
|
|
507
|
+
});
|
|
309
508
|
|
|
310
|
-
|
|
509
|
+
server.on("error", async (err: any) => {
|
|
510
|
+
logger.error("http.listen.error", {
|
|
511
|
+
message: err?.message,
|
|
512
|
+
stack: err?.stack,
|
|
513
|
+
});
|
|
514
|
+
await flushLogs().catch(() => {});
|
|
515
|
+
await disposeLogs().catch(() => {});
|
|
516
|
+
process.exit(1);
|
|
517
|
+
});
|
|
311
518
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
includeDefaults: true,
|
|
327
|
-
patterns: [
|
|
328
|
-
{
|
|
329
|
-
pattern: "\\b\\d{4}-\\d{4}-\\d{4}-\\d{4}\\b",
|
|
330
|
-
replaceWith: "****-****-****-****",
|
|
331
|
-
},
|
|
332
|
-
],
|
|
333
|
-
},
|
|
334
|
-
});
|
|
519
|
+
// Señales de apagado limpio
|
|
520
|
+
const shutdown = async (reason: string, code = 0) => {
|
|
521
|
+
logger.info("app.shutdown.begin", { reason });
|
|
522
|
+
server.close(async () => {
|
|
523
|
+
try {
|
|
524
|
+
await flushLogs();
|
|
525
|
+
} catch {}
|
|
526
|
+
try {
|
|
527
|
+
await disposeLogs();
|
|
528
|
+
} catch {}
|
|
529
|
+
logger.info("app.shutdown.end", { code });
|
|
530
|
+
process.exit(code);
|
|
531
|
+
});
|
|
532
|
+
};
|
|
335
533
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
await logger.info("User login", { email: "user@example.com" });
|
|
339
|
-
await logger.warn("High latency", { endpoint: "/api/users", duration: 950 });
|
|
340
|
-
await logger.error("Database error", { error: "Connection timeout" });
|
|
341
|
-
await logger.fatal("System crash", { stack: error.stack });
|
|
342
|
-
|
|
343
|
-
// Filtrar y consultar logs
|
|
344
|
-
const recentErrors = await logger.getLogs({
|
|
345
|
-
levelMin: LogLevel.ERROR,
|
|
346
|
-
since: Date.now() - 24 * 60 * 60 * 1000,
|
|
347
|
-
limit: 100,
|
|
348
|
-
});
|
|
534
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
535
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
349
536
|
|
|
350
|
-
//
|
|
351
|
-
|
|
537
|
+
// Fallos no controlados
|
|
538
|
+
process.on("unhandledRejection", async (err) => {
|
|
539
|
+
const e = err as any;
|
|
540
|
+
logger.error("unhandled.rejection", {
|
|
541
|
+
message: e?.message,
|
|
542
|
+
stack: e?.stack,
|
|
543
|
+
});
|
|
544
|
+
// Aquí se podría enviar una notificación (email, etc.)
|
|
545
|
+
await shutdown("unhandledRejection", 1);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
process.on("uncaughtException", async (err) => {
|
|
549
|
+
logger.error("uncaught.exception", {
|
|
550
|
+
message: err.message,
|
|
551
|
+
stack: err.stack,
|
|
552
|
+
});
|
|
553
|
+
// Aquí se podría enviar una notificación (email, etc.)
|
|
554
|
+
await shutdown("uncaughtException", 1);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
bootstrap().catch(async (err) => {
|
|
559
|
+
// Falla durante el bootstrap
|
|
560
|
+
const logger = await loggerReady.catch(() => null);
|
|
561
|
+
if (logger) {
|
|
562
|
+
logger.error("bootstrap.fatal", {
|
|
563
|
+
message: (err as Error)?.message,
|
|
564
|
+
stack: (err as Error)?.stack,
|
|
565
|
+
});
|
|
566
|
+
await flushLogs().catch(() => {});
|
|
567
|
+
await disposeLogs().catch(() => {});
|
|
568
|
+
} else {
|
|
569
|
+
// Último recurso si el logger no llegó a inicializar
|
|
570
|
+
console.error("Fatal error (no logger):", err);
|
|
571
|
+
}
|
|
572
|
+
process.exit(1);
|
|
573
|
+
});
|
|
352
574
|
```
|
|
353
575
|
|
|
354
|
-
|
|
576
|
+
Puntos clave:
|
|
355
577
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
578
|
+
- `loggerReady` asegura que el logger está inicializado antes de levantar HTTP.
|
|
579
|
+
- `flushLogs` y `disposeLogs` se usan:
|
|
580
|
+
- En errores de `server.listen`.
|
|
581
|
+
- En apagado por señales.
|
|
582
|
+
- En fallos no controlados.
|
|
359
583
|
|
|
360
|
-
|
|
584
|
+
---
|
|
361
585
|
|
|
362
|
-
|
|
586
|
+
### Filtros Avanzados
|
|
363
587
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
588
|
+
```ts
|
|
589
|
+
// Paginación
|
|
590
|
+
const page1 = await logger.getLogs({ limit: 10, offset: 0 });
|
|
591
|
+
const page2 = await logger.getLogs({ limit: 10, offset: 10 });
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
## 🔧 Configuración con Variables de Entorno
|
|
595
|
+
|
|
596
|
+
```env
|
|
597
|
+
###############################################
|
|
598
|
+
# @jmlq/logger – Dummy Environment Variables
|
|
599
|
+
###############################################
|
|
600
|
+
|
|
601
|
+
# ---------------------------------------------
|
|
602
|
+
# Nivel mínimo del logger
|
|
603
|
+
# Valores: TRACE, DEBUG, INFO, WARN, ERROR, FATAL
|
|
604
|
+
# ---------------------------------------------
|
|
605
|
+
LOG_LEVEL=INFO
|
|
606
|
+
|
|
607
|
+
# ---------------------------------------------
|
|
608
|
+
# Protección de PII (datos sensibles)
|
|
609
|
+
# ---------------------------------------------
|
|
610
|
+
LOGGER_PII_ENABLED=true
|
|
611
|
+
LOGGER_PII_INCLUDE_DEFAULTS=false
|
|
612
|
+
|
|
613
|
+
# ---------------------------------------------
|
|
614
|
+
# FileSystem Plugin
|
|
615
|
+
# ---------------------------------------------
|
|
616
|
+
LOGGER_FS_PATH=./logs
|
|
617
|
+
|
|
618
|
+
# ---------------------------------------------
|
|
619
|
+
# MongoDB Plugin
|
|
620
|
+
# ---------------------------------------------
|
|
621
|
+
LOGGER_MONGO_DB_URL=mongodb://demo_user:demo_pass@localhost:27018/?authSource=admin
|
|
622
|
+
LOGGER_MONGO_DB_NAME=logger_demo_db
|
|
623
|
+
LOGGER_MONGO_COLLECTION_NAME=logger_demo_logs
|
|
624
|
+
|
|
625
|
+
# ---------------------------------------------
|
|
626
|
+
# PostgreSQL Plugin
|
|
627
|
+
# ---------------------------------------------
|
|
628
|
+
LOGGER_PG_CONNECTION_STRING=postgres://demo_user:demo_pass@localhost:5436/logger_demo_db
|
|
629
|
+
LOGGER_PG_TABLE_NAME=logger_demo_table
|
|
630
|
+
LOGGER_PG_SCHEMA=public
|
|
631
|
+
|
|
632
|
+
```
|