logroller 2.0.1 → 2.1.0
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/CHANGELOG.md +52 -2
- package/README.md +232 -46
- package/dist/fsio.d.ts +3 -3
- package/dist/index.cjs +477 -132
- package/dist/index.d.ts +4 -2
- package/dist/index.esm.js +476 -133
- package/dist/parsers.d.ts +2 -0
- package/dist/rotate-file-stream.d.ts +54 -16
- package/dist/shutdown.d.ts +32 -0
- package/dist/types.d.ts +96 -66
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,57 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [2.0
|
|
8
|
+
## [2.1.0] — 2026-08-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `symlink: true | string` — stable "current" pointer (e.g. `app-current.log`)
|
|
13
|
+
for `tail -f`/Filebeat/promtail; atomically flipped on every segment open;
|
|
14
|
+
degrades gracefully on symlink-denying filesystems; excluded from retention.
|
|
15
|
+
- `maxTotalSize: "5g"` — byte-budget retention: deletes OLDEST archives until
|
|
16
|
+
under budget; composes with `maxFiles`.
|
|
17
|
+
- `onSeal` hook (+ `sealHookTimeoutMs`) — awaited after a segment is archived
|
|
18
|
+
and BEFORE retention can delete it: the S3/GCS upload extension point.
|
|
19
|
+
Errors/timeouts degrade to `warn`; rotation never stalls.
|
|
20
|
+
- `installShutdown(streams)` / `flushAll()` — graceful SIGINT/SIGTERM
|
|
21
|
+
handling that flushes buffered writes before exit (fixes the classic
|
|
22
|
+
"last log lines missing" support case).
|
|
23
|
+
- `stats()` and `listSegments()` — health snapshot and per-file listing.
|
|
24
|
+
- `compressionLevel` (0–9, default 6) — passes through to gzip.
|
|
25
|
+
|
|
26
|
+
## [2.0.2] — 2026-08-28
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **Corrupt audit manifest quarantine**: an unparseable `<stem>_audit.json`
|
|
31
|
+
(partial write, bad edit) is now renamed to `.corrupt` and preserved,
|
|
32
|
+
matching existing behaviour for version/family mismatches. Previously it
|
|
33
|
+
was silently overwritten by the first manifest save, destroying the
|
|
34
|
+
event journal.
|
|
35
|
+
- **Backward-clock safety**: a clock moving backward (NTP correction,
|
|
36
|
+
manual change) no longer reopens a past log family or forks the segment
|
|
37
|
+
series. The stream keeps the current segment, warns once, and resumes
|
|
38
|
+
normal rotation when the clock catches up.
|
|
39
|
+
- **Timer anti-spin**: with the rotation boundary in the past (backward
|
|
40
|
+
clock), the timer now polls at 1 s instead of firing every 100 ms,
|
|
41
|
+
which previously could keep processes from exiting cleanly.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **Construction-time pattern warning**: warns when `frequency` is finer
|
|
46
|
+
than the `datePattern` tokens allow (`daily`→`DD`, sub-daily→`HH`,
|
|
47
|
+
sub-minute→`mm`), preventing silent filename collisions where many
|
|
48
|
+
rotations overwrite a single file.
|
|
49
|
+
|
|
50
|
+
### Tests
|
|
51
|
+
|
|
52
|
+
- Suite expanded to 23 tests: interrupted-gzip repair, manifest divergence
|
|
53
|
+
healing (`archive-regressed`, `vanished`), manifest quarantine,
|
|
54
|
+
concurrent-writer detection, IST-midnight boundary, idle `period` slide,
|
|
55
|
+
`rotateNow()`, over-max resume, backward-clock guard, and coarse-pattern
|
|
56
|
+
warning.
|
|
57
|
+
|
|
58
|
+
## [2.0.1] — 2026-08-27
|
|
9
59
|
|
|
10
60
|
### Fixed
|
|
11
61
|
|
|
@@ -42,7 +92,7 @@ TypeScript consumers gain full type coverage.
|
|
|
42
92
|
- Modular source layout (`src/types.ts`, `parsers.ts`, `time.ts`, `fsio.ts`,
|
|
43
93
|
`rotate-file-stream.ts`) with `.js` extension imports so emitted
|
|
44
94
|
declarations resolve under `nodenext` consumers.
|
|
45
|
-
- Developer toolchain:
|
|
95
|
+
- Developer toolchain: Prettier, `npm run verify` gate (typecheck + build + tests).
|
|
46
96
|
- GitHub Actions: CI matrix on Node 18/20/22, release-triggered publish with
|
|
47
97
|
npm provenance (`id-token: write`, `--provenance`).
|
|
48
98
|
- Expanded README: comparison table, FAQ, restart-resume matrix, pino recipe.
|
package/README.md
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
## README.md
|
|
2
|
-
|
|
3
1
|
<p align="center"><strong>logroller</strong></p>
|
|
4
2
|
<h3 align="center">Zero-dependency rotating file streams for Node.js</h3>
|
|
5
3
|
|
|
@@ -14,8 +12,8 @@
|
|
|
14
12
|
|
|
15
13
|
---
|
|
16
14
|
|
|
17
|
-
`logroller` turns a plain append destination into a resilient, observable
|
|
18
|
-
|
|
15
|
+
`logroller` turns a plain append destination into a resilient, observable log
|
|
16
|
+
sink: **it is a Writable**, so `readable.pipe(log)` works, pino multistreams
|
|
19
17
|
work, anything that consumes streams works.
|
|
20
18
|
|
|
21
19
|
```bash
|
|
@@ -27,16 +25,24 @@ npm install logroller
|
|
|
27
25
|
- **Daily & interval rotation** — exact local midnight in any IANA zone,
|
|
28
26
|
or epoch-aligned intervals (`30s`, `5m`, `2h`)
|
|
29
27
|
- **Size caps** — `"20m"`-style limits roll numbered segments
|
|
30
|
-
- **Gzip archives** — sealed segments compressed atomically
|
|
31
|
-
- **
|
|
28
|
+
- **Gzip archives** — sealed segments compressed atomically, level 0–9
|
|
29
|
+
- **Stable "current" path** — a symlink log collectors can follow
|
|
30
|
+
(`tail -F app-current.log`, Filebeat, promtail) while files rotate beneath
|
|
31
|
+
- **Retention three ways** — by age, by count, **or by total byte budget**
|
|
32
|
+
("never exceed 5 GB of archives")
|
|
33
|
+
- **Upload pipeline** — an awaited `onSeal` hook that fires after a segment
|
|
34
|
+
is archived and _before_ retention can delete it: ship to S3/GCS safely
|
|
35
|
+
- **Graceful shutdown** — one helper flushes buffered writes on
|
|
36
|
+
SIGINT/SIGTERM (no more missing last lines)
|
|
32
37
|
- **Timezone-correct** — `Asia/Kolkata` means IST filenames, on a UTC box
|
|
33
|
-
- **Crash-safe** — restarts resume mid-series byte-exactly;
|
|
34
|
-
|
|
35
|
-
- **Observability** — `<stem>_audit.json` manifest with a uuid-tagged
|
|
36
|
-
|
|
38
|
+
- **Crash-safe** — restarts resume mid-series byte-exactly; gzips
|
|
39
|
+
interrupted by a kill are completed on boot
|
|
40
|
+
- **Observability** — `<stem>_audit.json` manifest with a uuid-tagged event
|
|
41
|
+
journal, plus `stats()` / `listSegments()` introspection
|
|
37
42
|
- **Testable time** — virtual clocks (`addHours`, `addHoursEveryMin`,
|
|
38
43
|
`advanceClock()`) let a day pass per minute, or instantly
|
|
39
44
|
- **Types first** — tuple-typed events, strict mode, dual ESM/CJS build
|
|
45
|
+
- **Zero dependencies**
|
|
40
46
|
|
|
41
47
|
## Quick start
|
|
42
48
|
|
|
@@ -62,6 +68,25 @@ log.on("rotated", ({ reason, archive, newFile }) =>
|
|
|
62
68
|
process.on("exit", () => log.end());
|
|
63
69
|
```
|
|
64
70
|
|
|
71
|
+
### Production shape — collectors, budget, uploads, shutdown
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { createStream, installShutdown } from "logroller";
|
|
75
|
+
|
|
76
|
+
const log = createStream({
|
|
77
|
+
filename: "logs/app-%DATE%.log",
|
|
78
|
+
zippedArchive: true,
|
|
79
|
+
maxSize: "20m",
|
|
80
|
+
maxTotalSize: "5g", // never hold more than 5 GB of archives
|
|
81
|
+
maxFiles: "90d",
|
|
82
|
+
symlink: true, // logs/app-current.log always points at the live file
|
|
83
|
+
onSeal: async ({ gz }) => uploadToS3(gz), // awaited BEFORE retention deletes
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// SIGINT/SIGTERM → flush buffered writes, then exit
|
|
87
|
+
installShutdown(log);
|
|
88
|
+
```
|
|
89
|
+
|
|
65
90
|
### CommonJS
|
|
66
91
|
|
|
67
92
|
```js
|
|
@@ -81,15 +106,37 @@ const logger = pino(
|
|
|
81
106
|
|
|
82
107
|
## What lands on disk
|
|
83
108
|
|
|
109
|
+
Example snapshot for `filename: "logs/app-%DATE%.log"` on **Aug 28**, with
|
|
110
|
+
`zippedArchive: true` and `symlink: true`:
|
|
111
|
+
|
|
84
112
|
```
|
|
85
|
-
app-2026-08-
|
|
86
|
-
app-2026-08-27.log.gz ← sealed (hit 20 MB)
|
|
87
|
-
app-2026-08-27.1.log
|
|
88
|
-
app-2026-08-
|
|
89
|
-
app-2026-08-28.log ← fresh day (in your tz), clean series
|
|
113
|
+
app-current.log ← symlink → app-2026-08-28.log (only with symlink: true)
|
|
114
|
+
app-2026-08-27.log.gz ← yesterday, sealed (hit 20 MB)
|
|
115
|
+
app-2026-08-27.1.log.gz ← yesterday, sealed too
|
|
116
|
+
app-2026-08-28.log ← ACTIVE — the only raw file on disk
|
|
90
117
|
app_audit.json ← library-owned manifest
|
|
91
118
|
```
|
|
92
119
|
|
|
120
|
+
Three invariants that hold at every moment:
|
|
121
|
+
|
|
122
|
+
1. **Exactly one raw file** — the active segment. Everything else is a
|
|
123
|
+
`.gz` archive (or a raw tail kept only because gzip failed — with a
|
|
124
|
+
`warn` event, never silently).
|
|
125
|
+
2. **A segment's `.log` and `.log.gz` never coexist.** The raw file is
|
|
126
|
+
deleted only _after_ its archive is verified.
|
|
127
|
+
3. **`app-current.log` always points at the active segment**, flipped
|
|
128
|
+
atomically on every open. It exists only when `symlink` is enabled, and
|
|
129
|
+
retention never touches it.
|
|
130
|
+
|
|
131
|
+
### How a day evolves (all states, in order)
|
|
132
|
+
|
|
133
|
+
| Moment (tz) | Directory contains | `app-current.log` → |
|
|
134
|
+
| ------------------------- | ----------------------------------------- | ---------------------------------------------------------------- |
|
|
135
|
+
| morning, first write | `…27.log` | `…27.log` |
|
|
136
|
+
| hit 20 MB → roll | `…27.log.gz` + `…27.1.log` | `…27.1.log` |
|
|
137
|
+
| `.1` fills → roll | `…27.log.gz` + `…27.1.log.gz` | _(stale until next write — segments open lazily on first write)_ |
|
|
138
|
+
| midnight → Aug 28 + write | `…27.log.gz` + `…27.1.log.gz` + `…28.log` | `…28.log` |
|
|
139
|
+
|
|
93
140
|
A killed process resumes without gaps or duplicates:
|
|
94
141
|
|
|
95
142
|
| Disk at crash | Restart continues into |
|
|
@@ -98,6 +145,106 @@ A killed process resumes without gaps or duplicates:
|
|
|
98
145
|
| `app-D.log.gz` + `app-D.1.log` | `app-D.1.log` |
|
|
99
146
|
| `app-D.1.log.gz` (chain sealed) | `app-D.2.log` |
|
|
100
147
|
| died during a gzip | boot finishes the gzip, then proceeds |
|
|
148
|
+
| crash left yesterday's tail raw | boot seals (gzips) it automatically |
|
|
149
|
+
|
|
150
|
+
## Log collectors & the stable current path
|
|
151
|
+
|
|
152
|
+
Rotating filenames are great for humans, hostile to consumers: `tail -f`,
|
|
153
|
+
Filebeat, Fluent Bit and promtail want **one path that never changes**.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const log = createStream({
|
|
157
|
+
filename: "logs/app-%DATE%.log",
|
|
158
|
+
symlink: true, // → logs/app-current.log
|
|
159
|
+
// symlink: "CURRENT", // → logs/CURRENT (custom name)
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
tail -F logs/app-current.log # survives every rotation (capital -F: re-resolve the link)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- The pointer is flipped **atomically** (tmp + rename) on every segment open,
|
|
168
|
+
so followers never observe a gap or a half-written name.
|
|
169
|
+
- The target is stored **relative**, so the directory stays portable.
|
|
170
|
+
- Best-effort by design: on filesystems that deny symlinks (Windows without
|
|
171
|
+
Developer Mode) the stream warns once and continues without it.
|
|
172
|
+
- The pointer is never touched by retention and never reported as a segment.
|
|
173
|
+
|
|
174
|
+
## Upload pipeline — archive, ship, _then_ delete
|
|
175
|
+
|
|
176
|
+
`onSeal` runs inside the rotation queue **after** the segment is compressed
|
|
177
|
+
and **before** retention can remove it — the safe window for shipping:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const log = createStream({
|
|
181
|
+
filename: "logs/cdr-%DATE%.log",
|
|
182
|
+
zippedArchive: true,
|
|
183
|
+
maxFiles: "7d", // local copies are disposable
|
|
184
|
+
onSeal: async ({ raw, gz }) => {
|
|
185
|
+
await uploadToS3(gz ?? raw); // awaited — rotation waits
|
|
186
|
+
},
|
|
187
|
+
sealHookTimeoutMs: 60_000, // a wedged upload warns instead of stalling
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Guarantees:
|
|
192
|
+
|
|
193
|
+
- invoked exactly once per sealed segment, with `{ raw, gz }` (`gz: null`
|
|
194
|
+
when compression is off or gzip failed)
|
|
195
|
+
- **ordering**: hook → then retention sweep, so an in-flight upload can never
|
|
196
|
+
race a deletion
|
|
197
|
+
- a throwing or timed-out hook degrades to a `warn` event — logging and
|
|
198
|
+
rotation continue
|
|
199
|
+
|
|
200
|
+
## Graceful shutdown
|
|
201
|
+
|
|
202
|
+
The classic support case: SIGTERM arrives, buffered lines are lost. One call
|
|
203
|
+
ends it:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { createStream, installShutdown } from "logroller";
|
|
207
|
+
|
|
208
|
+
const log = createStream({ filename: "logs/app-%DATE%.log" });
|
|
209
|
+
installShutdown(log); // SIGINT + SIGTERM → end() → exit 0
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- `installShutdown(stream | streams, { signals?, timeoutMs?, code? })` —
|
|
213
|
+
installs process listeners once; streams accumulate in a registry.
|
|
214
|
+
- On signal: every registered stream is ended (buffered writes flushed,
|
|
215
|
+
audit manifest finalized), with a hard `timeoutMs` deadline (default 5 s).
|
|
216
|
+
- `flushAll()` is the same graceful end **without exiting** — for tests and
|
|
217
|
+
custom signal handling.
|
|
218
|
+
|
|
219
|
+
## Introspection
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const st = log.stats();
|
|
223
|
+
// { dir, currentStamp, activeFile, activeIndex, segments, archived,
|
|
224
|
+
// totalBytes, clockOffsetMs, symlink }
|
|
225
|
+
|
|
226
|
+
for (const seg of log.listSegments()) {
|
|
227
|
+
// { file, stamp, index, gzipped, bytes, state, openedAt?, archivedAt? }
|
|
228
|
+
console.log(seg.stamp, seg.index, seg.gzipped ? "gz" : "raw", seg.bytes);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`listSegments()` reads the **directory** (disk is truth), so it also reports
|
|
233
|
+
files the manifest doesn't know (state `"untracked"`). Perfect for
|
|
234
|
+
`/healthz` endpoints, dashboards, and "where did my logs go" support.
|
|
235
|
+
|
|
236
|
+
## Retention — three policies, composable
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
maxFiles: "90d" // age: delete archives older than 90 days
|
|
240
|
+
maxFiles: 500 // count: keep the newest 500 archives
|
|
241
|
+
maxTotalSize: "5g" // budget: delete OLDEST archives until under 5 GB
|
|
242
|
+
maxFiles: "30d",
|
|
243
|
+
maxTotalSize: "2g", // combined: either policy may delete
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Unset means files live forever until you delete them. The active segment is
|
|
247
|
+
never a retention candidate, and the audit/symlink files are always reserved.
|
|
101
248
|
|
|
102
249
|
## API
|
|
103
250
|
|
|
@@ -110,8 +257,13 @@ A killed process resumes without gaps or duplicates:
|
|
|
110
257
|
| `filename` | **required**, must contain `%DATE%` | — |
|
|
111
258
|
| `datePattern` | `YYYY YY MM DD HH mm ss` | `YYYY-MM-DD` |
|
|
112
259
|
| `zippedArchive` | boolean | `false` |
|
|
260
|
+
| `compressionLevel` | gzip level `0`–`9` (0 = stored) | `6` |
|
|
113
261
|
| `maxSize` | `20m`, `512k`, `2g`, bytes, `0`=off | off |
|
|
114
262
|
| `maxFiles` | `"90d"` age, `500` count, unset=never | — |
|
|
263
|
+
| `maxTotalSize` | `"5g"`, `"500m"`, bytes, `0`=off | off |
|
|
264
|
+
| `symlink` | `true` (auto name) or custom string | off |
|
|
265
|
+
| `onSeal` | `async ({ raw, gz }) => void` | — |
|
|
266
|
+
| `sealHookTimeoutMs` | hard deadline for one `onSeal` call | `30000` |
|
|
115
267
|
| `frequency` | `daily`, `30s`, `5m`, `2h`, ms | `daily` |
|
|
116
268
|
| `tz` | any IANA zone | `UTC` |
|
|
117
269
|
| `audit` / `auditFile` | manifest control | `true` / auto-name |
|
|
@@ -122,24 +274,33 @@ A killed process resumes without gaps or duplicates:
|
|
|
122
274
|
|
|
123
275
|
### Methods
|
|
124
276
|
|
|
125
|
-
| Method | Returns | Purpose
|
|
126
|
-
| ----------------------- | ---------------- |
|
|
127
|
-
| `write / end / destroy` | stream semantics | normal `Writable`
|
|
128
|
-
| `rotateNow()` | `Promise<void>` | force a rotation
|
|
129
|
-
| `advanceClock()` | `Promise<void>` | apply one virtual tick now
|
|
277
|
+
| Method | Returns | Purpose |
|
|
278
|
+
| ----------------------- | ---------------- | ------------------------------- |
|
|
279
|
+
| `write / end / destroy` | stream semantics | normal `Writable` |
|
|
280
|
+
| `rotateNow()` | `Promise<void>` | force a rotation |
|
|
281
|
+
| `advanceClock()` | `Promise<void>` | apply one virtual tick now |
|
|
282
|
+
| `stats()` | `StreamStats` | health snapshot |
|
|
283
|
+
| `listSegments()` | `SegmentInfo[]` | every family file, oldest first |
|
|
284
|
+
|
|
285
|
+
### Module functions
|
|
286
|
+
|
|
287
|
+
| Function | Purpose |
|
|
288
|
+
| --------------------------------- | ------------------------------------------------------------ |
|
|
289
|
+
| `installShutdown(streams, opts?)` | register streams for SIGINT/SIGTERM flush; returns `flush()` |
|
|
290
|
+
| `flushAll(timeoutMs?)` | gracefully end all registered streams, no exit |
|
|
130
291
|
|
|
131
292
|
### Events (typed)
|
|
132
293
|
|
|
133
|
-
| Event | Payload | Meaning
|
|
134
|
-
| --------- | ------------------------------------- |
|
|
135
|
-
| `open` | `file` | segment opened for appending
|
|
136
|
-
| `rotated` | `{reason, oldFile, archive, newFile}` | segment closed
|
|
137
|
-
| `archive` | `gzFile` | gzip finished
|
|
138
|
-
| `deleted` | `file` | retention removed it
|
|
139
|
-
| `period` | `stamp` | boundary crossed while idle
|
|
140
|
-
| `clock` | `{offsetMs, …}` | virtual clock engaged
|
|
141
|
-
| `warn` | `Error` | non-fatal
|
|
142
|
-
| `error` | `Error` | fatal write problems
|
|
294
|
+
| Event | Payload | Meaning |
|
|
295
|
+
| --------- | ------------------------------------- | ------------------------------------------------------------ |
|
|
296
|
+
| `open` | `file` | segment opened for appending |
|
|
297
|
+
| `rotated` | `{reason, oldFile, archive, newFile}` | segment closed |
|
|
298
|
+
| `archive` | `gzFile` | gzip finished |
|
|
299
|
+
| `deleted` | `file` | retention removed it |
|
|
300
|
+
| `period` | `stamp` | boundary crossed while idle |
|
|
301
|
+
| `clock` | `{offsetMs, …}` | virtual clock engaged |
|
|
302
|
+
| `warn` | `Error` | non-fatal (hook failure, gzip retry, symlink unavailable, …) |
|
|
303
|
+
| `error` | `Error` | fatal write problems |
|
|
143
304
|
|
|
144
305
|
Errors must be handled — attach a listener on every instance.
|
|
145
306
|
|
|
@@ -149,46 +310,71 @@ Errors must be handled — attach a listener on every instance.
|
|
|
149
310
|
const s = createStream({
|
|
150
311
|
filename: "logs/demo-%DATE%.log",
|
|
151
312
|
zippedArchive: true,
|
|
152
|
-
|
|
313
|
+
addHours: "-48h", // start two days back
|
|
314
|
+
addHoursEveryMin: "24h", // one virtual day per real minute
|
|
153
315
|
clockStepIntervalMs: 1000, // or faster
|
|
154
316
|
});
|
|
155
317
|
await s.advanceClock(); // jump instantly when useful
|
|
156
318
|
```
|
|
157
319
|
|
|
320
|
+
Negative `addHoursEveryMin` deliberately triggers the backward-clock guard
|
|
321
|
+
(warn, no rotation) — it exists so you can test that safety net too.
|
|
322
|
+
|
|
158
323
|
## FAQ
|
|
159
324
|
|
|
160
325
|
**Why is `%DATE%` mandatory?**
|
|
161
326
|
It is the anchor that decouples _rotation scheduling_ from _filenames_.
|
|
162
327
|
Coarser patterns than your frequency collapse distinct segments onto one
|
|
163
|
-
name — match tokens to cadence (`HH` for hourly, etc.).
|
|
328
|
+
name — match tokens to cadence (`HH` for hourly, etc.). The library warns at
|
|
329
|
+
construction when it detects a mismatch.
|
|
330
|
+
|
|
331
|
+
**How do I ship logs to S3 without losing any?**
|
|
332
|
+
`zippedArchive: true` + `onSeal` (see the upload pipeline section). The hook
|
|
333
|
+
is awaited and runs before retention, so an in-flight upload can never race
|
|
334
|
+
a deletion.
|
|
335
|
+
|
|
336
|
+
**How do I keep `tail -f` working across rotations?**
|
|
337
|
+
`symlink: true` — follow `app-current.log`. Use `tail -F` (capital) so the
|
|
338
|
+
follower re-resolves the link after it flips.
|
|
164
339
|
|
|
165
340
|
**Can two processes share one directory?**
|
|
166
|
-
Different prefixes/families: yes. The _same_ family: no — the audit
|
|
167
|
-
|
|
341
|
+
Different prefixes/families: yes. The _same_ family: no — the audit manifest
|
|
342
|
+
detects a live second writer (`writer.heartbeat`) and warns loudly.
|
|
168
343
|
|
|
169
344
|
**Is the audit JSON required?**
|
|
170
345
|
No — `audit: false` reduces behaviour to pure disk-scan recovery. Recommended
|
|
171
|
-
to leave on: it repairs interrupted gzips
|
|
346
|
+
to leave on: it repairs interrupted gzips, demotes zombie records, and
|
|
347
|
+
powers `listSegments()` states.
|
|
348
|
+
|
|
349
|
+
**What if the system clock jumps backward?**
|
|
350
|
+
The stream keeps the current segment, warns once, and resumes normal
|
|
351
|
+
rotation when the clock catches up — the series is never forked into a past
|
|
352
|
+
family. (Requires zero-padded `datePattern` tokens, which all presets are.)
|
|
172
353
|
|
|
173
354
|
**Windows / macOS / Linux?**
|
|
174
355
|
Pure Node APIs; path handling via `node:path`. All three supported
|
|
175
|
-
(Node `>=16.14`).
|
|
356
|
+
(Node `>=16.14`). Symlinks are best-effort on Windows — see above.
|
|
176
357
|
|
|
177
358
|
## Compare
|
|
178
359
|
|
|
179
|
-
|
|
|
180
|
-
|
|
|
181
|
-
| Dependencies
|
|
182
|
-
| Crash-safe resume
|
|
183
|
-
| Interrupted-gzip repair
|
|
184
|
-
| Event journal
|
|
185
|
-
| TZ-correct daily rollover
|
|
186
|
-
|
|
|
187
|
-
|
|
|
360
|
+
| | logroller | file-stream-rotator | rotating-file-stream | winston-daily-rotate-file |
|
|
361
|
+
| --------------------------------- | ----------------- | ------------------- | -------------------- | ------------------------- |
|
|
362
|
+
| Dependencies | **0** | 4+ | 2+ | winston-bound |
|
|
363
|
+
| Crash-safe resume | ✅ audit-assisted | partial | partial | ✖ |
|
|
364
|
+
| Interrupted-gzip repair | ✅ | ✖ | ✖ | ✖ |
|
|
365
|
+
| Event journal | ✅ | partial | ✖ | ✖ |
|
|
366
|
+
| TZ-correct daily rollover | ✅ | partial | ✅ | ✅ |
|
|
367
|
+
| Stable current path | ✅ atomic | ✅ | ✅ | ✅ |
|
|
368
|
+
| Byte-budget retention | ✅ | ✅ (bytes only) | ✖ | ✖ |
|
|
369
|
+
| Awaited upload-before-delete hook | ✅ | ✖ | ✖ | ✖ |
|
|
370
|
+
| Graceful shutdown helper | ✅ | ✖ | ✖ | via winston |
|
|
371
|
+
| Introspection API | ✅ | ✖ | ✖ | partial |
|
|
372
|
+
| Virtual test clock | ✅ | ✖ | ✖ | ✖ |
|
|
373
|
+
| Winston required | no | no | no | yes |
|
|
188
374
|
|
|
189
375
|
## Contributing
|
|
190
376
|
|
|
191
|
-
PRs welcome. `npm run verify` gates every change (typecheck + tests).
|
|
377
|
+
PRs welcome. `npm run verify` gates every change (lint + typecheck + tests).
|
|
192
378
|
Please file issues before large refactors.
|
|
193
379
|
|
|
194
380
|
## License
|
package/dist/fsio.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ import fs from "node:fs";
|
|
|
3
3
|
export declare const statSizeSafe: (p: string | null | undefined) => number;
|
|
4
4
|
export declare const existsSafe: (p: string) => boolean;
|
|
5
5
|
/**
|
|
6
|
-
* Compress `src` → `dst`, deleting `src` ONLY on verified
|
|
7
|
-
* On failure removes the partial `dst` and reports via callback —
|
|
6
|
+
* Compress `src` → `dst` at gzip `level`, deleting `src` ONLY on verified
|
|
7
|
+
* success. On failure removes the partial `dst` and reports via callback —
|
|
8
8
|
* logs degrade to uncompressed rather than losing data.
|
|
9
9
|
*/
|
|
10
|
-
export declare function gzipMove(src: string, dst: string, onWarn?: (err: Error) => void): Promise<boolean>;
|
|
10
|
+
export declare function gzipMove(src: string, dst: string, onWarn?: (err: Error) => void, level?: number): Promise<boolean>;
|
|
11
11
|
export declare const endStream: (ws: fs.WriteStream | null) => Promise<void>;
|