@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.
Files changed (60) hide show
  1. package/README.md +137 -241
  2. package/dist/application/dto/index.d.ts +1 -0
  3. package/dist/application/dto/index.js +17 -0
  4. package/dist/application/dto/save-log.dto.d.ts +4 -0
  5. package/dist/application/dto/save-log.dto.js +2 -0
  6. package/dist/application/services/fs-datasource.service.d.ts +9 -0
  7. package/dist/application/services/fs-datasource.service.js +22 -0
  8. package/dist/application/services/index.d.ts +1 -0
  9. package/dist/application/services/index.js +17 -0
  10. package/dist/application/use-cases/append-log.usecase.d.ts +7 -0
  11. package/dist/application/use-cases/append-log.usecase.js +19 -0
  12. package/dist/application/use-cases/index.d.ts +3 -0
  13. package/dist/application/use-cases/index.js +19 -0
  14. package/dist/application/use-cases/persist-log.usecase.d.ts +23 -0
  15. package/dist/application/use-cases/persist-log.usecase.js +74 -0
  16. package/dist/application/use-cases/rotate-if-needed.usecase.d.ts +10 -0
  17. package/dist/application/use-cases/rotate-if-needed.usecase.js +39 -0
  18. package/dist/domain/contracts/clock.contract.d.ts +3 -0
  19. package/dist/domain/contracts/clock.contract.js +2 -0
  20. package/dist/domain/contracts/file-rotator.contract.d.ts +7 -0
  21. package/dist/domain/contracts/file-rotator.contract.js +2 -0
  22. package/dist/domain/contracts/index.d.ts +4 -0
  23. package/dist/domain/contracts/index.js +20 -0
  24. package/dist/domain/contracts/serializer.contract.d.ts +3 -0
  25. package/dist/domain/contracts/serializer.contract.js +2 -0
  26. package/dist/domain/contracts/stream-writer.port.d.ts +6 -0
  27. package/dist/domain/contracts/stream-writer.port.js +2 -0
  28. package/dist/domain/types/index.d.ts +1 -0
  29. package/dist/domain/types/index.js +17 -0
  30. package/dist/domain/types/options.type.d.ts +11 -0
  31. package/dist/domain/types/options.type.js +5 -0
  32. package/dist/domain/value-objects/file-name-pattern.vo.d.ts +4 -0
  33. package/dist/domain/value-objects/file-name-pattern.vo.js +14 -0
  34. package/dist/domain/value-objects/index.d.ts +2 -0
  35. package/dist/domain/value-objects/index.js +18 -0
  36. package/dist/domain/value-objects/rotation-policy.vo.d.ts +7 -0
  37. package/dist/domain/value-objects/rotation-policy.vo.js +20 -0
  38. package/dist/index.d.ts +3 -13
  39. package/dist/index.js +7 -38
  40. package/dist/infrastructure/datasources/fs.datasource.d.ts +17 -0
  41. package/dist/infrastructure/datasources/fs.datasource.js +84 -0
  42. package/dist/infrastructure/datasources/index.d.ts +1 -0
  43. package/dist/infrastructure/datasources/index.js +17 -0
  44. package/dist/infrastructure/fs/file-rotator.adapter.d.ts +22 -0
  45. package/dist/infrastructure/fs/file-rotator.adapter.js +51 -0
  46. package/dist/infrastructure/fs/fs-provider.d.ts +15 -0
  47. package/dist/infrastructure/fs/fs-provider.js +55 -0
  48. package/dist/infrastructure/fs/fs-writer.adapter.d.ts +10 -0
  49. package/dist/infrastructure/fs/fs-writer.adapter.js +26 -0
  50. package/dist/infrastructure/fs/index.d.ts +5 -0
  51. package/dist/infrastructure/fs/index.js +21 -0
  52. package/dist/infrastructure/fs/node-clock.adapter.d.ts +4 -0
  53. package/dist/infrastructure/fs/node-clock.adapter.js +10 -0
  54. package/dist/infrastructure/fs/path-utils.d.ts +6 -0
  55. package/dist/infrastructure/fs/path-utils.js +26 -0
  56. package/dist/presentation/factory/create-fs-datasource.d.ts +15 -0
  57. package/dist/presentation/factory/create-fs-datasource.js +39 -0
  58. package/dist/presentation/factory/index.d.ts +1 -0
  59. package/dist/presentation/factory/index.js +17 -0
  60. package/package.json +15 -3
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @jmlq/logger-plugin-fs
2
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.
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`. Asegúrate de instalar ambos paquetes.
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
- > - **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.
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
- # Archivo donde se guardarán los logs (crear carpeta si no existe)
51
- LOGGER_FS_PATH=./logs/app.log
63
+ # Ruta base (carpeta) para los archivos de log
64
+ LOGGER_FS_PATH=./logs
52
65
 
53
- # Nivel mínimo de log aceptado por el logger global
54
- # Valores: trace|debug|info|warn|error|fatal
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
- # --- (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
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
- Sugerencia: crea el directorio `./logs` en tu entorno local o asegúrate de que el proceso tenga permisos de escritura en la ruta indicada.
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
- // 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
- }
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
- **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
- ```
115
+ #### Opciones soportadas
268
116
 
269
- #### 3) Comportamiento esperado
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
- Con `.env`
128
+ ---
272
129
 
273
- ```ini
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
- > - 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.
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
- #### Consideraciones y opciones
164
+ ### 🌐 Zona horaria
286
165
 
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.
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,4 @@
1
+ import type { ILog } from "@jmlq/logger";
2
+ export interface SaveLogDTO {
3
+ log: ILog;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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,3 @@
1
+ export * from "./rotate-if-needed.usecase";
2
+ export * from "./append-log.usecase";
3
+ export * from "./persist-log.usecase";
@@ -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
+ }