@jmlq/logger 0.1.0-alpha.1 → 0.1.0-alpha.3

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 ADDED
@@ -0,0 +1,347 @@
1
+ # @jmlq/logger
2
+
3
+ Paquete de `logging` extensible y desacoplado, diseñado con principios de Arquitectura Limpia.
4
+ Permite registrar logs en múltiples destinos (`archivos`, `MongoDB`, `PostgreSQL`) mediante **plugins** y soporta enmascarado de datos sensibles (PII).
5
+
6
+ ---
7
+
8
+ ## 📦 Instalación
9
+
10
+ ```bash
11
+ # Instalar el core
12
+ npm i @jmlq/logger
13
+
14
+ # Instalar plugins opcionales según el backend de persistencia
15
+ npm i @jmlq/logger-plugin-fs
16
+ npm i @jmlq/logger-plugin-mongo
17
+ npm i @jmlq/logger-plugin-postgres
18
+
19
+ ```
20
+
21
+ ### DOCUMENTACION
22
+
23
+ > - [`@jmlq/logger-plugin-fs`](https://www.npmjs.com/package/@jmlq/logger-plugin-fs)
24
+ > - [`@jmlq/logger-plugin-mongo`](https://www.npmjs.com/package/@jmlq/logger-plugin-mongo)
25
+ > - [`@jmlq/logger-plugin-postgres`](https://www.npmjs.com/package/@jmlq/logger-plugin-postgres)
26
+
27
+ ---
28
+
29
+ ## 🧱 Estructura del paquete
30
+
31
+ ```txt
32
+ src/
33
+ composite/
34
+ index.ts # Implementa CompositeDatasource para fan-out de logs a múltiples destinos
35
+ config/
36
+ interfaces/
37
+ index.ts # Define contratos/puertos (ILogDatasource, ILogRepository)
38
+ types/
39
+ index.ts # Tipos y utilidades comunes (filtros de logs, opciones de logger, etc.)
40
+ domain/
41
+ services/
42
+ index.ts # Servicio PiiRedactor: enmascara datos sensibles
43
+ presentation/
44
+ factory/
45
+ index.ts # Factory createLogger: construye la API final del logger
46
+ index.ts # Punto de entrada que re-exporta contratos, utilidades y createLogger
47
+
48
+ ```
49
+
50
+ ## 📝 Resumen rápido
51
+
52
+ > - **composite** → Combinación de múltiples datasources para persistir en paralelo.
53
+ > - **config/interfaces** → Contratos base (ILogDatasource) que los plugins deben implementar.
54
+ > - **config/types** → Tipos auxiliares (`LogLevel`, `GetLogsFilter`, etc.).
55
+ > - **domain/services** → Lógica de dominio (PII redaction).
56
+ > - **presentation/factory** → `createLogger`, expone la API (`trace`, `debug`, `info`, `warn`, `error`, `fatal`).
57
+ > - **index.ts** → Punto de entrada limpio para el consumidor.
58
+
59
+ ---
60
+
61
+ ## 🧩 Configuración
62
+
63
+ ### 🔐 Variables de Entorno (.env)
64
+
65
+ ```ini
66
+ # --- MongoDB ---
67
+ MONGO_URL=mongodb://<usuario>:<password>@localhost:27017
68
+ # Dirección de conexión a MongoDB (usuario/contraseña opcionales)
69
+ MONGO_DB_NAME=my_database
70
+ # Nombre de la base de datos donde se guardarán los logs
71
+
72
+
73
+ # --- PostgreSQL ---
74
+ POSTGRES_URL=postgresql://<usuario>:<password>@localhost:5432/my_database
75
+ # URL de conexión a la base de datos principal
76
+ POSTGRES_DB=my_database
77
+ # Credenciales y nombre de la base de datos
78
+
79
+ # --- FileSystem ---
80
+ LOGGER_FS_PATH=./logs/app.log
81
+ # Ruta local donde se almacenarán los logs en formato JSONL
82
+
83
+ # --- Nivel de logging ---
84
+ LOGGER_LEVEL=debug
85
+ # Nivel mínimo de logs por entorno (dev: debug, prod: warn)
86
+
87
+ # --- PII (enmascarado de datos sensibles) ---
88
+ LOGGER_PII_ENABLED=true
89
+ # Activa/desactiva el redactor de datos sensibles
90
+
91
+ LOGGER_PII_INCLUDE_DEFAULTS=true
92
+ # Incluye los patrones de PII por defecto además de los definidos por el cliente
93
+
94
+ ```
95
+
96
+ ---
97
+
98
+ ### 🚀 Uso del paquete
99
+
100
+ 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**.
101
+
102
+ #### 1) Configuración de `pii.ts`
103
+
104
+ Se definen los patrones propios de la aplicación para enmascarar datos sensibles.
105
+ Estos se combinan con los patrones por defecto del core (`LOGGER_PII_INCLUDE_DEFAULTS=true`).
106
+
107
+ ```ts
108
+ // src/config/logger/pii.ts
109
+
110
+ import type { IPiiPattern } from "@jmlq/logger";
111
+
112
+ // Patrones PII propios del cliente
113
+ export const clientPiiPatterns: IPiiPattern[] = [
114
+ {
115
+ // Ejemplo: cédula ecuatoriana (10 dígitos con validaciones)
116
+ regex: /\b\d{10}\b/g,
117
+ replacement: "[REDACTED_CEDULA]",
118
+ },
119
+ {
120
+ // Ejemplo: token JWT simulado
121
+ regex: /\beyJ[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\b/g,
122
+ replacement: "[REDACTED_JWT]",
123
+ },
124
+ ];
125
+
126
+ // Claves a redactar siempre (aunque no hagan match con regex)
127
+ export const redactKeys = ["password", "secret", "token"];
128
+
129
+ // Claves a preservar (no se enmascaran aunque coincidan con regex)
130
+ export const preserveKeys = ["city"];
131
+ ```
132
+
133
+ #### 2) Inicialización de `logger/index.ts`
134
+
135
+ El logger se ensambla en un archivo central, cargando variables de entorno y adaptadores según disponibilidad:
136
+
137
+ ```tsx
138
+ // src/config/logger/index.ts
139
+
140
+ // Importa las funciones y tipos principales del core del logger
141
+ import { createLogger, LogLevel, CompositeDatasource } from "@jmlq/logger";
142
+
143
+ // Importa los plugins disponibles para persistencia de logs
144
+ import { FileSystemDatasource } from "@jmlq/logger-plugin-fs";
145
+ import { MongoDatasource } from "@jmlq/logger-plugin-mongo";
146
+ import {
147
+ connectPostgres,
148
+ ensurePostgresSchema,
149
+ PostgresDatasource,
150
+ } from "@jmlq/logger-plugin-postgres";
151
+
152
+ // Configuración cargada desde variables de entorno (env-var + dotenv)
153
+ import { envs } from "../plugins/envs.plugin";
154
+
155
+ // Patrones propios de PII definidos en src/config/logger/pii.ts
156
+ import { clientPiiPatterns, redactKeys, preserveKeys } from "./pii";
157
+
158
+ // Utilidades de Node.js para manejo de directorios y archivos
159
+ import { mkdirSync, existsSync } from "node:fs";
160
+ import { dirname } from "node:path";
161
+
162
+ // Cliente oficial de MongoDB
163
+ import { MongoClient } from "mongodb";
164
+
165
+ // Se mantiene una referencia global al cliente de MongoDB para cerrarlo en dispose
166
+ let mongoClient: MongoClient | null = null;
167
+
168
+ // Convierte un string (ej. "debug") al enum LogLevel
169
+ function toMinLevel(level: string): LogLevel {
170
+ switch (level.toLowerCase()) {
171
+ case "trace":
172
+ return LogLevel.TRACE;
173
+ case "debug":
174
+ return LogLevel.DEBUG;
175
+ case "info":
176
+ return LogLevel.INFO;
177
+ case "warn":
178
+ return LogLevel.WARN;
179
+ case "error":
180
+ return LogLevel.ERROR;
181
+ case "fatal":
182
+ return LogLevel.FATAL;
183
+ default:
184
+ return LogLevel.INFO; // Valor por defecto
185
+ }
186
+ }
187
+
188
+ // Asegura que exista el directorio de logs para el datasource de FileSystem
189
+ function ensureDirFor(filePath: string) {
190
+ const dir = dirname(filePath);
191
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
192
+ }
193
+
194
+ // Inicializa el logger ensamblando los datasources configurados en .env
195
+ async function initLogger() {
196
+ const datasources = [];
197
+
198
+ // --- FileSystem ---
199
+ // Si está definido LOGGER_FS_PATH, se usa un datasource local de archivos
200
+ if (envs.logger.LOGGER_FS_PATH) {
201
+ ensureDirFor(envs.logger.LOGGER_FS_PATH);
202
+ datasources.push(
203
+ new FileSystemDatasource({ filePath: envs.logger.LOGGER_FS_PATH })
204
+ );
205
+ }
206
+
207
+ // --- MongoDB ---
208
+ // Si están configurados MONGO_URL y MONGO_DB_NAME, se conecta y usa la colección logs
209
+ if (envs.logger.MONGO_URL && envs.logger.MONGO_DB_NAME) {
210
+ mongoClient = new MongoClient(envs.logger.MONGO_URL);
211
+ await mongoClient.connect();
212
+ const coll = mongoClient.db(envs.logger.MONGO_DB_NAME).collection("logs");
213
+ datasources.push(new MongoDatasource(coll));
214
+ }
215
+
216
+ // --- PostgreSQL ---
217
+ // Si está configurado POSTGRES_URL, se conecta, asegura la tabla y crea el datasource
218
+ if (envs.logger.POSTGRES_URL) {
219
+ await connectPostgres(envs.logger.POSTGRES_URL);
220
+ await ensurePostgresSchema();
221
+ datasources.push(new PostgresDatasource("logs"));
222
+ }
223
+
224
+ // Si hay más de un datasource, se compone con CompositeDatasource (fan-out)
225
+ const datasource =
226
+ datasources.length === 1
227
+ ? datasources[0]
228
+ : new CompositeDatasource(datasources);
229
+
230
+ // Crea y retorna el logger con nivel mínimo y configuración de PII
231
+ return createLogger(datasource, {
232
+ minLevel: toMinLevel(envs.logger.LOGGER_LEVEL),
233
+ pii: {
234
+ enabled: envs.logger.LOGGER_PII_ENABLED,
235
+ includeDefaultPatterns: envs.logger.LOGGER_PII_INCLUDE_DEFAULTS,
236
+ patterns: clientPiiPatterns,
237
+ redactKeys,
238
+ preserveKeys,
239
+ },
240
+ });
241
+ }
242
+
243
+ // Exporta el logger como una Promise porque la inicialización es async
244
+ export const loggerReady = initLogger();
245
+
246
+ // --- Funciones de utilidad ---
247
+
248
+ // Fuerza un flush() de todos los datasources, útil para apagar servicios con logs pendientes
249
+ export async function flushLogs() {
250
+ const logger: any = await loggerReady;
251
+ if (typeof logger.flush === "function") await logger.flush();
252
+ }
253
+
254
+ // Cierra conexiones abiertas (ej. MongoClient) y libera recursos
255
+ export async function disposeLogs() {
256
+ const logger: any = await loggerReady;
257
+ if (typeof logger.dispose === "function") await logger.dispose();
258
+ if (mongoClient) await mongoClient.close();
259
+ }
260
+ ```
261
+
262
+ #### 3) Uso desde cualquier aplicación Node.js
263
+
264
+ En cualquier parte de la app se puede usar el logger así:
265
+
266
+ ```ts
267
+ import { loggerReady } from "./config/logger";
268
+
269
+ async function main() {
270
+ const logger = await loggerReady;
271
+
272
+ await logger.info("Aplicación iniciada", { pid: process.pid });
273
+
274
+ await logger.error("Error en proceso", {
275
+ password: "123456", // será redactado
276
+ city: "Quito", // se preserva
277
+ });
278
+ }
279
+
280
+ main();
281
+ ```
282
+
283
+ ### 📦 Dependencias adicionales en el cliente
284
+
285
+ ```bash
286
+ # Si se va a usar MongoDB
287
+ npm i mongodb@^6.19.0
288
+
289
+ # Si se va a usar PostgreSQL
290
+ npm i pg@^8.16.3
291
+
292
+ ```
293
+
294
+ `mongodb` y `pg` son **peerDependencies**: `@jmlq/logger` no las instala por sí mismo para mantener el core desacoplado.
295
+
296
+ ### 🔎 Notas importantes
297
+
298
+ > - `loggerReady` es una Promise → debe resolverse con `await`.
299
+ > - `flushLogs()` y `disposeLogs()` deben usarse en procesos que cierran conexiones (ej. `SIGINT`, `SIGTERM`).
300
+ > - Los patrones definidos en `pii.ts` se combinan con los patrones por defecto cuando `LOGGER_PII_INCLUDE_DEFAULTS=true`.
301
+ > - El logger puede trabajar con **un único datasource** o con **CompositeDatasource** para múltiples.
302
+
303
+ ---
304
+
305
+ ### 🧪 Escenarios
306
+
307
+ > - **Solo FS** → logs locales en `./logs/app.log`.
308
+ > - **Solo MongoDB** → logs en colección `logs`.
309
+ > - **Solo PostgreSQL** → logs en tabla `logs`.
310
+ > - **Combinado** → fan-out a varios destinos simultáneamente.
311
+ > - **Extensión** → implementar `ILogDatasource`.
312
+
313
+ ---
314
+
315
+ ## 🧯 Troubleshooting
316
+
317
+ > - **No se inicializa ningún datasource** → definir al menos una variable (`LOGGER_FS_PATH`, `MONGO_URL`, `POSTGRES_URL`).
318
+ > - **MongoDB Auth** → incluir `authSource=admin` en la URL si se usan usuarios root.
319
+ > - **Postgres** → ejecutar `ensurePostgresSchema()` para crear tabla logs si no existe.
320
+ > - **Alto volumen de logs** → implementar `flush()` o batching en el datasource.
321
+
322
+ ---
323
+
324
+ ## 🧪 Tests
325
+
326
+ ```ts
327
+ import { createLogger, LogLevel } from "@jmlq/logger";
328
+ import { FileSystemDatasource } from "@jmlq/logger-plugin-fs";
329
+
330
+ test("logger redacta PII en FS", async () => {
331
+ const fsDs = new FileSystemDatasource({ filePath: "./logs/test.log" });
332
+ const logger = createLogger(fsDs, {
333
+ minLevel: LogLevel.DEBUG,
334
+ pii: { enabled: true },
335
+ });
336
+
337
+ await logger.info("Inicio de sesión", { user: "demo", password: "123456" });
338
+
339
+ // Luego verificar que el archivo test.log no contiene la contraseña en claro
340
+ });
341
+ ```
342
+
343
+ ---
344
+
345
+ ## 📄 Licencia
346
+
347
+ MIT © Mauricio Lahuasi
@@ -7,17 +7,48 @@ class CompositeDatasource {
7
7
  this.datasources = datasources;
8
8
  }
9
9
  async save(log) {
10
- await Promise.allSettled(this.datasources.map((ds) => ds.save(log)));
10
+ const results = await Promise.allSettled(this.datasources.map((ds) => ds.save(log)));
11
+ for (const r of results) {
12
+ if (r.status === "rejected") {
13
+ // Evita romper el flujo si un destino falla.
14
+ // Aquí podrías enrutar a "dead-letter", métricas, etc.
15
+ // eslint-disable-next-line no-console
16
+ console.warn("[CompositeDatasource] save failed:", r.reason);
17
+ }
18
+ }
11
19
  }
12
20
  async find(filter = {}) {
13
- const primary = this.datasources[0];
14
- return (await primary?.find?.(filter)) ?? [];
21
+ for (const ds of this.datasources) {
22
+ if (typeof ds.find === "function") {
23
+ try {
24
+ return await ds.find(filter);
25
+ }
26
+ catch (e) {
27
+ // eslint-disable-next-line no-console
28
+ console.warn("[CompositeDatasource] find failed on a datasource:", e);
29
+ // probar siguiente datasource
30
+ }
31
+ }
32
+ }
33
+ return [];
15
34
  }
16
35
  async flush() {
17
- await Promise.all(this.datasources.map((ds) => ds.flush?.() ?? Promise.resolve()));
36
+ const results = await Promise.allSettled(this.datasources.map((ds) => ds.flush?.() ?? Promise.resolve()));
37
+ for (const r of results) {
38
+ if (r.status === "rejected") {
39
+ // eslint-disable-next-line no-console
40
+ console.warn("[CompositeDatasource] flush failed:", r.reason);
41
+ }
42
+ }
18
43
  }
19
44
  async dispose() {
20
- await Promise.all(this.datasources.map((ds) => ds.dispose?.() ?? Promise.resolve()));
45
+ const results = await Promise.allSettled(this.datasources.map((ds) => ds.dispose?.() ?? Promise.resolve()));
46
+ for (const r of results) {
47
+ if (r.status === "rejected") {
48
+ // eslint-disable-next-line no-console
49
+ console.warn("[CompositeDatasource] dispose failed:", r.reason);
50
+ }
51
+ }
21
52
  }
22
53
  }
23
54
  exports.CompositeDatasource = CompositeDatasource;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmlq/logger",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.3",
4
4
  "author": "MLahuasi",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",