@jmlq/logger-plugin-fs 0.1.0-alpha.2 → 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.
Files changed (2) hide show
  1. package/README.md +292 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,292 @@
1
+ # @jmlq/logger-plugin-fs
2
+
3
+ Plugin de sistema de archivos (FileSystem) para **@jmlq/logger**. Permite persistir logs en archivos locales, ideal para entornos de desarrollo, servidores con volumen montado o como “sink” secundario junto a bases de datos.
4
+
5
+ ---
6
+
7
+ ## 📦 Instalación
8
+
9
+ ```bash
10
+ # Con npm
11
+ npm i @jmlq/logger @jmlq/logger-plugin-fs
12
+
13
+ # Con pnpm
14
+ pnpm add @jmlq/logger @jmlq/logger-plugin-fs
15
+
16
+ # Con yarn
17
+ yarn add @jmlq/logger @jmlq/logger-plugin-fs
18
+
19
+ ```
20
+
21
+ Este plugin **depende** de `@jmlq/logger`. Asegúrate de instalar ambos paquetes.
22
+
23
+ ## 🧱 Estructura del paquete
24
+
25
+ La estructura puede variar según tu versión; esta es la forma típica en librerías de “Clean Architecture”.
26
+
27
+ ```txt
28
+ @jmlq/logger-plugin-fs/
29
+ ├─ src/
30
+ │ ├─ index.ts # Implementa ILogDatasource escribiendo a disco y lo Exporta FileSystemDatasource
31
+ ├─ package.json
32
+ └─ README.md
33
+ ```
34
+
35
+ ### 📝 Resumen rápido
36
+
37
+ > - **Qué es**: Un `Datasource` para **@jmlq/logger** que escribe logs en un archivo.
38
+ > - **Cuándo usarlo**: Desarrollo, staging, escrituras locales, sidecar para auditoría en disco.
39
+ > - **Cómo se usa**: Instancia `FileSystemDatasource({ filePath })`, crea el logger con `createLogger`, y registra tus eventos.
40
+
41
+ ---
42
+
43
+ ### 🧩 Configuración
44
+
45
+ Configurar la ruta del archivo y el nivel mínimo de log vía variables de entorno (nombres sugeridos; adáptalos en el proyecto):
46
+
47
+ ### 🔐 Variables de Entorno (.env)
48
+
49
+ ```ini
50
+ # Archivo donde se guardarán los logs (crear carpeta si no existe)
51
+ LOGGER_FS_PATH=./logs/app.log
52
+
53
+ # Nivel mínimo de log aceptado por el logger global
54
+ # Valores: trace|debug|info|warn|error|fatal
55
+ LOGGER_LEVEL=info
56
+
57
+ # --- (Opcional) Controles de PII ---
58
+ # Activa redacción de datos sensibles
59
+ LOGGER_PII_ENABLED=true
60
+ # Incluye patrones por defecto del core, si tu build los expone
61
+ LOGGER_PII_INCLUDE_DEFAULTS=true
62
+
63
+ ```
64
+
65
+ Sugerencia: crea el directorio `./logs` en tu entorno local o asegúrate de que el proceso tenga permisos de escritura en la ruta indicada.
66
+
67
+ ---
68
+
69
+ ### 🚀 Uso del paquete
70
+
71
+ #### 1) Crea un wrapper: `rotating-fs.datasource.ts`
72
+
73
+ ```tsx
74
+ // src/config/logger/rotating-fs.datasource.ts
75
+ import { dirname, extname, basename } from "node:path";
76
+ import { existsSync, mkdirSync } from "node:fs";
77
+ import { FileSystemDatasource } from "@jmlq/logger-plugin-fs";
78
+ import type { ILog, ILogDatasource } from "@jmlq/logger";
79
+
80
+ /** YYYYMMDD con hora local del servidor */
81
+ function dateStamp(d = new Date()) {
82
+ const yyyy = d.getFullYear();
83
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
84
+ const dd = String(d.getDate()).padStart(2, "0");
85
+ return `${yyyy}${mm}${dd}`;
86
+ }
87
+
88
+ /** Crea carpeta si falta */
89
+ function ensureDirFor(filePath: string) {
90
+ const dir = dirname(filePath);
91
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
92
+ }
93
+
94
+ /** Devuelve ruta diaria: ./logs/app-YYYYMMDD.log a partir de base ./logs/app.log */
95
+ function dailyPath(basePath: string, stamp = dateStamp()) {
96
+ const ext = extname(basePath) || ".log";
97
+ const name = basename(basePath, ext);
98
+ const dir = dirname(basePath);
99
+ return `${dir}/${name}-${stamp}${ext}`;
100
+ }
101
+
102
+ /** Milisegundos hasta la próxima medianoche local */
103
+ function msUntilNextMidnight() {
104
+ const now = new Date();
105
+ const next = new Date(
106
+ now.getFullYear(),
107
+ now.getMonth(),
108
+ now.getDate() + 1,
109
+ 0,
110
+ 0,
111
+ 0,
112
+ 0
113
+ );
114
+ return next.getTime() - now.getTime();
115
+ }
116
+
117
+ /**
118
+ * Datasource que rota el archivo a diario sin reiniciar el proceso.
119
+ * Estrategia:
120
+ * - Chequea fecha en cada save().
121
+ * - Agenda un timer para forzar rotación exacto a medianoche.
122
+ */
123
+ export class RotatingDailyFSDatasource implements ILogDatasource {
124
+ private readonly basePath: string;
125
+ private currentStamp: string;
126
+ private inner: FileSystemDatasource;
127
+ private midnightTimer: NodeJS.Timeout | null = null;
128
+
129
+ constructor(opts: { basePath: string }) {
130
+ if (!opts.basePath) throw new Error("[logger] basePath requerido");
131
+ this.basePath = opts.basePath;
132
+ this.currentStamp = dateStamp();
133
+ const fp = dailyPath(this.basePath, this.currentStamp);
134
+ ensureDirFor(fp);
135
+ this.inner = new FileSystemDatasource({ filePath: fp });
136
+ this.armMidnightTimer();
137
+ }
138
+
139
+ /** Programa rotación en la próxima medianoche */
140
+ private armMidnightTimer() {
141
+ const delay = msUntilNextMidnight();
142
+ this.midnightTimer = setTimeout(() => {
143
+ // Forzamos rotación incluso si no llega ningún save()
144
+ this.rotateIfNeeded(/*force*/ true);
145
+ // Re-armar para el siguiente día
146
+ this.armMidnightTimer();
147
+ }, delay);
148
+ // Evita que el timer mantenga vivo el proceso si todo lo demás termina:
149
+ this.midnightTimer.unref?.();
150
+ }
151
+
152
+ /** Cierra el archivo actual y crea uno nuevo si cambió el día o si es forzado */
153
+ private async rotateIfNeeded(force = false) {
154
+ const today = dateStamp();
155
+ if (!force && today === this.currentStamp) return;
156
+
157
+ // Cerrar el datasource actual si expone dispose()
158
+ await this.inner.dispose?.().catch(() => {
159
+ /* no-op */
160
+ });
161
+
162
+ this.currentStamp = today;
163
+ const fp = dailyPath(this.basePath, this.currentStamp);
164
+ ensureDirFor(fp);
165
+ this.inner = new FileSystemDatasource({ filePath: fp });
166
+ }
167
+
168
+ /** Guarda un log; rota si el día cambió */
169
+ async save(log: ILog): Promise<void> {
170
+ // Rota on-demand si cambió la fecha
171
+ await this.rotateIfNeeded(false);
172
+ return this.inner.save(log);
173
+ }
174
+
175
+ /** Delegación opcional si el DS base lo implementa */
176
+ async find?(filter?: any): Promise<ILog[]> {
177
+ const maybe = (this.inner as any).find;
178
+ if (typeof maybe === "function") return maybe.call(this.inner, filter);
179
+ throw new Error("find() no soportado por FileSystemDatasource");
180
+ }
181
+
182
+ /** Intenta vaciar buffers si existe */
183
+ async flush?(): Promise<void> {
184
+ await this.inner.flush?.();
185
+ }
186
+
187
+ /** Cierra recursos, cancela timer */
188
+ async dispose?(): Promise<void> {
189
+ if (this.midnightTimer) {
190
+ clearTimeout(this.midnightTimer);
191
+ this.midnightTimer = null;
192
+ }
193
+ await this.inner.dispose?.();
194
+ }
195
+ }
196
+ ```
197
+
198
+ **Notas clave**
199
+
200
+ > - Usa **hora local** del servidor. Si necesitas zona específica (p. ej. America/Guayaquil), conviene fijar el TZ del contenedor/host.
201
+ > - `unref()` del timer ayuda a que el proceso pueda salir si no hay nada más pendiente.
202
+ > - Si tu `FileSystemDatasource` no implementa `flush`/`dispose`, las llamadas son seguras con el optional chaining.
203
+
204
+ #### 2) Configuración (solo FS)
205
+
206
+ ```ts
207
+ // src/config/logger/index.ts
208
+ import { envs } from "../plugins/envs.plugin";
209
+ import { LogLevel, createLogger } from "@jmlq/logger";
210
+ import type { ILogDatasource } from "@jmlq/logger";
211
+ import { clientPiiPatterns, redactKeys, preserveKeys } from "./pii";
212
+ import { RotatingDailyFSDatasource } from "./rotating-fs.datasource";
213
+
214
+ /** Mapea string (.env) -> LogLevel */
215
+ function toMinLevel(level: string): LogLevel {
216
+ switch (level?.toLowerCase?.()) {
217
+ case "trace":
218
+ return LogLevel.TRACE;
219
+ case "debug":
220
+ return LogLevel.DEBUG;
221
+ case "info":
222
+ return LogLevel.INFO;
223
+ case "warn":
224
+ return LogLevel.WARN;
225
+ case "error":
226
+ return LogLevel.ERROR;
227
+ case "fatal":
228
+ return LogLevel.FATAL;
229
+ default:
230
+ return LogLevel.DEBUG;
231
+ }
232
+ }
233
+
234
+ async function buildDatasource(): Promise<ILogDatasource> {
235
+ const basePath = envs.logger.LOGGER_FS_PATH;
236
+ if (!basePath) throw new Error("[logger] Falta LOGGER_FS_PATH en .env");
237
+
238
+ // << Cambio importante: datasource rotatorio >>
239
+ return new RotatingDailyFSDatasource({ basePath });
240
+ }
241
+
242
+ async function initLogger() {
243
+ const datasource = await buildDatasource();
244
+ return createLogger(datasource, {
245
+ minLevel: toMinLevel(envs.logger.LOGGER_LEVEL),
246
+ pii: {
247
+ enabled: envs.logger.LOGGER_PII_ENABLED,
248
+ includeDefaultPatterns: envs.logger.LOGGER_PII_INCLUDE_DEFAULTS,
249
+ patterns: clientPiiPatterns,
250
+ redactKeys,
251
+ preserveKeys,
252
+ },
253
+ });
254
+ }
255
+
256
+ export const loggerReady = initLogger();
257
+
258
+ export async function flushLogs() {
259
+ const logger = (await loggerReady) as any;
260
+ if (typeof logger.flush === "function") await logger.flush();
261
+ }
262
+
263
+ export async function disposeLogs() {
264
+ const logger = (await loggerReady) as any;
265
+ if (typeof logger.dispose === "function") await logger.dispose();
266
+ }
267
+ ```
268
+
269
+ #### 3) Comportamiento esperado
270
+
271
+ Con `.env`
272
+
273
+ ```ini
274
+ LOGGER_FS_PATH=./logs/app.log
275
+
276
+ ```
277
+
278
+ > - Hoy se escribe en: `./logs/app-YYYYMMDD.log`.
279
+ > - En `pleno funcionamiento`, al llegar a medianoche local:
280
+ > > - El wrapper cierra el archivo anterior y abre `./logs/app-YYYYMMDD+1.log`.
281
+ > > - No pierdes logs y no necesitas reiniciar el servicio.
282
+
283
+ ---
284
+
285
+ #### Consideraciones y opciones
286
+
287
+ > - **Rotación por tamaño**: si además quieres limitar tamaño, se necesitaría añadir lógica extra (por ejemplo, chequear `fs.stat` y rotar al superar un umbral) o usar un transport que ya lo haga.
288
+ > - **Compresión/retención**: se puede anexar un job (cron/worker) que comprima y borre archivos antiguos.
289
+
290
+ ## 📄 Licencia
291
+
292
+ MIT © Mauricio Lahuasi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmlq/logger-plugin-fs",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.3",
4
4
  "author": "MLahuasi",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",