@jmlq/logger-plugin-fs 0.1.0-alpha.3 → 0.1.0-alpha.5

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 +158 -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 +16 -4
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,198 @@ 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”.
20
+ ### 📝 Resumen rápido
26
21
 
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
- ```
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`).
55
+
56
+ #### [VER MAS](./ARQUITECTURA.md)
57
+
58
+ ## 🧩 Configuración
34
59
 
35
- ### 📝 Resumen rápido
60
+ ### 🔐 Variables de Entorno (.env)
36
61
 
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.
62
+ ```ini
63
+ # Ruta base (carpeta) para los archivos de log
64
+ LOGGER_FS_PATH=./logs
40
65
 
41
- ---
66
+ # Patrón (opcional). Si omites, usa "app-{yyyy}{MM}{dd}.log"
67
+ LOGGER_FS_PATTERN=app-{yyyy}{MM}{dd}.log
42
68
 
43
- ### 🧩 Configuración
69
+ # Rotación: "day" | "size" | "none"
70
+ LOGGER_FS_ROTATION=day
71
+ LOGGER_FS_MAX_SIZE_MB=50
44
72
 
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):
73
+ # Logger core
74
+ LOGGER_LEVEL=info # trace|debug|info|warn|error|fatal
46
75
 
47
- ### 🔐 Variables de Entorno (.env)
76
+ ```
48
77
 
49
- ```ini
50
- # Archivo donde se guardarán los logs (crear carpeta si no existe)
51
- LOGGER_FS_PATH=./logs/app.log
78
+ ---
52
79
 
53
- # Nivel mínimo de log aceptado por el logger global
54
- # Valores: trace|debug|info|warn|error|fatal
55
- LOGGER_LEVEL=info
80
+ ### 🚀 Uso del paquete
56
81
 
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
82
+ ```tsx
83
+ import { createLogger, LogLevel } from "@jmlq/logger";
84
+ import { createFsDatasource } from "@jmlq/logger-plugin-fs";
85
+
86
+ const ds = createFsDatasource({
87
+ basePath: "./logs", // Carpeta destino
88
+ mkdir: true, // Crea carpeta si no existe
89
+ fileNamePattern: "app-{yyyy}{MM}{dd}.log", // Rotación diaria por fecha (UTC)
90
+ // Alternativa por tamaño:
91
+ // rotation: { by: "size", maxSizeMB: 50 }
92
+ rotation: { by: "day" },
93
+ // Serializador opcional (por defecto JSON.stringify)
94
+ // serializer: { serialize: (log) => formatMyLine(log) },
95
+ onRotate: (oldP, newP) => console.log("[fs] rotated:", oldP, "->", newP),
96
+ onError: (e) => console.error("[fs] error:", e),
97
+ });
98
+
99
+ const logger = createLogger(ds, { minLevel: LogLevel.INFO });
100
+
101
+ logger.info("Servidor iniciado", { pid: process.pid });
102
+
103
+ // Cierre elegante
104
+ process.on("SIGTERM", async () => {
105
+ await logger.flush?.();
106
+ await logger.dispose?.();
107
+ process.exit(0);
108
+ });
109
+ ```
110
+
111
+ También:
62
112
 
113
+ ```ts
114
+ import { createLogger, LogLevel } from "@jmlq/logger";
115
+ import {
116
+ createFsDatasource,
117
+ RotationPolicy,
118
+ FileNamePattern,
119
+ } from "@jmlq/logger-plugin-fs";
120
+
121
+ // Validación/invariantes tempranas con VO (throws si es inválido)
122
+ const pt = new FileNamePattern("app-{yyyy}{MM}{dd}.log");
123
+ const rt = new RotationPolicy("size", 50 /* maxSizeMB */);
124
+
125
+ const ds = createFsDatasource({
126
+ basePath: "./logs",
127
+ mkdir: true,
128
+ // La factory acepta primitives; usamos los VO arriba solo para validar/centrar la decisión
129
+ fileNamePattern: pt.pattern,
130
+ rotation: { by: rt.by, maxSizeMB: rotation.maxSizeMB },
131
+ });
132
+
133
+ const logger = createLogger(ds, { minLevel: LogLevel.INFO });
63
134
  ```
64
135
 
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.
136
+ #### ⚙️ Opciones soportadas
137
+
138
+ > - `basePath: string` – carpeta donde se guardan los logs.
139
+ > - `mkdir?: boolean` – crea la carpeta si no existe.
140
+ > - `fileNamePattern?: string` – tokens {yyyy}{MM}{dd} (formateados en UTC).
141
+ > > - Ejemplos: `"app-{yyyy}{MM}{dd}.log"`, `"service.log"`.
142
+ > - `rotation?: { by: "none" | "day" | "size"; maxSizeMB?: number; maxFiles?: number }`
143
+ > > - `day` → rota al cambiar la fecha (UTC).
144
+ > > - `size` → rota al alcanzar `maxSizeMB` (genera app.1.log, app.2.log, …).
145
+ > - `serializer?: { serialize(entry: unknown): string }` – una línea sin `\n`.
146
+ > - `onRotate?: (oldPath, newPath) => void | Promise<void>`
147
+ > - `onError?: (err) => void | Promise<void>`
66
148
 
67
149
  ---
68
150
 
69
- ### 🚀 Uso del paquete
151
+ ### 🗂️ Formato de archivo
70
152
 
71
- #### 1) Crea un wrapper: `rotating-fs.datasource.ts`
153
+ > - **Una línea por evento** (JSONL por defecto).
154
+ > - El `serializer` determina el formato de cada línea (sin salto).
155
+ > - El plugin agrega `"\n"` y maneja `backpressure`/`drain` del stream.
72
156
 
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
- ```
157
+ ---
197
158
 
198
- **Notas clave**
159
+ ### 🔁 Rotación
199
160
 
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.
161
+ **Por día **(`rotation: { by: "day" }`)
203
162
 
204
- #### 2) Configuración (solo FS)
163
+ > - El archivo activo se calcula con `fileNamePattern` usando la `fecha UTC actual`.
164
+ > - Si al persistir cambia el día → `cierra` el stream anterior, abre uno nuevo y dispara `onRotate(old, new)`.
205
165
 
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
- ```
166
+ **Por tamaño** (`rotation: { by: "size", maxSizeMB }`)
268
167
 
269
- #### 3) Comportamiento esperado
168
+ > - Comprueba el tamaño del archivo activo. Si `>= maxSizeMB` → rota a `app.1.log`, `app.2.log`, … (buscando el siguiente índice libre).
270
169
 
271
- Con `.env`
170
+ ---
272
171
 
273
- ```ini
274
- LOGGER_FS_PATH=./logs/app.log
172
+ ### 🧯 Backpressure & drain (cómo evita perder logs)
275
173
 
276
- ```
174
+ Node.js devuelve `false` en `stream.write()` cuando el **buffer interno está lleno** (backpressure).
175
+ El plugin:
277
176
 
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.
177
+ 1. Serializa el log + `"\n"`.
178
+ 2. Llama `write(...)`.
179
+ 3. Si devuelve `false`, **espera el evento** `drain` antes de continuar.
180
+ 4. `flush()` espera a que se libere el buffer si `writableNeedDrain` es `true`.
181
+ 5. `dispose()` cierra el stream actual drenando el buffer pendiente.
282
182
 
283
183
  ---
284
184
 
285
- #### Consideraciones y opciones
185
+ ### 🌐 Zona horaria
286
186
 
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.
187
+ > - El formateo `{yyyy}{MM}{dd}` del patrón se realiza en **UTC** para evitar desfases por huso horario (máquinas/CI distintas).
188
+ > - Si necesitas otro criterio (p.ej., “día de Guayaquil”), puedes:
189
+ > > - Ajustar el **IClock** para proveer el “ahora” en otro huso y/o
190
+ > > - Reemplazar el util de formateo si deseas `{…}` en local time.
191
+
192
+ ---
193
+
194
+ ## ❓ FAQ
195
+
196
+ **¿Puedo usar mi propio serializador?**
197
+ Sí. Pasa serializer: { serialize: (log) => "mi-línea" }. Debe devolver una sola línea sin `\n`.
198
+
199
+ **¿Qué pasa si el proceso se detiene en medio de un write?**
200
+ Usamos los mecanismos de `Writable` (buffer + `drain`) y exponemos `flush()`/`dispose()` para el cierre. Llama a ambos en shutdown.
201
+
202
+ **¿Puedo rotar por hora?**
203
+ No out-of-the-box. Puedes implementar un **FileNamePattern** por hora (ej. `app-{yyyy}{MM}{dd}-{HH}.log`) + un `RotateIfNeededUseCase` extendido.
204
+
205
+ ---
289
206
 
290
207
  ## 📄 Licencia
291
208
 
@@ -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
+ }