@jmlq/logger 0.1.0-alpha.2 → 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 +639 -0
- package/architecture.md +193 -0
- package/assets/mongo-log.png +0 -0
- package/assets/pg-log.png +0 -0
- package/dist/application/factory/index.d.ts +1 -0
- package/dist/{config → application/factory}/index.js +1 -2
- package/dist/application/factory/logger.factory.d.ts +11 -0
- package/dist/application/factory/logger.factory.js +71 -0
- package/dist/application/index.d.ts +2 -0
- package/dist/{presentation → application}/index.js +1 -0
- package/dist/application/types/index.d.ts +1 -0
- package/dist/application/types/index.js +17 -0
- package/dist/application/types/logger-factory-config.type.d.ts +28 -0
- package/dist/{config/interfaces/index.js → application/types/logger-factory-config.type.js} +0 -1
- package/dist/application/use-cases/flush-buffers.use-case.d.ts +6 -0
- package/dist/application/use-cases/flush-buffers.use-case.js +13 -0
- package/dist/application/use-cases/get-logs.use-case.d.ts +8 -0
- package/dist/application/use-cases/get-logs.use-case.js +24 -0
- package/dist/application/use-cases/index.d.ts +3 -0
- package/dist/application/use-cases/index.js +19 -0
- package/dist/application/use-cases/save-log/index.d.ts +1 -0
- package/dist/application/use-cases/save-log/index.js +17 -0
- package/dist/application/use-cases/save-log/save-log.props.d.ts +7 -0
- package/dist/application/use-cases/save-log/save-log.props.js +2 -0
- package/dist/application/use-cases/save-log.use-case.d.ts +8 -0
- package/dist/application/use-cases/save-log.use-case.js +27 -0
- package/dist/domain/index.d.ts +7 -0
- package/dist/domain/index.js +7 -0
- package/dist/domain/model/index.d.ts +3 -0
- package/dist/domain/model/index.js +19 -0
- package/dist/domain/model/log-entry.model.d.ts +8 -0
- package/dist/domain/model/log-entry.model.js +2 -0
- package/dist/domain/model/pii-options.model.d.ts +8 -0
- package/dist/domain/model/pii-options.model.js +2 -0
- package/dist/domain/model/pii-replacement-rule.d.ts +5 -0
- package/dist/domain/model/pii-replacement-rule.js +2 -0
- package/dist/domain/ports/create-logger-options.port.d.ts +7 -0
- package/dist/domain/ports/create-logger-options.port.js +2 -0
- package/dist/domain/ports/index.d.ts +4 -0
- package/dist/domain/ports/index.js +20 -0
- package/dist/domain/ports/log-datasource.port.d.ts +10 -0
- package/dist/domain/ports/log-datasource.port.js +2 -0
- package/dist/domain/ports/logger.port.d.ts +15 -0
- package/dist/domain/ports/logger.port.js +2 -0
- package/dist/domain/ports/pii-redactor.port.d.ts +5 -0
- package/dist/domain/ports/pii-redactor.port.js +2 -0
- package/dist/domain/request/index.d.ts +1 -0
- package/dist/domain/request/index.js +17 -0
- package/dist/domain/request/log-filter.request.d.ts +9 -0
- package/dist/domain/request/log-filter.request.js +2 -0
- package/dist/domain/response/index.d.ts +1 -0
- package/dist/domain/response/index.js +17 -0
- package/dist/domain/response/log.response.d.ts +8 -0
- package/dist/domain/response/log.response.js +2 -0
- package/dist/domain/services/index.d.ts +1 -1
- package/dist/domain/services/index.js +1 -1
- package/dist/domain/services/pii-redactor.service.d.ts +10 -0
- package/dist/domain/services/pii-redactor.service.js +68 -0
- package/dist/domain/types/index.d.ts +1 -0
- package/dist/domain/types/index.js +17 -0
- package/dist/domain/types/log-message.type.d.ts +1 -0
- package/dist/domain/types/log-message.type.js +2 -0
- package/dist/domain/utils/index.d.ts +3 -0
- package/dist/domain/utils/index.js +19 -0
- package/dist/domain/utils/normalize-message.util.d.ts +3 -0
- package/dist/domain/utils/normalize-message.util.js +8 -0
- package/dist/domain/utils/parse-log-level.util.d.ts +2 -0
- package/dist/domain/utils/parse-log-level.util.js +27 -0
- package/dist/domain/utils/pii-regex.util.d.ts +2 -0
- package/dist/domain/utils/pii-regex.util.js +13 -0
- package/dist/domain/value-objects/index.d.ts +1 -0
- package/dist/domain/value-objects/index.js +17 -0
- package/dist/domain/value-objects/log-level.vo.d.ts +8 -0
- package/dist/domain/value-objects/log-level.vo.js +13 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +32 -7
- package/dist/infrastructure/index.d.ts +1 -0
- package/dist/infrastructure/index.js +17 -0
- package/dist/infrastructure/services/datasource.service.d.ts +18 -0
- package/dist/infrastructure/services/datasource.service.js +102 -0
- package/dist/infrastructure/services/index.d.ts +1 -0
- package/dist/infrastructure/services/index.js +17 -0
- package/dist/infrastructure/types/index.d.ts +1 -0
- package/dist/infrastructure/types/index.js +17 -0
- package/dist/infrastructure/types/on-data-source-error.type.d.ts +5 -0
- package/dist/infrastructure/types/on-data-source-error.type.js +2 -0
- package/package.json +37 -11
- package/dist/Composite/index.d.ts +0 -9
- package/dist/Composite/index.js +0 -54
- package/dist/Factory/index.d.ts +0 -5
- package/dist/Factory/index.js +0 -23
- package/dist/config/index.d.ts +0 -2
- package/dist/config/interfaces/index.d.ts +0 -67
- package/dist/config/types/index.d.ts +0 -10
- package/dist/config/types/index.js +0 -13
- package/dist/domain/services/pii-redactor.d.ts +0 -27
- package/dist/domain/services/pii-redactor.js +0 -139
- package/dist/interfaces/index.d.ts +0 -35
- package/dist/interfaces/index.js +0 -13
- package/dist/presentation/factory/index.d.ts +0 -2
- package/dist/presentation/factory/index.js +0 -74
- package/dist/presentation/index.d.ts +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
# @jmlq/logger
|
|
2
|
+
|
|
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
|
+
|
|
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
|
+
|
|
55
|
+
## 📦 Instalación
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Core del logger
|
|
59
|
+
npm install @jmlq/logger
|
|
60
|
+
|
|
61
|
+
# Plugins opcionales (instala según necesites)
|
|
62
|
+
npm install @jmlq/logger-plugin-fs # Para archivos
|
|
63
|
+
npm install @jmlq/logger-plugin-mongo # Para MongoDB
|
|
64
|
+
npm install @jmlq/logger-plugin-postgresql # Para PostgreSQL
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Configuración @jmlq/logger
|
|
70
|
+
|
|
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
|
+
```
|
|
103
|
+
|
|
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
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## 🎯 Características Principales
|
|
471
|
+
|
|
472
|
+
### Niveles de Log
|
|
473
|
+
|
|
474
|
+
- `TRACE` (10) - Información muy detallada
|
|
475
|
+
- `DEBUG` (20) - Información de depuración
|
|
476
|
+
- `INFO` (30) - Información general
|
|
477
|
+
- `WARN` (40) - Advertencias
|
|
478
|
+
- `ERROR` (50) - Errores
|
|
479
|
+
- `FATAL` (60) - Errores críticos
|
|
480
|
+
|
|
481
|
+
### Enmascarado PII Automático
|
|
482
|
+
|
|
483
|
+
El logger detecta y enmascara automáticamente datos sensibles utilizando patrones personalizados:
|
|
484
|
+
|
|
485
|
+
```js
|
|
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
|
+
],
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Por ejemplo, si se tiene la siguiente información:
|
|
506
|
+
|
|
507
|
+
```js
|
|
508
|
+
[
|
|
509
|
+
{
|
|
510
|
+
id: "1",
|
|
511
|
+
name: "John Doe",
|
|
512
|
+
email: "john.doe@example.com",
|
|
513
|
+
card: "1234-5678-9012-3456",
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
id: "2",
|
|
517
|
+
name: "Jane Doe",
|
|
518
|
+
email: "jane.doe@example.com",
|
|
519
|
+
card: "6258-3842-9524-3251",
|
|
520
|
+
},
|
|
521
|
+
];
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Los logs registrarán lo siguiente:
|
|
525
|
+
|
|
526
|
+
> - **Archivo .log** (`@jmlq/logger-plugin-fs`)
|
|
527
|
+
|
|
528
|
+
```js
|
|
529
|
+
{
|
|
530
|
+
"level": 30,
|
|
531
|
+
"message": "Users listed successfully",
|
|
532
|
+
"meta": {
|
|
533
|
+
"count": 2,
|
|
534
|
+
"result": [
|
|
535
|
+
{
|
|
536
|
+
"id": "1",
|
|
537
|
+
"name": "John Doe",
|
|
538
|
+
"email": "***@***",
|
|
539
|
+
"card": "****-****-****-****"
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
"id": "2",
|
|
543
|
+
"name": "Jane Doe",
|
|
544
|
+
"email": "***@***",
|
|
545
|
+
"card": "****-****-****-****"
|
|
546
|
+
}
|
|
547
|
+
]
|
|
548
|
+
},
|
|
549
|
+
"timestamp": 1765400661565,
|
|
550
|
+
"_id": "6939e0556925736129e1008d"
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
> - **MongoDB** (`@jmlq/logger-plugin-mongo`)
|
|
555
|
+
|
|
556
|
+

|
|
557
|
+
|
|
558
|
+
> - **PostgreSQL** (`@jmlq/logger-plugin-postgresql`)
|
|
559
|
+
|
|
560
|
+

|
|
561
|
+
|
|
562
|
+
### Filtros y Consultas
|
|
563
|
+
|
|
564
|
+
#### 1. Obtener todos los logs de nivel WARN o superior (retorna WARN, ERROR y FATAL)
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
const errors = await _req.logger?.getLogs({
|
|
568
|
+
levelMin: parseLogLevel("WARN"),
|
|
569
|
+
});
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Resultado:
|
|
573
|
+
|
|
574
|
+
```ini
|
|
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
|
|
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)"}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
**NOTA**: `parseLogLevel` es un helper para convertir un `string` a `LogLevel`.
|
|
580
|
+
|
|
581
|
+
#### 2. Filtrar por rango de fechas
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
const yesterday = Date.now() - 24 * 60 * 60 * 1000;
|
|
585
|
+
const recentLogs = await _req.logger?.getLogs({
|
|
586
|
+
since: yesterday,
|
|
587
|
+
until: Date.now(),
|
|
588
|
+
});
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
Resultado:
|
|
592
|
+
|
|
593
|
+
```ini
|
|
594
|
+
[2025-12-10T22:21:58.500Z] [30] app.shutdown.begin {"reason":"SIGINT"}
|
|
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)"}
|
|
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":"****-****-****-****"}]}
|
|
597
|
+
[2025-12-10T21:03:32.910Z] [30] http.start {"host":"127.0.0.1","port":3000}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
#### 3. Búsqueda por texto (en el campo mensaje del log)
|
|
601
|
+
|
|
602
|
+
```ts
|
|
603
|
+
const userLogs = await _req.logger?.getLogs({
|
|
604
|
+
query: "Users",
|
|
605
|
+
});
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Resultado:
|
|
609
|
+
|
|
610
|
+
```ini
|
|
611
|
+
[2025-12-10T22:58:09.874Z] [30] Users listed successfully {"count":2,"result":[{"id":"1","card":"****-****-****-****","name":"John Doe","email":"***@***"},{"id":"2","card":"****-****-****-****","name":"Jane Doe","email":"***@***"}]}
|
|
612
|
+
[2025-12-10T22:57:28.483Z] [30] Users listed successfully {"count":2,"result":[{"id":"1","name":"John Doe","email":"***@***","card":"****-****-****-****"},{"id":"2","name":"Jane Doe","email":"***@***","card":"****-****-****-****"}]}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
#### 4. Combinación de filtros
|
|
616
|
+
|
|
617
|
+
```ts
|
|
618
|
+
const logs = await _req.logger?.getLogs({
|
|
619
|
+
levelMin: parseLogLevel("INFO"),
|
|
620
|
+
since: Date.now() - 2 * 60 * 60 * 1000, // Últimas 2 horas
|
|
621
|
+
query: "successfully",
|
|
622
|
+
});
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
Resultado:
|
|
626
|
+
|
|
627
|
+
```ini
|
|
628
|
+
[2025-12-10T23:05:31.791Z] [30] Users listed successfully {"count":2,"result":[{"id":"1","name":"John Doe","email":"***@***","card":"****-****-****-****"},{"id":"2","name":"Jane Doe","email":"***@***","card":"****-****-****-****"}]}
|
|
629
|
+
[2025-12-10T23:05:31.791Z] [30] Users listed successfully {"count":2,"result":[{"id":"1","card":"****-****-****-****","name":"John Doe","email":"***@***"},{"id":"2","card":"****-****-****-****","name":"Jane Doe","email":"***@***"}]}
|
|
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":"****-****-****-****"}]}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
## 📄 Más Información
|
|
634
|
+
|
|
635
|
+
- **[Arquitectura Detallada](./architecture.md)** - Documentación técnica completa
|
|
636
|
+
|
|
637
|
+
## 📝 Licencia
|
|
638
|
+
|
|
639
|
+
MIT © Mauricio Lahuasi
|