@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.
- package/README.md +292 -0
- 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
|