@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 +347 -0
- package/dist/Composite/index.js +36 -5
- package/package.json +1 -1
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
|
package/dist/Composite/index.js
CHANGED
|
@@ -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
|
|
14
|
-
|
|
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.
|
|
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.
|
|
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;
|