@jmlq/logger-plugin-fs 0.1.0-alpha.10 → 0.1.0-alpha.11

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 CHANGED
@@ -1,49 +1,454 @@
1
1
  # @jmlq/logger-plugin-fs
2
2
 
3
+ ## Introducción técnica
4
+
3
5
  Plugin de **sistema de archivos** para [`@jmlq/logger`](https://www.npmjs.com/package/@jmlq/logger). Permite persistir logs en archivos con soporte para rotación automática, manejo de backpressure y configuración flexible de formato y estructura de archivos.
4
6
 
7
+ El plugin se encarga de:
8
+
9
+ - Persistencia de logs en File System
10
+ - Rotación automática de archivos por fecha o tamaño
11
+ - Búsqueda y filtrado de logs históricos
12
+ - Gestión eficiente de streams de escritura
13
+
14
+ ---
15
+
16
+ ## Componentes principales expuestos
17
+
18
+ ### Factory
19
+
20
+ - **[`createFsDatasource`](src/application/factory/create-fs-datasource.factory.ts)**
21
+ Factory de alto nivel que construye un `ILogDatasource` compatible con `@jmlq/logger`, resolviendo internamente:
22
+
23
+ - Cliente File System con adaptadores específicos de Node.js
24
+ - Casos de uso para persistencia, rotación y búsqueda
25
+ - Adaptadores de infraestructura para operaciones de archivos
26
+
27
+ > Es el **único punto recomendado de entrada** para consumidores del paquete.
28
+
29
+ ---
30
+
31
+ ### Tipos de configuración
32
+
33
+ - **[`IFilesystemDatasourceOptions`](src/application/types/filesystem-datasource-options.type.ts)**
34
+ Define la configuración necesaria para crear el datasource de File System:
35
+ - `basePath`: Directorio base donde se almacenarán los logs
36
+ - `fileNamePattern`: Patrón de nombres con tokens de fecha
37
+ - `rotation`: Configuración de rotación automática
38
+ - `mkdir`: Creación automática de directorios
39
+ - `onRotate`: Callback ejecutado al rotar archivos
40
+ - `onError`: Manejador de errores personalizado
41
+
42
+ ---
43
+
44
+ ## Contratos (Types / Ports)
45
+
46
+ Estos tipos existen para **desacoplar el dominio y la aplicación del File System**.
47
+ Son útiles para testing, extensiones avanzadas o integraciones personalizadas.
48
+
49
+ - **[`IFileSystemRotationConfig`](src/infrastructure/filesystem/types/filesystem-rotation.type.ts)**: Define estrategias de rotación (día, tamaño, ninguna)
50
+ - **[`FsRotationBy`](src/domain/types/fs-rotation-by.type.ts)**: Enum con tipos de rotación disponibles
51
+ - **[`FilePath`](src/domain/value-objects/file-path.vo.ts)**: Value Object para rutas de archivos con validaciones
52
+ - **[`FileSize`](src/domain/value-objects/file-size.vo.ts)**: Value Object para tamaños de archivos con conversiones
53
+ - **[`FileRotationPolicy`](src/domain/value-objects/file-rotation-policy.vo.ts)**: Encapsula la política de rotación configurada
54
+ - **[`FileNamePattern`](src/domain/value-objects/file-name-pattern.vo.ts)**: Maneja patrones de nombres con tokens de fecha
55
+
56
+ ---
57
+
5
58
  ## 📦 Instalación
6
59
 
7
60
  ```bash
8
61
  npm install @jmlq/logger @jmlq/logger-plugin-fs
9
62
  ```
10
63
 
11
- ### Dependencias
64
+ - `@jmlq/logger` >= 0.1.0-alpha.20
12
65
 
13
- El plugin requiere como peer dependency:
66
+ ---
14
67
 
15
- - `@jmlq/logger` >= 0.1.0-alpha.12
68
+ ## Configuración @jmlq/logger-plugin-fs
16
69
 
17
- > **Nota:** Este plugin requiere [`@jmlq/logger`](https://www.npmjs.com/package/@jmlq/logger) como dependencia principal.
70
+ Se recomienda implementar en la capa `infrastructure` de la API REST que usa [`@jmlq/logger`](https://www.npmjs.com/package/@jmlq/logger) y [`@jmlq/logger-plugin-fs`](https://www.npmjs.com/package/@jmlq/logger-plugin-fs):
18
71
 
19
- ## 🚀 Uso rápido
72
+ > - **Adapter de infraestructura**
20
73
 
21
- ```typescript
22
- import { LoggerFactory } from "@jmlq/logger";
23
- import { createFsDatasource } from "@jmlq/logger-plugin-fs";
74
+ ```ts
75
+ // Path Ejemplo: src/infrastructure/logger/fs/fs.adapter.ts
24
76
 
25
- // Crear datasource de filesystem
26
- const fsDatasource = createFsDatasource({
27
- basePath: "./logs",
28
- fileNamePattern: "app-{yyyy}{MM}{dd}.log",
29
- rotation: { by: "day" },
30
- });
77
+ import {
78
+ createFsDatasource,
79
+ IFilesystemDatasourceOptions,
80
+ } from "@jmlq/logger-plugin-fs";
81
+ import type { ILogDatasource } from "@jmlq/logger";
82
+
83
+ export class FsAdapter {
84
+ private constructor(private readonly ds: ILogDatasource) {}
85
+ static create(opts: IFilesystemDatasourceOptions): FsAdapter | undefined {
86
+ try {
87
+ const ds = createFsDatasource(opts);
88
+ console.log("[logger] Conectado a FS para logs");
89
+ return new FsAdapter(ds);
90
+ } catch (e: any) {
91
+ console.warn("[logger] FS deshabilitado:", e?.message ?? e);
92
+ }
93
+ }
94
+ get datasource(): ILogDatasource {
95
+ return this.ds;
96
+ }
97
+ }
98
+ ```
99
+
100
+ ---
31
101
 
32
- // Crear logger usando la factory
33
- const logger = LoggerFactory.create([fsDatasource]);
102
+ ## Configuración @jmlq/logger
34
103
 
35
- // Usar el logger
36
- logger.info("Aplicación iniciada", { timestamp: new Date(), pid: process.pid });
37
- logger.error("Error de conexión", { service: "database", retries: 3 });
104
+ ### Variables de entorno
105
+
106
+ El plugin no gestiona variables de entorno internamente, pero puedes implementar tu propia capa de configuración:
107
+
108
+ ```ini
109
+ # Nivel mínimo de logs
110
+ # Valores: TRACE, DEBUG, INFO, WARN, ERROR, FATAL
111
+ LOG_LEVEL=INFO
112
+ # PII Protection
113
+ LOGGER_PII_ENABLED=true
114
+ LOGGER_PII_INCLUDE_DEFAULTS=false
115
+ # FS Plugin Settings
116
+ LOGGER_FS_PATH=./logs/
117
+ ```
118
+
119
+ ### Interface de configuración del paquete
120
+
121
+ ```ts
122
+ // Path Ejemplo: src/infrastructure/logger/core/logger.bootstrap.options.ts
123
+
124
+ import { LogLevel } from "@jmlq/logger";
125
+ import { IFilesystemDatasourceOptions } from "@jmlq/logger-plugin-fs";
126
+
127
+ export interface LoggerBootstrapOptions {
128
+ minLevel: LogLevel;
129
+ pii?: {
130
+ enabled?: boolean;
131
+ whitelistKeys?: string[];
132
+ blacklistKeys?: string[];
133
+ patterns?: any[];
134
+ deep?: boolean;
135
+ includeDefaults?: boolean;
136
+ };
137
+ adapters?: {
138
+ // NOTA: Opcional, depende de las necesidades del cliente
139
+ fs?: IFilesystemDatasourceOptions;
140
+ };
141
+ }
142
+ ```
143
+
144
+ ### Implementación del Logger
145
+
146
+ ```ts
147
+ // Path Ejemplo: src/infrastructure/logger/core/logger.bootstrap.ts
148
+
149
+ import { type ILogDatasource, createLogger } from "@jmlq/logger";
150
+ import { FsAdapter } from "../fs/fs.adapter";
151
+ import { LoggerBootstrapOptions } from "./logger.bootstrap.options";
152
+
153
+ export class LoggerBootstrap {
154
+ private constructor(
155
+ private readonly _logger: ReturnType<typeof createLogger>
156
+ ) {}
157
+
158
+ static async create(opts: LoggerBootstrapOptions): Promise<LoggerBootstrap> {
159
+ const dsList: ILogDatasource[] = [];
160
+
161
+ // NOTA: Opcional, depende de las necesidades del cliente
162
+ if (opts.adapters?.fs) {
163
+ const fs = FsAdapter.create(opts.adapters.fs);
164
+ if (fs) dsList.push(fs.datasource);
165
+ }
166
+ //----
167
+
168
+ if (dsList.length === 0)
169
+ throw new Error("[logger] No hay datasources válidos.");
170
+
171
+ const logger = createLogger({
172
+ datasources: dsList,
173
+ minLevel: opts.minLevel,
174
+ redactorOptions: {
175
+ enabled: opts.pii?.enabled ?? false,
176
+ deep: opts.pii?.deep ?? true,
177
+ patterns: opts.pii?.patterns ?? [],
178
+ },
179
+ });
180
+
181
+ console.log("✅ Logger creado correctamente.\n");
182
+
183
+ return new LoggerBootstrap(logger);
184
+ }
185
+
186
+ get logger() {
187
+ return this._logger;
188
+ }
189
+
190
+ // Limpia buffers pendientes antes del cierre
191
+ async flush() {
192
+ const logs = this._logger as any;
193
+ if (typeof logs.flush === "function") await logs.flush();
194
+ }
195
+
196
+ // Cierra recursos (conexiones, handles, etc.)
197
+ async dispose() {
198
+ const logs = this._logger as any;
199
+ if (typeof logs.dispose === "function") await logs.dispose();
200
+ }
201
+ }
202
+ ```
203
+
204
+ ### Inicializador del Logger
205
+
206
+ ```ts
207
+ // Path Ejemplo: src/infrastructure/logger/index.ts
208
+
209
+ import { envs } from "../../../../config/plugins";
210
+ // NOTA: envs es un archivo donde se leen variables de entorno .env
211
+ import { LoggerBootstrap } from "./core/logger.bootstrap";
212
+ import { parseLogLevel } from "./helper";
213
+ // NOTA: parseLogLevel hace un match con los valores del objeto LogLevel de @jmlq/logger
214
+
215
+ declare global {
216
+ var __LOGGER_BOOT__: Promise<LoggerBootstrap> | undefined;
217
+ }
218
+
219
+ async function init() {
220
+ return LoggerBootstrap.create({
221
+ minLevel: parseLogLevel(envs.logger.LOGGER_LEVEL),
222
+ pii: {
223
+ enabled: envs.logger.LOGGER_PII_ENABLED,
224
+ includeDefaults: envs.logger.LOGGER_PII_INCLUDE_DEFAULTS,
225
+ deep: true,
226
+ patterns: [
227
+ {
228
+ pattern: "\\b\\d{4}-\\d{4}-\\d{4}-\\d{4}\\b",
229
+ replaceWith: "****-****-****-****",
230
+ },
231
+ {
232
+ pattern: "[\\w.-]+@[\\w.-]+",
233
+ replaceWith: "***@***",
234
+ },
235
+ ],
236
+ },
237
+ adapters: {
238
+ // NOTA: Opcional, depende de las necesidades del cliente
239
+ fs: envs.logger.LOGGER_FS_PATH
240
+ ? {
241
+ basePath: envs.logger.LOGGER_FS_PATH,
242
+ fileNamePattern: "app-{yyyy}{MM}{dd}.log",
243
+ rotation: { by: "day" },
244
+ mkdir: true,
245
+ onRotate: (oldPath, newPath) => {
246
+ console.log(
247
+ ` [Rotate] Rotación completada: ${oldPath.absolutePath} → ${newPath.absolutePath}`
248
+ );
249
+ },
250
+ onError: (err) => {
251
+ console.error(" [Error Handler]", err.message);
252
+ },
253
+ }
254
+ : undefined,
255
+ },
256
+ });
257
+ }
258
+
259
+ // 1. Es una promesa singleton de LoggerBootstrap
260
+ // 2. Usa el operador nullish-coalescing (??) para: Reutilizar globalThis.__LOGGER_BOOT__ si ya existe.
261
+ // En caso contrario crea y memoriza (= init()) la promesa si no existe aún.
262
+ // 3. Garantiza una sola inicialización global del sistema de logging (adapters, datasources, PII, etc.)
263
+ // aunque el módulo se importe múltiples veces
264
+ export const bootReady: Promise<LoggerBootstrap> =
265
+ globalThis.__LOGGER_BOOT__ ?? (globalThis.__LOGGER_BOOT__ = init());
266
+
267
+ // 1. Es una promesa que resuelve directamente al logger
268
+ // 2. Hace un map de la promesa anterior: bootReady.then(b => b.logger).
269
+ export const loggerReady = bootReady.then((b) => b.logger);
270
+
271
+ // 1. Espera a bootReady y llama boot.flush(), que a su vez pide al logger/datasources
272
+ // que vacíen buffers pendientes (útil antes de apagar el proceso o en tests).
273
+ export async function flushLogs() {
274
+ const boot = await bootReady;
275
+ await boot.flush();
276
+ }
277
+
278
+ // 1. Espera a bootReady y llama boot.dispose(), que cierra recursos
279
+ // (conexiones a MongoDB/Postgres, file handles, etc.)
280
+ export async function disposeLogs() {
281
+ const boot = await bootReady;
282
+ await boot.dispose();
283
+ }
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Configuración en Frameworks
289
+
290
+ **NOTA**: Se sugiere realizarlo en la capa `presentation` de la API que implementa `@jmlq/logger` y/o sus plugins.
291
+
292
+ ### Express
293
+
294
+ #### Crear middleware (opcional):
295
+
296
+ Se puede realizar de la forma que el usuario considere conveniente
297
+
298
+ ```ts
299
+ // Path Ejemplo: src/presentation/middlewares/attach-logger.middleware.ts
300
+
301
+ import { ILogger } from "@jmlq/logger";
302
+ import { randomUUID } from "crypto";
303
+ import { Request, Response, NextFunction } from "express";
304
+
305
+ /**
306
+ * Middleware factory que adjunta contexto de logging al request.
307
+ *
308
+ * Responsabilidades:
309
+ * 1. Inyectar un logger base accesible desde cualquier controller/middleware.
310
+ * 2. Garantizar un requestId único para trazabilidad y correlación de logs.
311
+ *
312
+ * No configura el logger ni emite logs:
313
+ * solo prepara el contexto por request.
314
+ */
315
+ export function attachLogger(base: ILogger) {
316
+ /**
317
+ * Middleware de Express ejecutado en cada request HTTP.
318
+ */
319
+ return async (req: Request, _res: Response, next: NextFunction) => {
320
+ /**
321
+ * Se adjunta el logger al objeto Request para evitar:
322
+ * - imports globales
323
+ * - singletons
324
+ * - acoplamiento directo en controllers
325
+ */
326
+ req.logger = base;
327
+
328
+ /**
329
+ * Se asigna un identificador único por request.
330
+ *
331
+ * - Si el cliente ya envía "x-request-id" (ej. API Gateway, Load Balancer),
332
+ * se reutiliza para mantener trazabilidad entre servicios.
333
+ * - Si no existe, se genera un UUID local.
334
+ */
335
+ req.requestId = (req.headers["x-request-id"] as string) ?? randomUUID();
336
+
337
+ /**
338
+ * Continúa el pipeline normal de Express.
339
+ */
340
+ next();
341
+ };
342
+ }
343
+ ```
38
344
 
39
- // Cierre elegante
40
- process.on("SIGTERM", async () => {
41
- await logger.flush();
42
- await logger.dispose();
43
- process.exit(0);
345
+ #### Adjuntar middleware en el servidor
346
+
347
+ ```ts
348
+ // Path Ejemplo: src/presentation/server.ts
349
+
350
+ import { ILogger } from "@jmlq/logger";
351
+ import express from "express";
352
+ import { attachLogger } from "./middlewares";
353
+
354
+ export function createServer(base: ILogger) {
355
+ const app = express();
356
+ app.use(express.json()); // <-- necesario para POST JSON
357
+ app.use(attachLogger(base));
358
+
359
+ app.get("/health", (_req, res) => res.json({ ok: true }));
360
+
361
+ return app;
362
+ }
363
+ ```
364
+
365
+ #### Iniciar servidor
366
+
367
+ **NOTA**: Esto se realiza en la raíz de la API
368
+
369
+ ```ts
370
+ // Path Ejemplo: src/app.ts
371
+
372
+ import { envs } from "./config/plugins";
373
+ import { disposeLogs, flushLogs, loggerReady } from "./infrastructure/logger";
374
+ import { createServer } from "./presentation/server";
375
+
376
+ async function bootstrap() {
377
+ const logger = await loggerReady;
378
+
379
+ const app = createServer(logger);
380
+ const server = app.listen(envs.PORT, envs.HOST, () => {
381
+ console.log(`🚀 Server running at http://${envs.HOST}:${envs.PORT}`);
382
+ logger.info("http.start", { host: envs.HOST, port: envs.PORT });
383
+ });
384
+
385
+ server.on("error", async (err: any) => {
386
+ logger.error("http.listen.error", {
387
+ message: err?.message,
388
+ stack: err?.stack,
389
+ });
390
+ await flushLogs().catch(() => {});
391
+ await disposeLogs().catch(() => {});
392
+ process.exit(1);
393
+ });
394
+
395
+ // 3) Señales de apagado limpio
396
+ const shutdown = async (reason: string, code = 0) => {
397
+ logger.info("app.shutdown.begin", { reason });
398
+ server.close(async () => {
399
+ try {
400
+ await flushLogs();
401
+ } catch {}
402
+ try {
403
+ await disposeLogs();
404
+ } catch {}
405
+ logger.info("app.shutdown.end", { code });
406
+ process.exit(code);
407
+ });
408
+ };
409
+
410
+ process.on("SIGINT", () => shutdown("SIGINT"));
411
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
412
+
413
+ // Fallos no controlados
414
+ process.on("unhandledRejection", async (err) => {
415
+ const e = err as any;
416
+ logger.error("unhandled.rejection", {
417
+ message: e?.message,
418
+ stack: e?.stack,
419
+ });
420
+ await shutdown("unhandledRejection", 1);
421
+ });
422
+
423
+ process.on("uncaughtException", async (err) => {
424
+ logger.error("uncaught.exception", {
425
+ message: err.message,
426
+ stack: err.stack,
427
+ });
428
+ await shutdown("uncaughtException", 1);
429
+ });
430
+ }
431
+
432
+ bootstrap().catch(async (err) => {
433
+ // Falla durante el bootstrap
434
+ const logger = await loggerReady.catch(() => null);
435
+ if (logger) {
436
+ logger.error("bootstrap.fatal", {
437
+ message: (err as Error)?.message,
438
+ stack: (err as Error)?.stack,
439
+ });
440
+ await flushLogs().catch(() => {});
441
+ await disposeLogs().catch(() => {});
442
+ } else {
443
+ // último recurso si el logger no llegó a inicializar
444
+ console.error("Fatal error (no logger):", err);
445
+ }
446
+ process.exit(1);
44
447
  });
45
448
  ```
46
449
 
450
+ ---
451
+
47
452
  ## ⚙️ Configuración del datasource
48
453
 
49
454
  El datasource de filesystem acepta las siguientes opciones de configuración:
@@ -52,7 +457,7 @@ El datasource de filesystem acepta las siguientes opciones de configuración:
52
457
 
53
458
  | Opción | Tipo | Descripción | Por defecto |
54
459
  | ----------------- | ---------------- | ------------------------------------------------------ | --------------------------- |
55
- | `basePath` | `string` | Directorio base donde se guardarán los archivos de log | `"./logs"` |
460
+ | `basePath` | `string` | Directorio base donde se guardarán los archivos de log | `"./logs/"` |
56
461
  | `fileNamePattern` | `string` | Patrón para nombrar archivos con tokens de fecha | `"app-{yyyy}{MM}{dd}.log"` |
57
462
  | `rotation` | `RotationConfig` | Configuración de política de rotación | `{ by: "day" }` |
58
463
  | `mkdir` | `boolean` | Crear directorio base si no existe | `true` |
@@ -92,7 +497,7 @@ Crea un archivo nuevo cada día basado en fecha UTC:
92
497
  import { createFsDatasource } from "@jmlq/logger-plugin-fs";
93
498
 
94
499
  const datasource = createFsDatasource({
95
- basePath: "./logs",
500
+ basePath: "./logs/",
96
501
  fileNamePattern: "app-{yyyy}{MM}{dd}.log",
97
502
  rotation: { by: "day" },
98
503
  onRotate: (oldPath, newPath) => {
@@ -109,7 +514,7 @@ Rota cuando el archivo alcanza el tamaño máximo especificado:
109
514
 
110
515
  ```typescript
111
516
  const datasource = createFsDatasource({
112
- basePath: "./logs",
517
+ basePath: "./logs/",
113
518
  fileNamePattern: "app.log",
114
519
  rotation: {
115
520
  by: "size",
@@ -130,98 +535,15 @@ Mantiene un único archivo que crece indefinidamente:
130
535
 
131
536
  ```typescript
132
537
  const datasource = createFsDatasource({
133
- basePath: "./logs",
538
+ basePath: "./logs/",
134
539
  fileNamePattern: "application.log",
135
540
  rotation: { by: "none" },
136
541
  });
137
542
  ```
138
543
 
139
- ## 🧩 Ejemplo completo
140
-
141
- ```typescript
142
- import { createFsDatasource } from "@jmlq/logger-plugin-fs";
143
- import { LogLevel } from "@jmlq/logger";
144
-
145
- const datasource = createFsDatasource({
146
- basePath: "./logs",
147
- fileNamePattern: "app-{yyyy}-{MM}-{dd}.log",
148
- rotation: { by: "day" },
149
- mkdir: true,
150
- serializer: {
151
- serialize(log: any) {
152
- return JSON.stringify(log, null, 2);
153
- },
154
- },
155
- onRotate: (oldPath, newPath) => {
156
- console.log(`Rotación: ${oldPath.absolutePath} → ${newPath.absolutePath}`);
157
- },
158
- onError: (err) => {
159
- console.error("Error en datasource:", err.message);
160
- },
161
- });
162
-
163
- // Guardar logs
164
- await datasource.save({
165
- level: LogLevel.INFO,
166
- message: "Servidor iniciado correctamente",
167
- timestamp: Date.now(),
168
- });
169
-
170
- await datasource.save({
171
- level: LogLevel.DEBUG,
172
- message: "Conectando a base de datos...",
173
- timestamp: Date.now(),
174
- });
175
-
176
- // Cierre
177
- await datasource.flush();
178
- await datasource.dispose();
179
- ```
180
-
181
- ## 🧪 Testing
182
-
183
- ```typescript
184
- import { createFsDatasource } from "@jmlq/logger-plugin-fs";
185
- import * as fs from "fs";
186
- import * as path from "path";
187
-
188
- describe("FileSystem Datasource", () => {
189
- const testLogsPath = "./test-logs";
190
-
191
- beforeEach(() => {
192
- if (fs.existsSync(testLogsPath)) {
193
- fs.rmSync(testLogsPath, { recursive: true });
194
- }
195
- });
196
-
197
- test("debe crear archivo de log", async () => {
198
- const datasource = createFsDatasource({
199
- basePath: testLogsPath,
200
- fileNamePattern: "test.log",
201
- rotation: { by: "none" },
202
- });
203
-
204
- await datasource.save({
205
- level: "info",
206
- message: "Test message",
207
- timestamp: new Date(),
208
- });
209
-
210
- await datasource.flush();
211
-
212
- const logFile = path.join(testLogsPath, "test.log");
213
- expect(fs.existsSync(logFile)).toBe(true);
214
-
215
- const content = fs.readFileSync(logFile, "utf8");
216
- expect(content).toContain("Test message");
217
- });
218
- });
219
- ```
220
-
221
544
  ## 📄 Más Información
222
545
 
223
546
  - **[Arquitectura Detallada](./architecture.md)** - Documentación técnica completa
224
- - **[Guía de Instalación](./install.md)** - Configuración paso a paso
225
547
  - **[Ejemplos](./examples/)** - Códigos de ejemplo funcionales
226
548
 
227
549
  ## 📄 Licencia