logroller 1.0.0 → 2.0.1
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 +101 -0
- package/README.md +144 -95
- package/dist/fsio.d.ts +11 -0
- package/dist/index.cjs +564 -448
- package/dist/index.d.ts +7 -140
- package/dist/index.esm.js +564 -448
- package/dist/parsers.d.ts +16 -0
- package/dist/rotate-file-stream.d.ts +121 -0
- package/dist/time.d.ts +13 -0
- package/dist/types.d.ts +129 -0
- package/package.json +31 -15
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [2.0.1] — 2026-09-0X
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Reactivated segments no longer inherit stale lifecycle timestamps
|
|
13
|
+
(`abandonedAt` could predate `openedAt`).
|
|
14
|
+
- Raw tails of already-passed periods are now gzipped at boot
|
|
15
|
+
(crash-near-midnight no longer leaves yesterday's file uncompressed).
|
|
16
|
+
- Rotation adopts a `.gz` produced by a (warned-about) concurrent writer
|
|
17
|
+
instead of recording a false `drop-empty` that orphaned the archive.
|
|
18
|
+
|
|
19
|
+
## [2.0.0] — 2026-08-28
|
|
20
|
+
|
|
21
|
+
Architecture release: the library moved from a single-file JavaScript SDK to a
|
|
22
|
+
modular **TypeScript** codebase with strict typings, first-class DX tooling,
|
|
23
|
+
and CI-gated releases. The public runtime API (`createStream`,
|
|
24
|
+
`RotateFileStream`, all options and events) remains compatible with 1.x;
|
|
25
|
+
TypeScript consumers gain full type coverage.
|
|
26
|
+
|
|
27
|
+
> ⚠️ Consumers on 1.x should re-run their smoke/integration tests after
|
|
28
|
+
> upgrading — mostly due to the deterministic boot-order fix below.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Full TypeScript source with shipped declarations** (`dist/index.d.ts`);
|
|
33
|
+
strict-mode compile (`noImplicitOverride`, `noUnusedLocals`,
|
|
34
|
+
`verbatimModuleSyntax`, …).
|
|
35
|
+
- **Tuple-typed events** via `RotateFileStreamEventMap` — `on("rotated", …)`
|
|
36
|
+
autocompletes and payload types are checked (`RotatedInfo`, `ClockInfo`),
|
|
37
|
+
while Node-core fallback overloads keep `.pipe()` structurally valid.
|
|
38
|
+
- `advanceClock()` — apply one virtual-clock tick immediately (previously the
|
|
39
|
+
step timer fired only every minute); makes day-crossing tests fully
|
|
40
|
+
deterministic.
|
|
41
|
+
- `clockStepIntervalMs` option — control how often `addHoursEveryMin` ticks.
|
|
42
|
+
- Modular source layout (`src/types.ts`, `parsers.ts`, `time.ts`, `fsio.ts`,
|
|
43
|
+
`rotate-file-stream.ts`) with `.js` extension imports so emitted
|
|
44
|
+
declarations resolve under `nodenext` consumers.
|
|
45
|
+
- Developer toolchain: Prettier, `npm run verify` gate (typecheck + build + tests).
|
|
46
|
+
- GitHub Actions: CI matrix on Node 18/20/22, release-triggered publish with
|
|
47
|
+
npm provenance (`id-token: write`, `--provenance`).
|
|
48
|
+
- Expanded README: comparison table, FAQ, restart-resume matrix, pino recipe.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- **Deterministic boot sequencing**: housekeeping (zombie demotion, disk
|
|
53
|
+
reconciliation, interrupted-gzip repair) is chained as job #1 of the
|
|
54
|
+
serial operation queue in the constructor instead of `process.nextTick`,
|
|
55
|
+
so a rapid first `write()` can no longer race a boot-time reopen or skip
|
|
56
|
+
zombie cleanup.
|
|
57
|
+
- Retention sweep skips `<audit>.corrupt` artifacts alongside `.tmp`.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- Internals restructured behind the same public exports; barrels re-export
|
|
62
|
+
types explicitly (`import type { RotatedInfo } from "logroller"`).
|
|
63
|
+
- Stricter package hygiene metadata: `sideEffects: false`, explicit
|
|
64
|
+
`./package.json` export, `files` whitelist (`dist`, docs, license,
|
|
65
|
+
changelog).
|
|
66
|
+
- Test suite runs against the built bundle and covers: base writing +
|
|
67
|
+
manifest creation, size rotation/gzip/indexing, restart resume,
|
|
68
|
+
`addHours` naming, virtual midnight crossing, count-retention ordering
|
|
69
|
+
(mtime-deterministic), zombie-record lifecycle (`abandon → active →
|
|
70
|
+
closed`), and CJS loadability through the exports map.
|
|
71
|
+
|
|
72
|
+
## [1.0.0] — 2026-08-26
|
|
73
|
+
|
|
74
|
+
Initial release. Single-file zero-dependency JavaScript SDK
|
|
75
|
+
(`file-rotation-sdk.js`) providing a Writable stream with scheduled file
|
|
76
|
+
rotation.
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- Time-based rotation: `frequency: "daily"` (local midnight in any IANA
|
|
81
|
+
timezone via `Intl`) or epoch-aligned intervals (`"30s"`, `"5m"`, `"2h"`).
|
|
82
|
+
- Size-based rotation: `maxSize` (`"20m"` / `"512k"` / raw bytes) producing
|
|
83
|
+
numbered segments — `app-D.log`, `app-D.1.log`, … — inserted between the
|
|
84
|
+
date stamp and the extension.
|
|
85
|
+
- Optional gzip archiving of sealed segments (`zippedArchive`), atomic tmp+
|
|
86
|
+
rename writes, source deleted only after verified compression success.
|
|
87
|
+
- Retention via `maxFiles`: age policy (`"90d"`) or count policy (`500`);
|
|
88
|
+
unset means files are never auto-deleted.
|
|
89
|
+
- Crash-safe restart resume from disk scan: raw tails are appended to,
|
|
90
|
+
sealed chains continue at the next free index; lazy period-roll catches
|
|
91
|
+
boundaries missed while suspended or shut down.
|
|
92
|
+
- Audit manifest (`<stem>_audit.json`): lifecycle records per file, capped
|
|
93
|
+
uuid-tagged event journal, `gzipPending` intent markers enabling automatic
|
|
94
|
+
repair of gzips interrupted by a crash, zombie active-record demotion,
|
|
95
|
+
disk-divergence healing (`archive-regressed`, `vanished`),
|
|
96
|
+
concurrent-writer detection via pid/bootId heartbeat.
|
|
97
|
+
- Virtual test clock: static shift (`addHours`) and per-minute stepping
|
|
98
|
+
(`addHoursEveryMin`).
|
|
99
|
+
- Dual-format distribution (ESM + CJS) bundled with Rollup; named exports
|
|
100
|
+
`createStream` and `RotateFileStream`; factory options mirroring common
|
|
101
|
+
rotator APIs; streams pipe-ready for pino multistreams.
|
package/README.md
CHANGED
|
@@ -1,19 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
## README.md
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
<p align="center"><strong>logroller</strong></p>
|
|
4
|
+
<h3 align="center">Zero-dependency rotating file streams for Node.js</h3>
|
|
5
|
+
|
|
6
|
+
<div align="center">
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
`logroller` turns a plain append destination into a resilient, observable
|
|
18
|
+
log sink: **it is a Writable**, so `readable.pipe(log)` works, pino multistreams
|
|
19
|
+
work, anything that consumes streams works.
|
|
7
20
|
|
|
8
21
|
```bash
|
|
9
22
|
npm install logroller
|
|
10
23
|
```
|
|
11
24
|
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- **Daily & interval rotation** — exact local midnight in any IANA zone,
|
|
28
|
+
or epoch-aligned intervals (`30s`, `5m`, `2h`)
|
|
29
|
+
- **Size caps** — `"20m"`-style limits roll numbered segments
|
|
30
|
+
- **Gzip archives** — sealed segments compressed atomically
|
|
31
|
+
- **Retention** — by age or count; unset means files live forever
|
|
32
|
+
- **Timezone-correct** — `Asia/Kolkata` means IST filenames, on a UTC box
|
|
33
|
+
- **Crash-safe** — restarts resume mid-series byte-exactly;
|
|
34
|
+
gzips interrupted by a kill are completed on boot
|
|
35
|
+
- **Observability** — `<stem>_audit.json` manifest with a uuid-tagged
|
|
36
|
+
event journal for every file's lifecycle
|
|
37
|
+
- **Testable time** — virtual clocks (`addHours`, `addHoursEveryMin`,
|
|
38
|
+
`advanceClock()`) let a day pass per minute, or instantly
|
|
39
|
+
- **Types first** — tuple-typed events, strict mode, dual ESM/CJS build
|
|
40
|
+
|
|
12
41
|
## Quick start
|
|
13
42
|
|
|
14
43
|
### ESM
|
|
15
44
|
|
|
16
|
-
```
|
|
45
|
+
```ts
|
|
17
46
|
import { createStream } from "logroller";
|
|
18
47
|
|
|
19
48
|
const log = createStream({
|
|
@@ -27,7 +56,10 @@ const log = createStream({
|
|
|
27
56
|
});
|
|
28
57
|
|
|
29
58
|
log.write("hello\n");
|
|
30
|
-
log.on("rotated", (
|
|
59
|
+
log.on("rotated", ({ reason, archive, newFile }) =>
|
|
60
|
+
console.log(`${reason}: ${archive ?? "-"} → ${newFile}`),
|
|
61
|
+
);
|
|
62
|
+
process.on("exit", () => log.end());
|
|
31
63
|
```
|
|
32
64
|
|
|
33
65
|
### CommonJS
|
|
@@ -36,112 +68,129 @@ log.on("rotated", (r) => console.log("rotated:", r));
|
|
|
36
68
|
const { createStream } = require("logroller");
|
|
37
69
|
```
|
|
38
70
|
|
|
39
|
-
###
|
|
71
|
+
### pino destination
|
|
40
72
|
|
|
41
73
|
```js
|
|
42
|
-
|
|
74
|
+
const logger = pino(
|
|
75
|
+
pino.multistream([
|
|
76
|
+
{ level: "info", stream: createStream({ filename: "logs/general-%DATE%.log" }) },
|
|
77
|
+
{ level: "error", stream: createStream({ filename: "logs/error-%DATE%.log" }) },
|
|
78
|
+
]),
|
|
79
|
+
);
|
|
43
80
|
```
|
|
44
81
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```js
|
|
48
|
-
import pino from "pino";
|
|
49
|
-
import { createStream } from "logroller";
|
|
50
|
-
|
|
51
|
-
const stream = createStream({
|
|
52
|
-
filename: "logs/general-%DATE%.log",
|
|
53
|
-
zippedArchive: true,
|
|
54
|
-
maxSize: "20m",
|
|
55
|
-
maxFiles: "90d",
|
|
56
|
-
tz: "Asia/Kolkata",
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const logger = pino(pino.multistream([
|
|
60
|
-
{ level: "info", stream: pino.transport({ target: "./pretty.js" }) },
|
|
61
|
-
{ level: "info", stream },
|
|
62
|
-
]));
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## Options
|
|
66
|
-
|
|
67
|
-
| Option | Values | Default |
|
|
68
|
-
|---|---|---|
|
|
69
|
-
| `filename` | must contain `%DATE%` | required |
|
|
70
|
-
| `datePattern` | `YYYY YY MM DD HH mm ss` | `YYYY-MM-DD` |
|
|
71
|
-
| `zippedArchive` | boolean | `false` |
|
|
72
|
-
| `maxSize` | `20m`, `1k`, `5g`, number of bytes, `0` = off | off |
|
|
73
|
-
| `maxFiles` | `"90d"` (age), `500` (count), unset = never delete | off |
|
|
74
|
-
| `frequency` | `"daily"`, `"1s"`, `"5m"`, `"2h"`, ms number | `"daily"` |
|
|
75
|
-
| `tz` | any IANA zone (`Intl`-based) | `"UTC"` |
|
|
76
|
-
| `audit` | write `<stem>_audit.json` manifest | `true` |
|
|
77
|
-
| `auditFile` | custom manifest name | `<stem>_audit.json` |
|
|
78
|
-
| `unrefTimers` | don't hold the process open | `false` |
|
|
79
|
-
| `addHours` | static clock shift (`"-6h"`) — **test only** | off |
|
|
80
|
-
| `addHoursEveryMin` | hours added per step (`"24h"`) — **test only** | off |
|
|
81
|
-
| `clockStepIntervalMs` | step interval for `addHoursEveryMin` | `60000` |
|
|
82
|
-
|
|
83
|
-
## File naming
|
|
82
|
+
## What lands on disk
|
|
84
83
|
|
|
85
84
|
```
|
|
86
85
|
app-2026-08-27.log ← active
|
|
87
|
-
app-2026-08-27.log.gz ← hit
|
|
88
|
-
app-2026-08-27.1.log ← next segment
|
|
89
|
-
app-2026-08-27.1.log.gz ←
|
|
90
|
-
app-2026-08-28.log ←
|
|
86
|
+
app-2026-08-27.log.gz ← sealed (hit 20 MB)
|
|
87
|
+
app-2026-08-27.1.log ← next segment mid-day
|
|
88
|
+
app-2026-08-27.1.log.gz ← sealed too
|
|
89
|
+
app-2026-08-28.log ← fresh day (in your tz), clean series
|
|
90
|
+
app_audit.json ← library-owned manifest
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
| Disk at crash
|
|
96
|
-
|
|
97
|
-
| `app-D.log`
|
|
98
|
-
| `app-D.log.gz` + `app-D.1.log`
|
|
99
|
-
| `app-D.1.log.gz`
|
|
100
|
-
|
|
|
101
|
-
|
|
93
|
+
A killed process resumes without gaps or duplicates:
|
|
94
|
+
|
|
95
|
+
| Disk at crash | Restart continues into |
|
|
96
|
+
| ------------------------------- | ------------------------------------- |
|
|
97
|
+
| `app-D.log` | `app-D.log` (append) |
|
|
98
|
+
| `app-D.log.gz` + `app-D.1.log` | `app-D.1.log` |
|
|
99
|
+
| `app-D.1.log.gz` (chain sealed) | `app-D.2.log` |
|
|
100
|
+
| died during a gzip | boot finishes the gzip, then proceeds |
|
|
101
|
+
|
|
102
|
+
## API
|
|
103
|
+
|
|
104
|
+
### `createStream(options)` → `RotateFileStream`
|
|
105
|
+
|
|
106
|
+
### Options
|
|
107
|
+
|
|
108
|
+
| Option | Values | Default |
|
|
109
|
+
| --------------------- | ------------------------------------- | ------------------ |
|
|
110
|
+
| `filename` | **required**, must contain `%DATE%` | — |
|
|
111
|
+
| `datePattern` | `YYYY YY MM DD HH mm ss` | `YYYY-MM-DD` |
|
|
112
|
+
| `zippedArchive` | boolean | `false` |
|
|
113
|
+
| `maxSize` | `20m`, `512k`, `2g`, bytes, `0`=off | off |
|
|
114
|
+
| `maxFiles` | `"90d"` age, `500` count, unset=never | — |
|
|
115
|
+
| `frequency` | `daily`, `30s`, `5m`, `2h`, ms | `daily` |
|
|
116
|
+
| `tz` | any IANA zone | `UTC` |
|
|
117
|
+
| `audit` / `auditFile` | manifest control | `true` / auto-name |
|
|
118
|
+
| `unrefTimers` | detach from event loop | `false` |
|
|
119
|
+
| `addHours` | ⚠️ static clock shift, test only | off |
|
|
120
|
+
| `addHoursEveryMin` | ⚠️ virtual time per tick, test only | off |
|
|
121
|
+
| `clockStepIntervalMs` | tick interval | `60000` |
|
|
122
|
+
|
|
123
|
+
### Methods
|
|
124
|
+
|
|
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 |
|
|
130
|
+
|
|
131
|
+
### Events (typed)
|
|
132
|
+
|
|
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 problems |
|
|
142
|
+
| `error` | `Error` | fatal write problems |
|
|
143
|
+
|
|
144
|
+
Errors must be handled — attach a listener on every instance.
|
|
145
|
+
|
|
146
|
+
## Testing rotation without waiting a week
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const s = createStream({
|
|
150
|
+
filename: "logs/demo-%DATE%.log",
|
|
151
|
+
zippedArchive: true,
|
|
152
|
+
addHoursEveryMin: "24h", // one day per real minute
|
|
153
|
+
clockStepIntervalMs: 1000, // or faster
|
|
154
|
+
});
|
|
155
|
+
await s.advanceClock(); // jump instantly when useful
|
|
156
|
+
```
|
|
102
157
|
|
|
103
|
-
##
|
|
158
|
+
## FAQ
|
|
104
159
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
| `archive` | `(gzFile: string)` |
|
|
110
|
-
| `deleted` | `(file: string)` — retention |
|
|
111
|
-
| `period` | `(stamp: string)` — new period, nothing to archive |
|
|
112
|
-
| `clock` | `({ addHours, addHoursEveryMin, offsetMs })` — test clock active |
|
|
113
|
-
| `warn` | `(err: Error)` — non-fatal (gzip failure, concurrent writer, …) |
|
|
160
|
+
**Why is `%DATE%` mandatory?**
|
|
161
|
+
It is the anchor that decouples _rotation scheduling_ from _filenames_.
|
|
162
|
+
Coarser patterns than your frequency collapse distinct segments onto one
|
|
163
|
+
name — match tokens to cadence (`HH` for hourly, etc.).
|
|
114
164
|
|
|
115
|
-
|
|
165
|
+
**Can two processes share one directory?**
|
|
166
|
+
Different prefixes/families: yes. The _same_ family: no — the audit
|
|
167
|
+
manifest detects a live second writer (`writer.heartbeat`) and warns loudly.
|
|
116
168
|
|
|
117
|
-
|
|
169
|
+
**Is the audit JSON required?**
|
|
170
|
+
No — `audit: false` reduces behaviour to pure disk-scan recovery. Recommended
|
|
171
|
+
to leave on: it repairs interrupted gzips and demotes zombies automatically.
|
|
118
172
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
gzip interrupted by a crash, and demotes records left behind by dead
|
|
123
|
-
processes. Writes are atomic (tmp + rename); a corrupt/foreign file is
|
|
124
|
-
quarantined as `*.corrupt` and logging continues via disk scan.
|
|
173
|
+
**Windows / macOS / Linux?**
|
|
174
|
+
Pure Node APIs; path handling via `node:path`. All three supported
|
|
175
|
+
(Node `>=16.14`).
|
|
125
176
|
|
|
126
|
-
##
|
|
177
|
+
## Compare
|
|
127
178
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
```
|
|
179
|
+
| | logroller | file-stream-rotator | rotating-file-stream | winston-daily-rotate-file |
|
|
180
|
+
| ------------------------- | ----------------- | ------------------- | -------------------- | ------------------------- |
|
|
181
|
+
| Dependencies | **0** | 4+ | 2+ | winston-bound |
|
|
182
|
+
| Crash-safe resume | ✅ audit-assisted | partial | partial | ✖ |
|
|
183
|
+
| Interrupted-gzip repair | ✅ | ✖ | ✖ | ✖ |
|
|
184
|
+
| Event journal | ✅ | ✖ | ✖ | ✖ |
|
|
185
|
+
| TZ-correct daily rollover | ✅ | partial | ✅ | ✅ |
|
|
186
|
+
| Virtual test clock | ✅ | ✖ | ✖ | ✖ |
|
|
187
|
+
| Winston required | no | no | no | yes |
|
|
138
188
|
|
|
139
|
-
##
|
|
189
|
+
## Contributing
|
|
140
190
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
- unset — never auto-delete; remove manually
|
|
191
|
+
PRs welcome. `npm run verify` gates every change (typecheck + tests).
|
|
192
|
+
Please file issues before large refactors.
|
|
144
193
|
|
|
145
194
|
## License
|
|
146
195
|
|
|
147
|
-
MIT
|
|
196
|
+
[MIT](./LICENSE)
|
package/dist/fsio.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
/** `stat().size` guarded to 0; string paths only. */
|
|
3
|
+
export declare const statSizeSafe: (p: string | null | undefined) => number;
|
|
4
|
+
export declare const existsSafe: (p: string) => boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Compress `src` → `dst`, deleting `src` ONLY on verified success.
|
|
7
|
+
* On failure removes the partial `dst` and reports via callback —
|
|
8
|
+
* logs degrade to uncompressed rather than losing data.
|
|
9
|
+
*/
|
|
10
|
+
export declare function gzipMove(src: string, dst: string, onWarn?: (err: Error) => void): Promise<boolean>;
|
|
11
|
+
export declare const endStream: (ws: fs.WriteStream | null) => Promise<void>;
|