@jmlq/logger 0.1.0-alpha.4 → 0.1.0-alpha.6
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/LICENSE +21 -0
- package/README.md +466 -187
- package/package.json +2 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2025 mlahuasi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the “Software”), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npm i @jmlq/logger
|
|
|
14
14
|
# Instalar plugins opcionales según el backend de persistencia
|
|
15
15
|
npm i @jmlq/logger-plugin-fs
|
|
16
16
|
npm i @jmlq/logger-plugin-mongo
|
|
17
|
-
npm i @jmlq/logger-plugin-
|
|
17
|
+
npm i @jmlq/logger-plugin-postgresql
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -30,44 +30,7 @@ npm i pg@^8.16.3
|
|
|
30
30
|
|
|
31
31
|
> - [`@jmlq/logger-plugin-fs`](https://www.npmjs.com/package/@jmlq/logger-plugin-fs)
|
|
32
32
|
> - [`@jmlq/logger-plugin-mongo`](https://www.npmjs.com/package/@jmlq/logger-plugin-mongo)
|
|
33
|
-
> - [`@jmlq/logger-plugin-
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## 🧱 Estructura del paquete
|
|
38
|
-
|
|
39
|
-
### 📝 Resumen rápido
|
|
40
|
-
|
|
41
|
-
> - **`src/domain/`** — Reglas del negocio (sin dependencias de frameworks).
|
|
42
|
-
> > - **`value-objects/`**
|
|
43
|
-
> > > - `log-level.ts` — Define niveles (`TRACE…FATAL`) y `toLogLevel()` para convertir strings a nivel.
|
|
44
|
-
> > - **`types/`**
|
|
45
|
-
> > > - `log.types.ts` — Tipos puros del dominio: `ILog`, `IGetLogsFilter`, `PiiOptions`, etc.
|
|
46
|
-
> > - **`contracts/`**
|
|
47
|
-
> > > - `log.datasource.ts` — Puerto que debe implementar cualquier destino de logs (`save/find/flush/dispose`).
|
|
48
|
-
> > > - `logger.ts` — Contrato del logger público (`ILogger`, `ICreateLoggerOptions`).
|
|
49
|
-
> > > - `pii.ts` — Contrato del redactor de PII.
|
|
50
|
-
> > - **`services/`**
|
|
51
|
-
> > > - `pii-redactor.ts` — Enmascarado de datos sensibles (whitelist/blacklist, patrones, modo profundo).
|
|
52
|
-
|
|
53
|
-
> - **`src/application/`** — Orquestación de casos de uso (no depende de infraestructura).
|
|
54
|
-
> > - **`use-cases/`**
|
|
55
|
-
> > > - `save-log.ts` — Aplica `minLevel` + PII y delega a `datasource.save()`.
|
|
56
|
-
> > > - `get-logs.ts` — Recupera logs con filtros/paginación si el datasource lo soporta.
|
|
57
|
-
> > > - `flush-buffers.ts` — Ejecuta `flush()` en el datasource cuando exista.
|
|
58
|
-
|
|
59
|
-
> - **`src/infrastructure/`** — Adaptadores concretos (tecnología).
|
|
60
|
-
> > - **`adapters/`**
|
|
61
|
-
> > > - `composite.datasource.ts` — Fan-out: envía el log a varios datasources y no falla el todo si uno cae (avisa con `console.warn`).
|
|
62
|
-
|
|
63
|
-
> - **`src/presentation/`** — API pública y fábricas (cara del paquete).
|
|
64
|
-
> > - **`factory/`**
|
|
65
|
-
> > > - `create-logger.ts` — Crea el logger listo para usar (`trace…fatal`, `flush`, `dispose`) conectando casos de uso + PII.
|
|
66
|
-
|
|
67
|
-
> - **`src/index.ts`** — Barrel.
|
|
68
|
-
> > - Re-exporta lo público: `createLogger`, `LogLevel`, tipos/contratos y `CompositeDatasource`.
|
|
69
|
-
|
|
70
|
-
### [](./ARQUITECTURA.md)
|
|
33
|
+
> - [`@jmlq/logger-plugin-postgresql`](https://www.npmjs.com/package/@jmlq/logger-plugin-postgresql)
|
|
71
34
|
|
|
72
35
|
---
|
|
73
36
|
|
|
@@ -76,33 +39,28 @@ npm i pg@^8.16.3
|
|
|
76
39
|
### 🔐 Variables de Entorno (.env)
|
|
77
40
|
|
|
78
41
|
```ini
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
LOGGER_PII_ENABLED=true
|
|
102
|
-
# Activa/desactiva el redactor de datos sensibles
|
|
103
|
-
|
|
104
|
-
LOGGER_PII_INCLUDE_DEFAULTS=true
|
|
105
|
-
# Incluye los patrones de PII por defecto además de los definidos por el cliente
|
|
42
|
+
# @jmlq/logger
|
|
43
|
+
# Nivel mínimo por entorno (dev: debug, prod: warn)
|
|
44
|
+
LOGGER_LEVEL=debug
|
|
45
|
+
# PII (Personally Identifiable Information)
|
|
46
|
+
LOGGER_PII_ENABLED=true
|
|
47
|
+
LOGGER_PII_INCLUDE_DEFAULTS=true
|
|
48
|
+
|
|
49
|
+
# MONGO DB @jmlq/logger-plugin-mongo
|
|
50
|
+
MONGO_URL=mongodb://<user>:<password>@localhost:<port>
|
|
51
|
+
MONGO_DB_NAME=<db-name>
|
|
52
|
+
MONGO_COLLECTION=<collection-name>
|
|
53
|
+
LOGGER_MONGO_RETENTION_DAYS=30
|
|
54
|
+
|
|
55
|
+
# POSTGRESQL @jmlq/logger-plugin-postgresql
|
|
56
|
+
POSTGRES_URL="postgresql://mlahuasi:123456@localhost:5432/NOC"
|
|
57
|
+
POSTGRES_DB=<db-name>
|
|
58
|
+
POSTGRES_SCHEMA=public
|
|
59
|
+
POSTGRES_TABLE=<table-name>
|
|
60
|
+
LOGGER_PG_RETENTION_DAYS=30
|
|
61
|
+
|
|
62
|
+
# FILESYSTEM @jmlq/logger-plugin-fs
|
|
63
|
+
LOGGER_FS_PATH=./logs
|
|
106
64
|
|
|
107
65
|
```
|
|
108
66
|
|
|
@@ -110,77 +68,132 @@ LOGGER_PII_INCLUDE_DEFAULTS=true
|
|
|
110
68
|
|
|
111
69
|
### 🚀 Uso del paquete
|
|
112
70
|
|
|
113
|
-
|
|
71
|
+
#### 1) Estructura recomendada
|
|
114
72
|
|
|
115
|
-
|
|
73
|
+
```
|
|
74
|
+
src/
|
|
75
|
+
├─ infrastructure/
|
|
76
|
+
│ ├─ logger/
|
|
77
|
+
│ │ ├─ adapters/
|
|
78
|
+
│ │ │ ├─ fs.adapter.ts
|
|
79
|
+
│ │ │ ├─ mongo.adapter.ts
|
|
80
|
+
│ │ │ └─ postgresql.adapter.ts
|
|
81
|
+
│ │ ├─ settings/
|
|
82
|
+
│ │ │ ├─ pii.settings.ts
|
|
83
|
+
│ │ │ └─ loglevel.settings.ts
|
|
84
|
+
│ │ └─ bootstrap.ts # LoggerBootstrap (orquesta adapters + PII)
|
|
85
|
+
│ └─ plugins/
|
|
86
|
+
│ ├─ env.plugin.ts
|
|
87
|
+
│ └─ index.ts
|
|
88
|
+
├─ config/
|
|
89
|
+
│ └─ logger/
|
|
90
|
+
│ └─ index.ts # singleton global: loggerReady / flush / dispose
|
|
91
|
+
└─ presentation/
|
|
92
|
+
└─ server.ts # uso del logger en Express
|
|
116
93
|
|
|
117
|
-
|
|
118
|
-
Estos se combinan con los patrones por defecto del core (`LOGGER_PII_INCLUDE_DEFAULTS=true`).
|
|
94
|
+
```
|
|
119
95
|
|
|
120
|
-
|
|
121
|
-
// src/config/logger/pii.ts
|
|
96
|
+
#### 2) Settings
|
|
122
97
|
|
|
123
|
-
|
|
98
|
+
#### 2.1) pii.settings.ts (patrones PII + merge)
|
|
124
99
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
100
|
+
```ts
|
|
101
|
+
//Se definine reglas de redacción PII (buscar-y-reemplazar con regex) que el logger aplica antes de persistir/emitir un log.
|
|
102
|
+
|
|
103
|
+
export type PiiReplacement = {
|
|
104
|
+
pattern: string;
|
|
105
|
+
replaceWith: string;
|
|
106
|
+
flags?: string;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export interface PiiConfig {
|
|
110
|
+
enabled: boolean;
|
|
111
|
+
whitelistKeys?: string[];
|
|
112
|
+
blacklistKeys?: string[];
|
|
113
|
+
patterns?: PiiReplacement[];
|
|
114
|
+
deep?: boolean;
|
|
115
|
+
includeDefaults?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// patrones “de fábrica” (del sistema):
|
|
119
|
+
export const DEFAULT_PII_PATTERNS: PiiReplacement[] = [
|
|
132
120
|
{
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
121
|
+
pattern: String.raw`[^@\n\r ]+@[^@\n\r ]+`,
|
|
122
|
+
replaceWith: "[EMAIL]",
|
|
123
|
+
flags: "g",
|
|
136
124
|
},
|
|
137
125
|
];
|
|
138
126
|
|
|
139
|
-
//
|
|
140
|
-
export const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
export const preserveKeys = ["city"];
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
#### 2) Inicialización de `logger/index.ts`
|
|
147
|
-
|
|
148
|
-
El logger se ensambla en un archivo central, cargando variables de entorno y adaptadores según disponibilidad:
|
|
149
|
-
|
|
150
|
-
```tsx
|
|
151
|
-
// src/config/logger/index.ts
|
|
152
|
-
|
|
153
|
-
// Importa las funciones y tipos principales del core del logger
|
|
154
|
-
import { createLogger, LogLevel, CompositeDatasource } from "@jmlq/logger";
|
|
155
|
-
|
|
156
|
-
// Importa los plugins disponibles para persistencia de logs
|
|
157
|
-
import { FileSystemDatasource } from "@jmlq/logger-plugin-fs";
|
|
158
|
-
import { MongoDatasource } from "@jmlq/logger-plugin-mongo";
|
|
159
|
-
import {
|
|
160
|
-
connectPostgres,
|
|
161
|
-
ensurePostgresSchema,
|
|
162
|
-
PostgresDatasource,
|
|
163
|
-
} from "@jmlq/logger-plugin-postgres";
|
|
164
|
-
|
|
165
|
-
// Configuración cargada desde variables de entorno (env-var + dotenv)
|
|
166
|
-
import { envs } from "../plugins/envs.plugin";
|
|
127
|
+
// patrones “del cliente/proyecto”:
|
|
128
|
+
export const clientPiiPatterns: PiiReplacement[] = [
|
|
129
|
+
{ pattern: String.raw`\b\d{10}\b`, flags: "g", replaceWith: "[EC_DNI]" },
|
|
130
|
+
];
|
|
167
131
|
|
|
168
|
-
//
|
|
169
|
-
|
|
132
|
+
// lista negra de nombres de clave que se siempre se ocultan
|
|
133
|
+
export const redactKeys: string[] = ["password", "secret"];
|
|
134
|
+
// lista blanca de nombres de clave que no se deben ocultar por clave
|
|
135
|
+
export const preserveKeys: string[] = ["city"];
|
|
136
|
+
|
|
137
|
+
// Helpers
|
|
138
|
+
// 1. Filtra valores no válidos: elimina null, undefined y "" (cadena vacía).
|
|
139
|
+
// 2. Elimina duplicados usando Set.
|
|
140
|
+
// 3. Conserva el primero de cada valor repetido (porque Set guarda la primera aparición).
|
|
141
|
+
export function dedupeStrings(arr: Array<string | undefined | null>): string[] {
|
|
142
|
+
return [...new Set(arr.filter((x): x is string => !!x && x.length > 0))];
|
|
143
|
+
}
|
|
170
144
|
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
145
|
+
// 1. Construye una clave de identidad por patrón: pattern + "__" + replaceWith + "__" + (flags || "").
|
|
146
|
+
// 2. Inserta cada elemento en un `Map` usando esa clave. Si la clave ya existe, sobrescribe el anterior (es decir, gana el último).
|
|
147
|
+
// 3. Devuelve los valores únicos del `Map`.
|
|
148
|
+
export function dedupePatterns(arr: PiiReplacement[]): PiiReplacement[] {
|
|
149
|
+
const m = new Map<string, PiiReplacement>();
|
|
150
|
+
for (const p of arr)
|
|
151
|
+
m.set(`${p.pattern}__${p.replaceWith}__${p.flags ?? ""}`, p);
|
|
152
|
+
return [...m.values()];
|
|
153
|
+
}
|
|
174
154
|
|
|
175
|
-
//
|
|
176
|
-
|
|
155
|
+
// Builder unificado
|
|
156
|
+
export function buildPiiConfig(
|
|
157
|
+
opts?: PiiConfig
|
|
158
|
+
): Required<Omit<PiiConfig, "includeDefaults">> {
|
|
159
|
+
const includeDefaults = opts?.includeDefaults ?? true;
|
|
160
|
+
|
|
161
|
+
const patterns = dedupePatterns([
|
|
162
|
+
...(includeDefaults ? DEFAULT_PII_PATTERNS : []),
|
|
163
|
+
...clientPiiPatterns,
|
|
164
|
+
...(opts?.patterns ?? []),
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const whitelistKeys = dedupeStrings([
|
|
168
|
+
...(opts?.whitelistKeys ?? []),
|
|
169
|
+
...preserveKeys,
|
|
170
|
+
]);
|
|
171
|
+
const blacklistKeys = dedupeStrings([
|
|
172
|
+
...(opts?.blacklistKeys ?? []),
|
|
173
|
+
...redactKeys,
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
enabled: !!opts?.enabled,
|
|
178
|
+
whitelistKeys,
|
|
179
|
+
blacklistKeys,
|
|
180
|
+
patterns,
|
|
181
|
+
deep: opts?.deep ?? true,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
```
|
|
177
185
|
|
|
178
|
-
|
|
179
|
-
let mongoClient: MongoClient | null = null;
|
|
186
|
+
##### 2.2) loglevel.settings.ts (normalización de nivel)
|
|
180
187
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
```ts
|
|
189
|
+
// src/infrastructure/logger/settings/loglevel.settings.ts
|
|
190
|
+
import { LogLevel } from "@jmlq/logger";
|
|
191
|
+
|
|
192
|
+
export function toMinLevel(
|
|
193
|
+
level: LogLevel | keyof typeof LogLevel | string
|
|
194
|
+
): LogLevel {
|
|
195
|
+
if (typeof level === "number") return level as LogLevel;
|
|
196
|
+
switch (String(level || "debug").toLowerCase()) {
|
|
184
197
|
case "trace":
|
|
185
198
|
return LogLevel.TRACE;
|
|
186
199
|
case "debug":
|
|
@@ -194,103 +207,365 @@ function toMinLevel(level: string): LogLevel {
|
|
|
194
207
|
case "fatal":
|
|
195
208
|
return LogLevel.FATAL;
|
|
196
209
|
default:
|
|
197
|
-
return LogLevel.
|
|
210
|
+
return LogLevel.DEBUG;
|
|
198
211
|
}
|
|
199
212
|
}
|
|
213
|
+
```
|
|
200
214
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
215
|
+
#### 3) Adapters (capa infrastructure)
|
|
216
|
+
|
|
217
|
+
Todos `devuelven`/`encapsulan` un `ILogDatasource` para evitar acoplar la app a clases concretas.
|
|
218
|
+
|
|
219
|
+
**NOTA**: Solo se implementan los que se necesiten, por ejemplo:
|
|
220
|
+
|
|
221
|
+
> - Si se necesita generar logs en archivos se crea adapter para `FS`.
|
|
222
|
+
> - Si se necesita guardar logs en una base de datos no relacional se crea adapter para `mongo`.
|
|
223
|
+
> - Si se necesita guardar logs en una base de datos relacional se crea adapter para `postgresql`.
|
|
224
|
+
> - También se pueden combinar.
|
|
225
|
+
> - Se debe implementar al menos un adapter.
|
|
226
|
+
|
|
227
|
+
##### 3.1) FS (`fs.adapter.ts`)
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { createFsDatasource } from "@jmlq/logger-plugin-fs";
|
|
231
|
+
import type { ILogDatasource } from "@jmlq/logger";
|
|
232
|
+
|
|
233
|
+
export interface IFsProps {
|
|
234
|
+
basePath: string;
|
|
235
|
+
fileNamePattern: string;
|
|
236
|
+
rotationPolicy: { by: "none" | "day" | "size"; maxSizeMB?: number };
|
|
205
237
|
}
|
|
206
238
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
239
|
+
export class FsAdapter {
|
|
240
|
+
private constructor(private readonly ds: ILogDatasource) {}
|
|
241
|
+
static create(opts: IFsProps): FsAdapter | undefined {
|
|
242
|
+
try {
|
|
243
|
+
const ds = createFsDatasource({
|
|
244
|
+
basePath: opts.basePath,
|
|
245
|
+
mkdir: true,
|
|
246
|
+
fileNamePattern: opts.fileNamePattern,
|
|
247
|
+
rotation: opts.rotationPolicy,
|
|
248
|
+
onRotate: (oldP, newP) =>
|
|
249
|
+
console.log("[fs] rotated:", oldP, "->", newP),
|
|
250
|
+
onError: (e) => console.error("[fs] error:", e),
|
|
251
|
+
});
|
|
252
|
+
console.log("[logger] Conectado a FS para logs");
|
|
253
|
+
return new FsAdapter(ds);
|
|
254
|
+
} catch (e: any) {
|
|
255
|
+
console.warn("[logger] FS deshabilitado:", e?.message ?? e);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
get datasource(): ILogDatasource {
|
|
259
|
+
return this.ds;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
210
263
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
264
|
+
##### 3.2) Mongo (`mongo.adapter.ts`)
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import type { ILogDatasource } from "@jmlq/logger";
|
|
268
|
+
import { createMongoInfra, MongoDatasource } from "@jmlq/logger-plugin-mongo";
|
|
269
|
+
|
|
270
|
+
export interface IMongoProps {
|
|
271
|
+
url: string;
|
|
272
|
+
dbName: string;
|
|
273
|
+
collectionName?: string;
|
|
274
|
+
retentionDays?: number | null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export class MongoAdapter {
|
|
278
|
+
private constructor(private readonly ds: ILogDatasource) {}
|
|
279
|
+
static async create(opts: IMongoProps): Promise<MongoAdapter | undefined> {
|
|
280
|
+
try {
|
|
281
|
+
const infra = await createMongoInfra({
|
|
282
|
+
url: opts.url,
|
|
283
|
+
dbName: opts.dbName,
|
|
284
|
+
collectionName: opts.collectionName ?? "logs",
|
|
285
|
+
createIfMissing: true,
|
|
286
|
+
ensureIndexes: true,
|
|
287
|
+
retentionDays: opts.retentionDays ?? 0,
|
|
288
|
+
extraIndexes: [{ key: { "meta.userId": 1 } }],
|
|
289
|
+
});
|
|
290
|
+
return new MongoAdapter(new MongoDatasource(infra.collection));
|
|
291
|
+
} catch (e: any) {
|
|
292
|
+
console.warn("[logger] Mongo deshabilitado:", e?.message ?? e);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
get datasource(): ILogDatasource {
|
|
296
|
+
return this.ds;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
##### 3.2) Postgresql (`postgresql.adapter.ts`)
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import type { ILogDatasource } from "@jmlq/logger";
|
|
305
|
+
import { createPostgresDatasource } from "@jmlq/logger-plugin-postgresql";
|
|
306
|
+
|
|
307
|
+
export interface IPostgresqlProps {
|
|
308
|
+
url: string;
|
|
309
|
+
schema: string;
|
|
310
|
+
table?: string;
|
|
311
|
+
retentionDays?: number | null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export class PostgresqlAdapter {
|
|
315
|
+
private constructor(private readonly ds: ILogDatasource) {}
|
|
316
|
+
static async create(
|
|
317
|
+
opts: IPostgresqlProps
|
|
318
|
+
): Promise<PostgresqlAdapter | undefined> {
|
|
319
|
+
try {
|
|
320
|
+
const ps = await createPostgresDatasource({
|
|
321
|
+
connectionString: opts.url,
|
|
322
|
+
schema: opts.schema,
|
|
323
|
+
table: opts.table ?? "logs",
|
|
324
|
+
createIfMissing: true,
|
|
325
|
+
retentionDays: opts.retentionDays ?? 0,
|
|
326
|
+
});
|
|
327
|
+
console.log("[logger] Conectado a PostgreSQL para logs");
|
|
328
|
+
return new PostgresqlAdapter(ps);
|
|
329
|
+
} catch (e: any) {
|
|
330
|
+
console.warn("[logger] Postgres deshabilitado:", e?.message ?? e);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
get datasource(): ILogDatasource {
|
|
334
|
+
return this.ds;
|
|
218
335
|
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### 4) `LoggerBootstrap` (orquestador de adapters + PII)
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
// src/infrastructure/logger/bootstrap.ts
|
|
343
|
+
import {
|
|
344
|
+
createLogger,
|
|
345
|
+
CompositeDatasource,
|
|
346
|
+
type ILogDatasource,
|
|
347
|
+
} from "@jmlq/logger";
|
|
348
|
+
import { buildPiiConfig } from "./settings/pii.settings";
|
|
349
|
+
import { toMinLevel } from "./settings/loglevel.settings";
|
|
350
|
+
// NOTA: Opcionales depende de las necesidades del cliente
|
|
351
|
+
import { FsAdapter, type IFsProps } from "./adapters/fs.adapter";
|
|
352
|
+
import { MongoAdapter, type IMongoProps } from "./adapters/mongo.adapter";
|
|
353
|
+
import {
|
|
354
|
+
PostgresqlAdapter,
|
|
355
|
+
type IPostgresqlProps,
|
|
356
|
+
} from "./adapters/postgresql.adapter";
|
|
357
|
+
|
|
358
|
+
export interface LoggerBootstrapOptions {
|
|
359
|
+
minLevel: string | number;
|
|
360
|
+
pii?: {
|
|
361
|
+
enabled?: boolean;
|
|
362
|
+
whitelistKeys?: string[];
|
|
363
|
+
blacklistKeys?: string[];
|
|
364
|
+
patterns?: any[];
|
|
365
|
+
deep?: boolean;
|
|
366
|
+
includeDefaults?: boolean;
|
|
367
|
+
};
|
|
368
|
+
adapters?: {
|
|
369
|
+
// NOTA: Opcionales depende de las necesidades del cliente
|
|
370
|
+
fs?: IFsProps;
|
|
371
|
+
mongo?: IMongoProps;
|
|
372
|
+
postgres?: IPostgresqlProps;
|
|
373
|
+
};
|
|
374
|
+
}
|
|
219
375
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
376
|
+
export class LoggerBootstrap {
|
|
377
|
+
private constructor(
|
|
378
|
+
private readonly _logger: ReturnType<typeof createLogger>,
|
|
379
|
+
private readonly _ds: ILogDatasource
|
|
380
|
+
) {}
|
|
381
|
+
|
|
382
|
+
static async create(opts: LoggerBootstrapOptions): Promise<LoggerBootstrap> {
|
|
383
|
+
const dsList: ILogDatasource[] = [];
|
|
384
|
+
|
|
385
|
+
// NOTA: Opcionales depende de las necesidades del cliente
|
|
386
|
+
if (opts.adapters?.fs) {
|
|
387
|
+
const fs = FsAdapter.create(opts.adapters.fs);
|
|
388
|
+
if (fs) dsList.push(fs.datasource);
|
|
389
|
+
}
|
|
390
|
+
if (opts.adapters?.mongo) {
|
|
391
|
+
const mg = await MongoAdapter.create(opts.adapters.mongo);
|
|
392
|
+
if (mg) dsList.push(mg.datasource);
|
|
393
|
+
}
|
|
394
|
+
if (opts.adapters?.postgres) {
|
|
395
|
+
const pg = await PostgresqlAdapter.create(opts.adapters.postgres);
|
|
396
|
+
if (pg) dsList.push(pg.datasource);
|
|
397
|
+
}
|
|
398
|
+
//----
|
|
399
|
+
|
|
400
|
+
if (dsList.length === 0)
|
|
401
|
+
throw new Error("[logger] No hay datasources válidos.");
|
|
402
|
+
const datasource =
|
|
403
|
+
dsList.length === 1 ? dsList[0] : new CompositeDatasource(dsList);
|
|
404
|
+
|
|
405
|
+
const pii = buildPiiConfig({
|
|
406
|
+
enabled: opts.pii?.enabled ?? false,
|
|
407
|
+
includeDefaults: opts.pii?.includeDefaults ?? true,
|
|
408
|
+
whitelistKeys: opts.pii?.whitelistKeys,
|
|
409
|
+
blacklistKeys: opts.pii?.blacklistKeys,
|
|
410
|
+
patterns: opts.pii?.patterns,
|
|
411
|
+
deep: opts.pii?.deep ?? true,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const logger = createLogger(datasource, {
|
|
415
|
+
minLevel: toMinLevel(opts.minLevel),
|
|
416
|
+
pii,
|
|
417
|
+
});
|
|
418
|
+
return new LoggerBootstrap(logger, datasource);
|
|
227
419
|
}
|
|
228
420
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
421
|
+
get logger() {
|
|
422
|
+
return this._logger;
|
|
423
|
+
}
|
|
424
|
+
async flush() {
|
|
425
|
+
const any = this._logger as any;
|
|
426
|
+
if (typeof any.flush === "function") await any.flush();
|
|
427
|
+
}
|
|
428
|
+
async dispose() {
|
|
429
|
+
const any = this._logger as any;
|
|
430
|
+
if (typeof any.dispose === "function") await any.dispose();
|
|
235
431
|
}
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
#### 5) Configuración global del logger (singleton)
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
// src/config/logger/index.ts
|
|
439
|
+
import { LoggerBootstrap } from "../../infrastructure/logger/bootstrap";
|
|
440
|
+
import { envs } from "../../infrastructure/plugins";
|
|
236
441
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
? datasources[0]
|
|
241
|
-
: new CompositeDatasource(datasources);
|
|
442
|
+
declare global {
|
|
443
|
+
var __LOGGER_BOOT__: Promise<LoggerBootstrap> | undefined;
|
|
444
|
+
}
|
|
242
445
|
|
|
243
|
-
|
|
244
|
-
return
|
|
245
|
-
minLevel:
|
|
446
|
+
async function init() {
|
|
447
|
+
return LoggerBootstrap.create({
|
|
448
|
+
minLevel: envs.logger.LOGGER_LEVEL ?? "debug",
|
|
246
449
|
pii: {
|
|
247
450
|
enabled: envs.logger.LOGGER_PII_ENABLED,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
451
|
+
includeDefaults: envs.logger.LOGGER_PII_INCLUDE_DEFAULTS,
|
|
452
|
+
deep: true,
|
|
453
|
+
},
|
|
454
|
+
adapters: {
|
|
455
|
+
// NOTA: Opcionales depende de las necesidades del cliente
|
|
456
|
+
fs: envs.logger.LOGGER_FS_PATH
|
|
457
|
+
? {
|
|
458
|
+
basePath: envs.logger.LOGGER_FS_PATH,
|
|
459
|
+
fileNamePattern: "app-{yyyy}{MM}{dd}.log",
|
|
460
|
+
rotationPolicy: { by: "day" },
|
|
461
|
+
}
|
|
462
|
+
: undefined,
|
|
463
|
+
mongo: envs.logger.MONGO_URL
|
|
464
|
+
? {
|
|
465
|
+
url: envs.logger.MONGO_URL!,
|
|
466
|
+
dbName: envs.logger.MONGO_DB_NAME!,
|
|
467
|
+
collectionName: envs.logger.MONGO_COLLECTION ?? "logs",
|
|
468
|
+
retentionDays: Number(envs.logger.LOGGER_MONGO_RETENTION_DAYS) || 0,
|
|
469
|
+
}
|
|
470
|
+
: undefined,
|
|
471
|
+
postgres: envs.logger.POSTGRES_URL
|
|
472
|
+
? {
|
|
473
|
+
url: envs.logger.POSTGRES_URL!,
|
|
474
|
+
schema: envs.logger.POSTGRES_SCHEMA ?? "public",
|
|
475
|
+
table: envs.logger.POSTGRES_TABLE ?? "logs",
|
|
476
|
+
retentionDays: Number(envs.logger.LOGGER_PG_RETENTION_DAYS) || 0,
|
|
477
|
+
}
|
|
478
|
+
: undefined,
|
|
479
|
+
// ---
|
|
252
480
|
},
|
|
253
481
|
});
|
|
254
482
|
}
|
|
255
483
|
|
|
256
|
-
//
|
|
257
|
-
|
|
484
|
+
// 1. Es una promesa singleton de LoggerBootstrap
|
|
485
|
+
// 2. usa el operador nullish-coalescing (??) para: Reusar globalThis.__LOGGER_BOOT__ si ya existe. En caso contrario crea y memoriza (= init()) la promesa si no existe aún.
|
|
486
|
+
// 3. Garantiza una sola inicialización global del sistema de logging (adapters, datasources, PII, etc.) aunque el módulo se importe múltiples veces
|
|
487
|
+
export const bootReady: Promise<LoggerBootstrap> =
|
|
488
|
+
globalThis.__LOGGER_BOOT__ ?? (globalThis.__LOGGER_BOOT__ = init());
|
|
258
489
|
|
|
259
|
-
//
|
|
490
|
+
// 1. Es una promesa que resuelve directamente al logger
|
|
491
|
+
// 2. Hace un map de la promesa anterior: bootReady.then(b => b.logger).
|
|
492
|
+
export const loggerReady = bootReady.then((b) => b.logger);
|
|
260
493
|
|
|
261
|
-
//
|
|
494
|
+
// 1. Espera a bootReady y llama boot.flush(), que a su vez pide al logger/datasources que vacíen buffers pendientes (útil antes de apagar el proceso o en tests).
|
|
262
495
|
export async function flushLogs() {
|
|
263
|
-
const
|
|
264
|
-
|
|
496
|
+
const boot = await bootReady;
|
|
497
|
+
await boot.flush();
|
|
265
498
|
}
|
|
266
499
|
|
|
267
|
-
//
|
|
500
|
+
// 1. Espera a bootReady y llama boot.dispose(), que cierra recursos (conexiones a Mongo/Postgres, file handles, etc.)
|
|
268
501
|
export async function disposeLogs() {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
if (mongoClient) await mongoClient.close();
|
|
502
|
+
const boot = await bootReady;
|
|
503
|
+
await boot.dispose();
|
|
272
504
|
}
|
|
273
505
|
```
|
|
274
506
|
|
|
275
|
-
####
|
|
276
|
-
|
|
277
|
-
En cualquier parte de la app se puede usar el logger así:
|
|
507
|
+
#### 6) Uso en la aplicación (Express)
|
|
278
508
|
|
|
279
509
|
```ts
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
510
|
+
// src/presentation/server.ts
|
|
511
|
+
import express, { Request, Response, NextFunction } from "express";
|
|
512
|
+
import { loggerReady } from "../config/logger";
|
|
513
|
+
|
|
514
|
+
function attachLogger() {
|
|
515
|
+
const boot = loggerReady;
|
|
516
|
+
return async (req: Request, _res: Response, next: NextFunction) => {
|
|
517
|
+
// @ts-expect-error: extensión ad-hoc
|
|
518
|
+
req.logger = await boot;
|
|
519
|
+
// @ts-expect-error: idem
|
|
520
|
+
req.requestId = (req.headers["x-request-id"] as string) ?? randomUUID();
|
|
521
|
+
next();
|
|
522
|
+
};
|
|
523
|
+
}
|
|
286
524
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
525
|
+
export function createServer() {
|
|
526
|
+
const app = express();
|
|
527
|
+
app.use(express.json());
|
|
528
|
+
app.use(attachLogger());
|
|
529
|
+
|
|
530
|
+
app.get("/health", (_req, res) =>
|
|
531
|
+
res
|
|
532
|
+
.status(200)
|
|
533
|
+
.json({ ok: true, service: "ml-dev-test", timestamp: Date.now() })
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
app.get("/debug/log-demo", async (req, res) => {
|
|
537
|
+
// @ts-expect-error: logger agregado por attachLogger
|
|
538
|
+
const logger = req.logger;
|
|
539
|
+
await logger.info(
|
|
540
|
+
"Pago con tarjeta 4111 1111 1111 1111 del email demo@correo.com",
|
|
541
|
+
{
|
|
542
|
+
password: "abc123",
|
|
543
|
+
phone: "0987654321",
|
|
544
|
+
city: "Quito",
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
res.json({ ok: true });
|
|
290
548
|
});
|
|
291
|
-
}
|
|
292
549
|
|
|
293
|
-
|
|
550
|
+
// Error handler con logging
|
|
551
|
+
app.use(
|
|
552
|
+
async (err: any, req: Request, res: Response, _next: NextFunction) => {
|
|
553
|
+
const status = err?.statusCode ?? 500;
|
|
554
|
+
// @ts-expect-error
|
|
555
|
+
const logger = req.logger ?? (await loggerReady);
|
|
556
|
+
await logger.error("http_error", {
|
|
557
|
+
message: err?.message,
|
|
558
|
+
stack: err?.stack,
|
|
559
|
+
status,
|
|
560
|
+
});
|
|
561
|
+
res
|
|
562
|
+
.status(status)
|
|
563
|
+
.json({ error: err?.message ?? "Internal Server Error" });
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
return app;
|
|
568
|
+
}
|
|
294
569
|
```
|
|
295
570
|
|
|
296
571
|
### 🔎 Notas importantes
|
|
@@ -342,6 +617,10 @@ test("logger redacta PII en FS", async () => {
|
|
|
342
617
|
|
|
343
618
|
---
|
|
344
619
|
|
|
620
|
+
### [LEER MAS...](./ARQUITECTURA.md)
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
345
624
|
## 📄 Licencia
|
|
346
625
|
|
|
347
626
|
MIT © Mauricio Lahuasi
|