@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.
- package/README.md +158 -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 +16 -4
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,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
|
|
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
|
-
|
|
20
|
+
### 📝 Resumen rápido
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
###
|
|
60
|
+
### 🔐 Variables de Entorno (.env)
|
|
36
61
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
69
|
+
# Rotación: "day" | "size" | "none"
|
|
70
|
+
LOGGER_FS_ROTATION=day
|
|
71
|
+
LOGGER_FS_MAX_SIZE_MB=50
|
|
44
72
|
|
|
45
|
-
|
|
73
|
+
# Logger core
|
|
74
|
+
LOGGER_LEVEL=info # trace|debug|info|warn|error|fatal
|
|
46
75
|
|
|
47
|
-
|
|
76
|
+
```
|
|
48
77
|
|
|
49
|
-
|
|
50
|
-
# Archivo donde se guardarán los logs (crear carpeta si no existe)
|
|
51
|
-
LOGGER_FS_PATH=./logs/app.log
|
|
78
|
+
---
|
|
52
79
|
|
|
53
|
-
|
|
54
|
-
# Valores: trace|debug|info|warn|error|fatal
|
|
55
|
-
LOGGER_LEVEL=info
|
|
80
|
+
### 🚀 Uso del paquete
|
|
56
81
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
151
|
+
### 🗂️ Formato de archivo
|
|
70
152
|
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
+
### 🔁 Rotación
|
|
199
160
|
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
+
---
|
|
272
171
|
|
|
273
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
185
|
+
### 🌐 Zona horaria
|
|
286
186
|
|
|
287
|
-
> -
|
|
288
|
-
> -
|
|
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,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
|
+
}
|