@jmlq/logger-plugin-fs 0.1.0-alpha.3 → 0.1.0-alpha.4
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 +137 -241
- package/dist/application/dto/index.d.ts +1 -0
- package/dist/application/dto/index.js +17 -0
- package/dist/application/dto/save-log.dto.d.ts +4 -0
- package/dist/application/dto/save-log.dto.js +2 -0
- package/dist/application/services/fs-datasource.service.d.ts +9 -0
- package/dist/application/services/fs-datasource.service.js +22 -0
- package/dist/application/services/index.d.ts +1 -0
- package/dist/application/services/index.js +17 -0
- package/dist/application/use-cases/append-log.usecase.d.ts +7 -0
- package/dist/application/use-cases/append-log.usecase.js +19 -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/persist-log.usecase.d.ts +23 -0
- package/dist/application/use-cases/persist-log.usecase.js +74 -0
- package/dist/application/use-cases/rotate-if-needed.usecase.d.ts +10 -0
- package/dist/application/use-cases/rotate-if-needed.usecase.js +39 -0
- package/dist/domain/contracts/clock.contract.d.ts +3 -0
- package/dist/domain/contracts/clock.contract.js +2 -0
- package/dist/domain/contracts/file-rotator.contract.d.ts +7 -0
- package/dist/domain/contracts/file-rotator.contract.js +2 -0
- package/dist/domain/contracts/index.d.ts +4 -0
- package/dist/domain/contracts/index.js +20 -0
- package/dist/domain/contracts/serializer.contract.d.ts +3 -0
- package/dist/domain/contracts/serializer.contract.js +2 -0
- package/dist/domain/contracts/stream-writer.port.d.ts +6 -0
- package/dist/domain/contracts/stream-writer.port.js +2 -0
- package/dist/domain/types/index.d.ts +1 -0
- package/dist/domain/types/index.js +17 -0
- package/dist/domain/types/options.type.d.ts +11 -0
- package/dist/domain/types/options.type.js +5 -0
- package/dist/domain/value-objects/file-name-pattern.vo.d.ts +4 -0
- package/dist/domain/value-objects/file-name-pattern.vo.js +14 -0
- package/dist/domain/value-objects/index.d.ts +2 -0
- package/dist/domain/value-objects/index.js +18 -0
- package/dist/domain/value-objects/rotation-policy.vo.d.ts +7 -0
- package/dist/domain/value-objects/rotation-policy.vo.js +20 -0
- package/dist/index.d.ts +3 -13
- package/dist/index.js +7 -38
- package/dist/infrastructure/datasources/fs.datasource.d.ts +17 -0
- package/dist/infrastructure/datasources/fs.datasource.js +84 -0
- package/dist/infrastructure/datasources/index.d.ts +1 -0
- package/dist/infrastructure/datasources/index.js +17 -0
- package/dist/infrastructure/fs/file-rotator.adapter.d.ts +22 -0
- package/dist/infrastructure/fs/file-rotator.adapter.js +51 -0
- package/dist/infrastructure/fs/fs-provider.d.ts +15 -0
- package/dist/infrastructure/fs/fs-provider.js +55 -0
- package/dist/infrastructure/fs/fs-writer.adapter.d.ts +10 -0
- package/dist/infrastructure/fs/fs-writer.adapter.js +26 -0
- package/dist/infrastructure/fs/index.d.ts +5 -0
- package/dist/infrastructure/fs/index.js +21 -0
- package/dist/infrastructure/fs/node-clock.adapter.d.ts +4 -0
- package/dist/infrastructure/fs/node-clock.adapter.js +10 -0
- package/dist/infrastructure/fs/path-utils.d.ts +6 -0
- package/dist/infrastructure/fs/path-utils.js +26 -0
- package/dist/presentation/factory/create-fs-datasource.d.ts +15 -0
- package/dist/presentation/factory/create-fs-datasource.js +39 -0
- package/dist/presentation/factory/index.d.ts +1 -0
- package/dist/presentation/factory/index.js +17 -0
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @jmlq/logger-plugin-fs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Datasource de `sistema de archivos` para [`@jmlq/logger`](https://www.npmjs.com/package/@jmlq/logger).
|
|
4
|
+
Escribe cada evento de log como **línea** (JSONL por defecto) y soporta `rotación por día` o por `tamaño`, manejo de `backpressure/drain`, `flush()` y `dispose()`.
|
|
4
5
|
|
|
5
6
|
---
|
|
6
7
|
|
|
@@ -10,282 +11,177 @@ Plugin de sistema de archivos (FileSystem) para **@jmlq/logger**. Permite persis
|
|
|
10
11
|
# Con npm
|
|
11
12
|
npm i @jmlq/logger @jmlq/logger-plugin-fs
|
|
12
13
|
|
|
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
14
|
```
|
|
20
15
|
|
|
21
|
-
Este plugin **depende** de `@jmlq/logger
|
|
16
|
+
Este plugin **depende** de [`@jmlq/logger`](https://www.npmjs.com/package/@jmlq/logger). Asegúrate de instalar ambos paquetes.
|
|
22
17
|
|
|
23
18
|
## 🧱 Estructura del paquete
|
|
24
19
|
|
|
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
20
|
### 📝 Resumen rápido
|
|
36
21
|
|
|
37
|
-
> -
|
|
38
|
-
> -
|
|
39
|
-
> -
|
|
22
|
+
> - **`src/domain/`** — Reglas del negocio del plugin (sin dependencias de Node).
|
|
23
|
+
> > - **`contracts/`**
|
|
24
|
+
> > > - `clock.contract.ts` — Puerto que abstrae el tiempo actual (`now()`), permite testear rotación sin depender de `Date.now()`.
|
|
25
|
+
> > > - `file-rotator.port.ts` — Puerto para rotación: calcula path esperado, tamaño, índice, etc.
|
|
26
|
+
> > > - `stream-writer.port.ts` — Puerto para escritura secuencial: `write`, `once("drain")`, `end`.
|
|
27
|
+
> > - **`value-objects/`**
|
|
28
|
+
> > > - `file-name-pattern.vo.ts` — VO que encapsula el patrón de nombres (`{yyyy}{MM}{dd}`) con validación.
|
|
29
|
+
> > > - `rotation-policy.vo.ts` — VO que define estrategia de rotación (`none | day | size`) con invariantes (`maxSizeMB > 0`).
|
|
30
|
+
> > - **`types/`**
|
|
31
|
+
> > > - `options.type.ts` — Define `FsDatasourceOptions` y `IFsSerializer` (contrato de serialización de líneas).
|
|
32
|
+
|
|
33
|
+
> - **`src/application/`** — Casos de uso (orquestan, no dependen de Node).
|
|
34
|
+
> > - **`dto/`**
|
|
35
|
+
> > > - `save-log.dto.ts` — DTO de entrada: `{ log: ILog }`.
|
|
36
|
+
> > - **`use-cases/`**
|
|
37
|
+
> > > - `rotate-if-needed.usecase.ts` — Decide si se rota: compara fecha/tamaño contra `RotationPolicy`.
|
|
38
|
+
> > > - `append-log.usecase.ts` — Serializa y escribe línea; maneja backpressure (`write=false` → espera `drain`).
|
|
39
|
+
> > > - `persist-log.usecase.ts` — Orquesta: asegura writer, rota si corresponde, escribe, dispara hooks.
|
|
40
|
+
> > - **`services/`**
|
|
41
|
+
> > > - `fs-datasource.service.ts` — Implementa `ILogDatasource` del core usando los UC anteriores.
|
|
42
|
+
|
|
43
|
+
> - **`src/infrastructure/`** — Adaptadores técnicos (Node.js).
|
|
44
|
+
> > - **`fs/`**
|
|
45
|
+
> > > - `fs-provider.ts` — Wrapper de `fs`/`fs.promises` (mockeable en tests).
|
|
46
|
+
> > > - `path-utils.ts` — Utilidades para `join`, `splitBaseExt`, `formatPattern` (UTC).
|
|
47
|
+
> > > - `file-rotator.adapter.ts` — Implementa `IFileRotatorPort` usando `fs-provider` y `path-utils`.
|
|
48
|
+
> > > - `fs-writer.adapter.ts` — Implementa `IStreamWriterPort` con `fs.createWriteStream`.
|
|
49
|
+
> > > - `node-clock.adapter.ts` — Implementa `IClock` devolviendo `new Date()`.
|
|
50
|
+
|
|
51
|
+
> - **`src/presentation/`** — API pública (cara del paquete).
|
|
52
|
+
> > - **`factory/`**
|
|
53
|
+
> > > - `create-fs-datasource.ts` — Ensambla VO + adaptadores + casos de uso y devuelve un `ILogDatasource`.
|
|
54
|
+
> > - `index.ts` — Barrel: reexporta la factory y VO/contratos públicos (`RotationPolicy`, `FileNamePattern`).
|
|
40
55
|
|
|
41
56
|
---
|
|
42
57
|
|
|
43
58
|
### 🧩 Configuración
|
|
44
59
|
|
|
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
60
|
### 🔐 Variables de Entorno (.env)
|
|
48
61
|
|
|
49
62
|
```ini
|
|
50
|
-
#
|
|
51
|
-
LOGGER_FS_PATH=./logs
|
|
63
|
+
# Ruta base (carpeta) para los archivos de log
|
|
64
|
+
LOGGER_FS_PATH=./logs
|
|
52
65
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
LOGGER_LEVEL=info
|
|
66
|
+
# Patrón (opcional). Si omites, usa "app-{yyyy}{MM}{dd}.log"
|
|
67
|
+
LOGGER_FS_PATTERN=app-{yyyy}{MM}{dd}.log
|
|
56
68
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# Incluye patrones por defecto del core, si tu build los expone
|
|
61
|
-
LOGGER_PII_INCLUDE_DEFAULTS=true
|
|
69
|
+
# Rotación: "day" | "size" | "none"
|
|
70
|
+
LOGGER_FS_ROTATION=day
|
|
71
|
+
LOGGER_FS_MAX_SIZE_MB=50
|
|
62
72
|
|
|
63
|
-
|
|
73
|
+
# Logger core
|
|
74
|
+
LOGGER_LEVEL=info # trace|debug|info|warn|error|fatal
|
|
64
75
|
|
|
65
|
-
|
|
76
|
+
```
|
|
66
77
|
|
|
67
78
|
---
|
|
68
79
|
|
|
69
80
|
### 🚀 Uso del paquete
|
|
70
81
|
|
|
71
|
-
#### 1) Crea un wrapper: `rotating-fs.datasource.ts`
|
|
72
|
-
|
|
73
82
|
```tsx
|
|
74
|
-
|
|
75
|
-
import {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
}
|
|
83
|
+
import { createLogger, LogLevel } from "@jmlq/logger";
|
|
84
|
+
import {
|
|
85
|
+
createFsDatasource,
|
|
86
|
+
RotationPolicy,
|
|
87
|
+
FileNamePattern,
|
|
88
|
+
} from "@jmlq/logger-plugin-fs";
|
|
89
|
+
|
|
90
|
+
const ds = createFsDatasource({
|
|
91
|
+
basePath: "./logs", // Carpeta destino
|
|
92
|
+
mkdir: true, // Crea carpeta si no existe
|
|
93
|
+
fileNamePattern: "app-{yyyy}{MM}{dd}.log", // Rotación diaria por fecha (UTC)
|
|
94
|
+
// Alternativa por tamaño:
|
|
95
|
+
// rotation: { by: "size", maxSizeMB: 50 }
|
|
96
|
+
rotation: { by: "day" },
|
|
97
|
+
// Serializador opcional (por defecto JSON.stringify)
|
|
98
|
+
// serializer: { serialize: (log) => formatMyLine(log) },
|
|
99
|
+
onRotate: (oldP, newP) => console.log("[fs] rotated:", oldP, "->", newP),
|
|
100
|
+
onError: (e) => console.error("[fs] error:", e),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const logger = createLogger(ds, { minLevel: LogLevel.INFO });
|
|
104
|
+
|
|
105
|
+
logger.info("Servidor iniciado", { pid: process.pid });
|
|
106
|
+
|
|
107
|
+
// Cierre elegante
|
|
108
|
+
process.on("SIGTERM", async () => {
|
|
109
|
+
await logger.flush?.();
|
|
110
|
+
await logger.dispose?.();
|
|
111
|
+
process.exit(0);
|
|
112
|
+
});
|
|
196
113
|
```
|
|
197
114
|
|
|
198
|
-
|
|
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
|
-
```
|
|
115
|
+
#### Opciones soportadas
|
|
268
116
|
|
|
269
|
-
|
|
117
|
+
> - `basePath: string` – carpeta donde se guardan los logs.
|
|
118
|
+
> - `mkdir?: boolean` – crea la carpeta si no existe.
|
|
119
|
+
> - `fileNamePattern?: string` – tokens {yyyy}{MM}{dd} (formateados en UTC).
|
|
120
|
+
> > - Ejemplos: `"app-{yyyy}{MM}{dd}.log"`, `"service.log"`.
|
|
121
|
+
> - `rotation?: { by: "none" | "day" | "size"; maxSizeMB?: number; maxFiles?: number }`
|
|
122
|
+
> > - `day` → rota al cambiar la fecha (UTC).
|
|
123
|
+
> > - `size` → rota al alcanzar `maxSizeMB` (genera app.1.log, app.2.log, …).
|
|
124
|
+
> - `serializer?: { serialize(entry: unknown): string }` – una línea sin `\n`.
|
|
125
|
+
> - `onRotate?: (oldPath, newPath) => void | Promise<void>`
|
|
126
|
+
> - `onError?: (err) => void | Promise<void>`
|
|
270
127
|
|
|
271
|
-
|
|
128
|
+
---
|
|
272
129
|
|
|
273
|
-
|
|
274
|
-
LOGGER_FS_PATH=./logs/app.log
|
|
130
|
+
### 🗂️ Formato de archivo
|
|
275
131
|
|
|
276
|
-
|
|
132
|
+
> - **Una línea por evento** (JSONL por defecto).
|
|
133
|
+
> - El `serializer` determina el formato de cada línea (sin salto).
|
|
134
|
+
> - El plugin agrega `"\n"` y maneja `backpressure`/`drain` del stream.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### 🔁 Rotación
|
|
277
139
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
>
|
|
281
|
-
>
|
|
140
|
+
**Por día **(`rotation: { by: "day" }`)
|
|
141
|
+
|
|
142
|
+
> - El archivo activo se calcula con `fileNamePattern` usando la `fecha UTC actual`.
|
|
143
|
+
> - Si al persistir cambia el día → `cierra` el stream anterior, abre uno nuevo y dispara `onRotate(old, new)`.
|
|
144
|
+
|
|
145
|
+
**Por tamaño** (`rotation: { by: "size", maxSizeMB }`)
|
|
146
|
+
|
|
147
|
+
> - Comprueba el tamaño del archivo activo. Si `>= maxSizeMB` → rota a `app.1.log`, `app.2.log`, … (buscando el siguiente índice libre).
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### 🧯 Backpressure & drain (cómo evita perder logs)
|
|
152
|
+
|
|
153
|
+
Node.js devuelve `false` en `stream.write()` cuando el **buffer interno está lleno** (backpressure).
|
|
154
|
+
El plugin:
|
|
155
|
+
|
|
156
|
+
1. Serializa el log + `"\n"`.
|
|
157
|
+
2. Llama `write(...)`.
|
|
158
|
+
3. Si devuelve `false`, **espera el evento** `drain` antes de continuar.
|
|
159
|
+
4. `flush()` espera a que se libere el buffer si `writableNeedDrain` es `true`.
|
|
160
|
+
5. `dispose()` cierra el stream actual drenando el buffer pendiente.
|
|
282
161
|
|
|
283
162
|
---
|
|
284
163
|
|
|
285
|
-
|
|
164
|
+
### 🌐 Zona horaria
|
|
286
165
|
|
|
287
|
-
> -
|
|
288
|
-
> -
|
|
166
|
+
> - El formateo `{yyyy}{MM}{dd}` del patrón se realiza en **UTC** para evitar desfases por huso horario (máquinas/CI distintas).
|
|
167
|
+
> - Si necesitas otro criterio (p.ej., “día de Guayaquil”), puedes:
|
|
168
|
+
> > - Ajustar el **IClock** para proveer el “ahora” en otro huso y/o
|
|
169
|
+
> > - Reemplazar el util de formateo si deseas `{…}` en local time.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## ❓ FAQ
|
|
174
|
+
|
|
175
|
+
**¿Puedo usar mi propio serializador?**
|
|
176
|
+
Sí. Pasa serializer: { serialize: (log) => "mi-línea" }. Debe devolver una sola línea sin `\n`.
|
|
177
|
+
|
|
178
|
+
**¿Qué pasa si el proceso se detiene en medio de un write?**
|
|
179
|
+
Usamos los mecanismos de `Writable` (buffer + `drain`) y exponemos `flush()`/`dispose()` para el cierre. Llama a ambos en shutdown.
|
|
180
|
+
|
|
181
|
+
**¿Puedo rotar por hora?**
|
|
182
|
+
No out-of-the-box. Puedes implementar un **FileNamePattern** por hora (ej. `app-{yyyy}{MM}{dd}-{HH}.log`) + un `RotateIfNeededUseCase` extendido.
|
|
183
|
+
|
|
184
|
+
---
|
|
289
185
|
|
|
290
186
|
## 📄 Licencia
|
|
291
187
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./save-log.dto";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./save-log.dto"), exports);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ILogDatasource, ILog } from "@jmlq/logger";
|
|
2
|
+
import { PersistLogUseCase } from "../use-cases";
|
|
3
|
+
export declare class FsDatasourceService implements ILogDatasource {
|
|
4
|
+
private readonly persistUC;
|
|
5
|
+
constructor(persistUC: PersistLogUseCase);
|
|
6
|
+
save(log: ILog): Promise<void>;
|
|
7
|
+
flush(): Promise<void>;
|
|
8
|
+
dispose(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FsDatasourceService = void 0;
|
|
4
|
+
class FsDatasourceService {
|
|
5
|
+
constructor(persistUC) {
|
|
6
|
+
this.persistUC = persistUC;
|
|
7
|
+
}
|
|
8
|
+
// Guarda un log (core llama esto). Orquesta vía PersistLogUseCase.
|
|
9
|
+
async save(log) {
|
|
10
|
+
const dto = { log };
|
|
11
|
+
await this.persistUC.execute(dto);
|
|
12
|
+
}
|
|
13
|
+
// Espera drenaje del buffer (útil para shutdown)
|
|
14
|
+
async flush() {
|
|
15
|
+
await this.persistUC.flush();
|
|
16
|
+
}
|
|
17
|
+
// Cierra recursos (stream)
|
|
18
|
+
async dispose() {
|
|
19
|
+
await this.persistUC.dispose();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.FsDatasourceService = FsDatasourceService;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./fs-datasource.service";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./fs-datasource.service"), exports);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { IFsSerializer, IStreamWriterPort } from "../../domain/contracts";
|
|
2
|
+
import { SaveLogDTO } from "../dto";
|
|
3
|
+
export declare class AppendLogUseCase {
|
|
4
|
+
private readonly serializer;
|
|
5
|
+
constructor(serializer: IFsSerializer);
|
|
6
|
+
execute(input: SaveLogDTO, writer: IStreamWriterPort): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Serializa (línea + '\n') y escribe en el stream con manejo de backpressure.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.AppendLogUseCase = void 0;
|
|
5
|
+
class AppendLogUseCase {
|
|
6
|
+
constructor(serializer) {
|
|
7
|
+
this.serializer = serializer;
|
|
8
|
+
}
|
|
9
|
+
async execute(input, writer) {
|
|
10
|
+
// 1) Serializa el log a texto + salto de línea
|
|
11
|
+
const line = this.serializer.serialize(input.log) + "\n";
|
|
12
|
+
// 2) Intenta escribir; si devuelve false, espera 'drain'
|
|
13
|
+
const ok = writer.write(line);
|
|
14
|
+
if (!ok) {
|
|
15
|
+
await new Promise((resolve) => writer.once("drain", resolve));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.AppendLogUseCase = AppendLogUseCase;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./rotate-if-needed.usecase"), exports);
|
|
18
|
+
__exportStar(require("./append-log.usecase"), exports);
|
|
19
|
+
__exportStar(require("./persist-log.usecase"), exports);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AppendLogUseCase, RotateIfNeededUseCase } from ".";
|
|
2
|
+
import { IFileRotatorPort, IStreamWriterPort } from "../../domain/contracts";
|
|
3
|
+
import { SaveLogDTO } from "../dto";
|
|
4
|
+
export declare class PersistLogUseCase {
|
|
5
|
+
private readonly rotateUC;
|
|
6
|
+
private readonly appendUC;
|
|
7
|
+
private readonly rotator;
|
|
8
|
+
private readonly openWriter;
|
|
9
|
+
private readonly onRotate?;
|
|
10
|
+
private readonly onError?;
|
|
11
|
+
constructor(rotateUC: RotateIfNeededUseCase, // caso de uso de rotación
|
|
12
|
+
appendUC: AppendLogUseCase, // caso de uso de escritura
|
|
13
|
+
rotator: IFileRotatorPort, // puerto para actualizar activePath
|
|
14
|
+
openWriter: (path: string) => IStreamWriterPort, // factory: abre stream
|
|
15
|
+
onRotate?: ((oldPath: string, newPath: string) => void | Promise<void>) | undefined, onError?: ((err: unknown) => void | Promise<void>) | undefined);
|
|
16
|
+
private writer;
|
|
17
|
+
private path;
|
|
18
|
+
private ensureWriter;
|
|
19
|
+
private disposeWriter;
|
|
20
|
+
execute(input: SaveLogDTO): Promise<void>;
|
|
21
|
+
flush(): Promise<void>;
|
|
22
|
+
dispose(): Promise<void>;
|
|
23
|
+
}
|