@trebired/logger 2.5.6 → 2.5.11
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 +19 -0
- package/README.md +268 -249
- package/dist/config/console_visibility.d.ts.map +1 -1
- package/dist/config/console_visibility.js +5 -3
- package/dist/config/console_visibility.js.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -2
- package/dist/constants.js.map +1 -1
- package/dist/core/create_log/api.d.ts.map +1 -1
- package/dist/core/create_log/api.js +5 -2
- package/dist/core/create_log/api.js.map +1 -1
- package/dist/core/create_log/finalize.js +3 -3
- package/dist/core/create_log/finalize.js.map +1 -1
- package/dist/core/create_log/runtime.d.ts.map +1 -1
- package/dist/core/create_log/runtime.js +3 -1
- package/dist/core/create_log/runtime.js.map +1 -1
- package/dist/package-metadata.d.ts +6 -0
- package/dist/package-metadata.d.ts.map +1 -0
- package/dist/package-metadata.js +30 -0
- package/dist/package-metadata.js.map +1 -0
- package/dist/storage/backend/index.d.ts.map +1 -1
- package/dist/storage/backend/index.js +3 -2
- package/dist/storage/backend/index.js.map +1 -1
- package/dist/storage/partitions/errors.d.ts +1 -1
- package/dist/storage/partitions/errors.d.ts.map +1 -1
- package/dist/storage/partitions/errors.js +3 -3
- package/dist/storage/partitions/errors.js.map +1 -1
- package/dist/storage/partitions/records.js +2 -2
- package/dist/storage/partitions/records.js.map +1 -1
- package/dist/storage/partitions/transforms.js +1 -1
- package/dist/storage/partitions/transforms.js.map +1 -1
- package/dist/storage/write.d.ts.map +1 -1
- package/dist/storage/write.js +3 -1
- package/dist/storage/write.js.map +1 -1
- package/dist/types/partitions.d.ts +1 -1
- package/dist/types/partitions.d.ts.map +1 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +10 -1
- package/dist/utils/runtime.js.map +1 -1
- package/native/darwin-arm64.node +0 -0
- package/native/darwin-x64.node +0 -0
- package/native/linux-arm64-gnu.node +0 -0
- package/native/linux-x64-gnu.node +0 -0
- package/package.json +57 -114
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to `@trebired/logger` will be documented here.
|
|
|
4
4
|
|
|
5
5
|
This project follows semantic versioning once published.
|
|
6
6
|
|
|
7
|
+
## 2.5.9
|
|
8
|
+
|
|
9
|
+
- Added package-owned organization metadata and derived logger package notices from `package.json`.
|
|
10
|
+
- Kept browser-facing logger constants free of package metadata filesystem reads.
|
|
11
|
+
- Updated internal package dependency ranges to the current sibling package releases.
|
|
12
|
+
|
|
13
|
+
## 2.5.8
|
|
14
|
+
|
|
15
|
+
- Removed dead test scripts and stale test commands from publish workflows and maintainer docs.
|
|
16
|
+
|
|
17
|
+
## 2.5.7
|
|
18
|
+
|
|
19
|
+
- Removed package test suites and banned committed `*.spec.ts`/`*.spec.tsx` files through Code Discipline.
|
|
20
|
+
- Added Code Discipline enforcement for hardcoded `trebired` strings outside package metadata.
|
|
21
|
+
- Migrated Code Discipline to `.code-discipline/config.ts` with alias-map sync output.
|
|
22
|
+
- Updated package-generated artifact ignores and internal package dependency ranges.
|
|
23
|
+
|
|
7
24
|
## 2.5.6
|
|
8
25
|
|
|
9
26
|
- Moved package-owned logger startup notices under the `trebired.logger.initialize` group root while leaving caller-supplied log groups unchanged.
|
|
@@ -128,3 +145,5 @@ This project follows semantic versioning once published.
|
|
|
128
145
|
- Added group-based JSONL storage with ISO-style filenames.
|
|
129
146
|
- Added retention cleanup, max-size rolling, optional gzip compression, redaction, serializers, sampling, request middleware, stream events, and query helpers.
|
|
130
147
|
- Initial public release.
|
|
148
|
+
|
|
149
|
+
- Standardized package metadata ordering and contributing guidance around the Trebired writing style.
|
package/README.md
CHANGED
|
@@ -6,28 +6,14 @@ Local-first JSONL logger with human-browsable group folders, durable writes, ret
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
Runtime support: Bun 1
|
|
10
|
-
|
|
11
|
-
The package can use bundled native binaries for supported Linux and macOS targets to speed up large storage/export workloads. Consumers still install a single package:
|
|
12
|
-
|
|
13
|
-
```txt
|
|
14
|
-
@trebired/logger
|
|
15
|
-
dist/
|
|
16
|
-
native/
|
|
17
|
-
linux-x64-gnu.node
|
|
18
|
-
linux-arm64-gnu.node
|
|
19
|
-
darwin-arm64.node
|
|
20
|
-
darwin-x64.node
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
At runtime the JS wrapper always tries the matching `.node` file first when one is bundled for the current platform, and falls back to the built-in JS backend if native loading is unavailable or fails. Bundled native binaries are currently published for Linux GNU (`x64` and `arm64`) and macOS targets. End users do not need Rust installed.
|
|
24
|
-
|
|
25
|
-
Set `TB_LOGGER_DISABLE_NATIVE=1` only when you explicitly want to force the JS backend.
|
|
9
|
+
Runtime support: Bun 1+.
|
|
26
10
|
|
|
27
11
|
```sh
|
|
28
|
-
|
|
12
|
+
bun i @trebired/logger
|
|
29
13
|
```
|
|
30
14
|
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
31
17
|
```ts
|
|
32
18
|
import { createLog } from "@trebired/logger";
|
|
33
19
|
|
|
@@ -48,58 +34,9 @@ import { createBrowserLog } from "@trebired/logger/browser";
|
|
|
48
34
|
import { LogProvider, useLog } from "@trebired/logger/browser/react";
|
|
49
35
|
```
|
|
50
36
|
|
|
51
|
-
##
|
|
52
|
-
|
|
53
|
-
Use `@trebired/logger/browser` when you want the same levels, metadata conventions, grouping rules, and scoped logger behavior in browser code:
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
import { createBrowserLog } from "@trebired/logger/browser";
|
|
57
|
-
|
|
58
|
-
const log = createBrowserLog({
|
|
59
|
-
group: "frontend.app",
|
|
60
|
-
metadata: {
|
|
61
|
-
deploymentId: "deploy-42",
|
|
62
|
-
requestId: "req-abc",
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
log.info("frontend boot");
|
|
67
|
-
await log.flush();
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
The first browser release ships with console delivery built in. It also supports custom browser transports with in-memory batching so you can add fetch, beacon, websocket, or other delivery later without changing the logger API.
|
|
71
|
-
|
|
72
|
-
There is no built-in SSR bootstrap helper in this release. If you want browser correlation data such as `requestId`, `sessionId`, or `deploymentId`, pass it explicitly through `metadata` or per-log metadata.
|
|
73
|
-
|
|
74
|
-
## React Adapter
|
|
75
|
-
|
|
76
|
-
`@trebired/logger/browser/react` is intentionally thin. It does not create a logger for you. It only helps wire an existing browser logger into React context:
|
|
77
|
-
|
|
78
|
-
```tsx
|
|
79
|
-
import { createRoot } from "react-dom/client";
|
|
80
|
-
import { createBrowserLog } from "@trebired/logger/browser";
|
|
81
|
-
import { LogProvider, useLog } from "@trebired/logger/browser/react";
|
|
82
|
-
|
|
83
|
-
const log = createBrowserLog({
|
|
84
|
-
group: "frontend.app",
|
|
85
|
-
metadata: { deploymentId: "deploy-42" },
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
function SaveButton() {
|
|
89
|
-
const scopedLog = useLog("ui.save_button");
|
|
90
|
-
return <button onClick={() => scopedLog.info("clicked")}>Save</button>;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
createRoot(document.getElementById("root")!).render(
|
|
94
|
-
<LogProvider log={log}>
|
|
95
|
-
<SaveButton />
|
|
96
|
-
</LogProvider>,
|
|
97
|
-
);
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
`LogErrorBoundary` is also available from `@trebired/logger/browser/react` when you want render errors logged with the shared event shape.
|
|
37
|
+
## Concepts
|
|
101
38
|
|
|
102
|
-
|
|
39
|
+
### Why This Logger
|
|
103
40
|
|
|
104
41
|
Most loggers either write to stdout and expect an external collector, or provide a very broad transport system. This package is intentionally opinionated around a simpler operational workflow:
|
|
105
42
|
|
|
@@ -176,7 +113,7 @@ If you already have a full custom partition string, pass it directly with `parti
|
|
|
176
113
|
|
|
177
114
|
Temporary partitions are now self-cleaning: once a logger switches away from a temporary partition, promotes it, or closes, any temporary partition in that log directory that is no longer current is deleted automatically.
|
|
178
115
|
|
|
179
|
-
If you want the logger to own the common
|
|
116
|
+
If you want the logger to own the common "temp first, final later" lifecycle, call `finalizePartition()` instead of open-coding a `promotePartition()` plus fallback sequence:
|
|
180
117
|
|
|
181
118
|
```ts
|
|
182
119
|
const result = await log.finalizePartition(final, {
|
|
@@ -188,140 +125,7 @@ if (result.action === "switched") {
|
|
|
188
125
|
}
|
|
189
126
|
```
|
|
190
127
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
You can manage partitions either from a live logger or with standalone helpers:
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
import {
|
|
197
|
-
copyPartition,
|
|
198
|
-
createPartition,
|
|
199
|
-
deleteLogs,
|
|
200
|
-
getPartitionInfo,
|
|
201
|
-
listPartitions,
|
|
202
|
-
renamePartition,
|
|
203
|
-
} from "@trebired/logger";
|
|
204
|
-
|
|
205
|
-
await createPartition("/var/log/my-app", "2026-05-17-12-00-00-1-staged", {
|
|
206
|
-
temporary: true,
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const partitions = await listPartitions("/var/log/my-app");
|
|
210
|
-
console.log(partitions.total.megabytes);
|
|
211
|
-
console.log(partitions[0]?.total.megabytes);
|
|
212
|
-
|
|
213
|
-
await renamePartition("/var/log/my-app", {
|
|
214
|
-
from: "2026-05-17-12-00-00-1-staged",
|
|
215
|
-
to: "2026-05-17-12-00-00-1-final",
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
await copyPartition({
|
|
219
|
-
fromDir: "/var/log/my-app",
|
|
220
|
-
from: "2026-05-17-12-00-00-1-final",
|
|
221
|
-
toDir: "/var/log/archive",
|
|
222
|
-
to: "2026-05-17-12-00-00-1-final-copy",
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
await deleteLogs("/var/log/my-app", {
|
|
226
|
-
partition: "2026-05-17-12-00-00-1-final",
|
|
227
|
-
groupKey: "jobs.queue",
|
|
228
|
-
level: "warn",
|
|
229
|
-
olderThanDays: 7,
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
console.log(await getPartitionInfo("/var/log/my-app", "2026-05-17-12-00-00-1-final"));
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
For live loggers, the package now exposes two layers:
|
|
236
|
-
|
|
237
|
-
- `promotePartition()` stays the lower-level explicit primitive. By default it still errors on target conflicts unless you pass `merge: true` or `ifExists`.
|
|
238
|
-
- `finalizePartition()` is the higher-level lifecycle helper for idempotent temp-to-final transitions.
|
|
239
|
-
|
|
240
|
-
`finalizePartition()` returns structured outcomes instead of forcing application code to catch expected conflicts:
|
|
241
|
-
|
|
242
|
-
```ts
|
|
243
|
-
const result = await log.finalizePartition("2026-05-17-12-00-00-1-final", {
|
|
244
|
-
ifExists: "switch",
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
result.action;
|
|
248
|
-
// "renamed" | "merged" | "switched" | "activated-target"
|
|
249
|
-
// "marked-permanent" | "already-finalized"
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
Conflict policies:
|
|
253
|
-
|
|
254
|
-
- `ifExists: "error"` keeps strict low-level behavior
|
|
255
|
-
- `ifExists: "merge"` merges the active source partition into the existing target
|
|
256
|
-
- `ifExists: "switch"` activates the existing target without merging the current source
|
|
257
|
-
|
|
258
|
-
For advanced callers that still use the lower-level primitives directly, `getPartitionErrorCode()` and `isPartitionError()` can inspect partition lifecycle errors without parsing raw message strings.
|
|
259
|
-
|
|
260
|
-
## Core API
|
|
261
|
-
|
|
262
|
-
```ts
|
|
263
|
-
const log = createLog({
|
|
264
|
-
dir: "/var/log/my-app",
|
|
265
|
-
save: true,
|
|
266
|
-
console: true,
|
|
267
|
-
timeZone: "America/New_York",
|
|
268
|
-
source: "api",
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
log.debug("app.boot", "config loaded");
|
|
272
|
-
log.info("app.boot", "ready");
|
|
273
|
-
log.success("job.import", "finished", { rows: 1200 });
|
|
274
|
-
log.warn("http.request", "slow request", { took_ms: 842 });
|
|
275
|
-
log.fail("job.import", "failed validation");
|
|
276
|
-
log.error("app.runtime", "uncaught error");
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
`save` defaults to `true` when `dir` is provided. If no `dir` is provided, the logger can still emit console output and live stream events.
|
|
280
|
-
|
|
281
|
-
If you log without passing a group, the logger always uses `"default"`.
|
|
282
|
-
|
|
283
|
-
`@trebired/logger` runs on both Bun and Node.js. It may print one-time package notices for runtime-specific guidance or important future package messages. For example, when it detects Node.js, it recommends Bun for best startup and file I/O performance. Pass `quiet: true` to suppress package notices:
|
|
284
|
-
|
|
285
|
-
```ts
|
|
286
|
-
const log = createLog({
|
|
287
|
-
quiet: true,
|
|
288
|
-
});
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
When `quiet` is not `true`, the package also prints a one-time startup greeting using the same console logger style as normal entries.
|
|
292
|
-
|
|
293
|
-
`timeZone` is a top-level logger option because it controls the actual local moment used for saved file names and console timestamps. It defaults to the host timezone, then falls back to `America/New_York`.
|
|
294
|
-
|
|
295
|
-
Console output is configurable. `level` and `message` are always shown; timestamp, group, and metadata can be hidden. `console.locale` only controls display formatting. When `locale` is omitted or invalid, the logger passes `undefined` to `Intl.DateTimeFormat`, so JavaScript uses the runtime or system default locale:
|
|
296
|
-
|
|
297
|
-
```ts
|
|
298
|
-
const log = createLog({
|
|
299
|
-
timeZone: "America/New_York",
|
|
300
|
-
console: {
|
|
301
|
-
colors: true,
|
|
302
|
-
timestamp: true,
|
|
303
|
-
group: false,
|
|
304
|
-
metadata: false,
|
|
305
|
-
locale: "en-US",
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
Use `timeZone` for the local hour and `console.locale` for the display style.
|
|
311
|
-
|
|
312
|
-
Server runtimes also auto-discover a top-level `tb.logger.json` file by walking up from `process.cwd()`. When present, it overrides console group visibility in both development and production without affecting saved logs:
|
|
313
|
-
|
|
314
|
-
```json
|
|
315
|
-
{
|
|
316
|
-
"hideConsoleGroups": ["blog.post", "http.request"]
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
Each configured group hides both the exact group and descendants such as `blog.post.comment`. Invalid files print a warning and are ignored.
|
|
321
|
-
|
|
322
|
-
European dot-date locales such as `cs-CZ` and `de-DE` are formatted consistently as `03.05.2026, 15:59:23`.
|
|
323
|
-
|
|
324
|
-
## Full API Example
|
|
128
|
+
### Full API Example
|
|
325
129
|
|
|
326
130
|
```ts
|
|
327
131
|
import { createLog } from "@trebired/logger";
|
|
@@ -395,7 +199,7 @@ await log.flush();
|
|
|
395
199
|
await log.close();
|
|
396
200
|
```
|
|
397
201
|
|
|
398
|
-
|
|
202
|
+
### Custom Levels
|
|
399
203
|
|
|
400
204
|
Levels are weighted. `minLevel` filters out entries with lower weight.
|
|
401
205
|
|
|
@@ -416,7 +220,183 @@ Built-in levels are `debug`, `info`, `success`, `warn`, `fail`, and `error`. A c
|
|
|
416
220
|
|
|
417
221
|
Custom level methods are available at runtime. In TypeScript, the logger exposes dynamic level methods through an index signature, so custom names work but are not exhaustively autocompleted from the `levels` object yet.
|
|
418
222
|
|
|
419
|
-
|
|
223
|
+
### Scoped Loggers
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const jobs = log.group("jobs.queue");
|
|
227
|
+
jobs.info("started", { jobId: "job_1" });
|
|
228
|
+
|
|
229
|
+
const worker = log.withScope("worker", "jobs.queue", 2);
|
|
230
|
+
worker.error("failed", { jobId: "job_1" });
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Express-Style Middleware
|
|
234
|
+
|
|
235
|
+
`requestLogger()` is compatible with Express-style middleware and attaches `req.log` by default.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
app.use(log.requestLogger({
|
|
239
|
+
group: "http.request",
|
|
240
|
+
idHeader: "x-request-id",
|
|
241
|
+
}));
|
|
242
|
+
|
|
243
|
+
app.get("/", (req, res) => {
|
|
244
|
+
req.log.info("handled");
|
|
245
|
+
res.end("ok");
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Live Stream
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { logStream } from "@trebired/logger";
|
|
253
|
+
|
|
254
|
+
logStream.on("log", (entry, context) => {
|
|
255
|
+
// context.dir is the active log directory, if one is configured
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Runtime
|
|
260
|
+
|
|
261
|
+
### Install Notes
|
|
262
|
+
|
|
263
|
+
The package can use bundled native binaries for supported Linux and macOS targets to speed up large storage/export workloads. Consumers still install a single package:
|
|
264
|
+
|
|
265
|
+
```txt
|
|
266
|
+
@trebired/logger
|
|
267
|
+
dist/
|
|
268
|
+
native/
|
|
269
|
+
linux-x64-gnu.node
|
|
270
|
+
linux-arm64-gnu.node
|
|
271
|
+
darwin-arm64.node
|
|
272
|
+
darwin-x64.node
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
At runtime the JS wrapper always tries the matching `.node` file first when one is bundled for the current platform, and falls back to the built-in JS backend if native loading is unavailable or fails. Bundled native binaries are currently published for Linux GNU (`x64` and `arm64`) and macOS targets. End users do not need Rust installed.
|
|
276
|
+
|
|
277
|
+
Set `TB_LOGGER_DISABLE_NATIVE=1` only when you explicitly want to force the JS backend.
|
|
278
|
+
|
|
279
|
+
### Browser Runtime
|
|
280
|
+
|
|
281
|
+
Use `@trebired/logger/browser` when you want the same levels, metadata conventions, grouping rules, and scoped logger behavior in browser code:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import { createBrowserLog } from "@trebired/logger/browser";
|
|
285
|
+
|
|
286
|
+
const log = createBrowserLog({
|
|
287
|
+
group: "frontend.app",
|
|
288
|
+
metadata: {
|
|
289
|
+
deploymentId: "deploy-42",
|
|
290
|
+
requestId: "req-abc",
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
log.info("frontend boot");
|
|
295
|
+
await log.flush();
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The first browser release ships with console delivery built in. It also supports custom browser transports with in-memory batching so you can add fetch, beacon, websocket, or other delivery later without changing the logger API.
|
|
299
|
+
|
|
300
|
+
There is no built-in SSR bootstrap helper in this release. If you want browser correlation data such as `requestId`, `sessionId`, or `deploymentId`, pass it explicitly through `metadata` or per-log metadata.
|
|
301
|
+
|
|
302
|
+
### React Adapter
|
|
303
|
+
|
|
304
|
+
`@trebired/logger/browser/react` is intentionally thin. It does not create a logger for you. It only helps wire an existing browser logger into React context:
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
import { createRoot } from "react-dom/client";
|
|
308
|
+
import { createBrowserLog } from "@trebired/logger/browser";
|
|
309
|
+
import { LogProvider, useLog } from "@trebired/logger/browser/react";
|
|
310
|
+
|
|
311
|
+
const log = createBrowserLog({
|
|
312
|
+
group: "frontend.app",
|
|
313
|
+
metadata: { deploymentId: "deploy-42" },
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
function SaveButton() {
|
|
317
|
+
const scopedLog = useLog("ui.save_button");
|
|
318
|
+
return <button onClick={() => scopedLog.info("clicked")}>Save</button>;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
createRoot(document.getElementById("root")!).render(
|
|
322
|
+
<LogProvider log={log}>
|
|
323
|
+
<SaveButton />
|
|
324
|
+
</LogProvider>,
|
|
325
|
+
);
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
`LogErrorBoundary` is also available from `@trebired/logger/browser/react` when you want render errors logged with the shared event shape.
|
|
329
|
+
|
|
330
|
+
### Partition Lifecycle
|
|
331
|
+
|
|
332
|
+
You can manage partitions either from a live logger or with standalone helpers:
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
import {
|
|
336
|
+
copyPartition,
|
|
337
|
+
createPartition,
|
|
338
|
+
deleteLogs,
|
|
339
|
+
getPartitionInfo,
|
|
340
|
+
listPartitions,
|
|
341
|
+
renamePartition,
|
|
342
|
+
} from "@trebired/logger";
|
|
343
|
+
|
|
344
|
+
await createPartition("/var/log/my-app", "2026-05-17-12-00-00-1-staged", {
|
|
345
|
+
temporary: true,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const partitions = await listPartitions("/var/log/my-app");
|
|
349
|
+
console.log(partitions.total.megabytes);
|
|
350
|
+
console.log(partitions[0]?.total.megabytes);
|
|
351
|
+
|
|
352
|
+
await renamePartition("/var/log/my-app", {
|
|
353
|
+
from: "2026-05-17-12-00-00-1-staged",
|
|
354
|
+
to: "2026-05-17-12-00-00-1-final",
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
await copyPartition({
|
|
358
|
+
fromDir: "/var/log/my-app",
|
|
359
|
+
from: "2026-05-17-12-00-00-1-final",
|
|
360
|
+
toDir: "/var/log/archive",
|
|
361
|
+
to: "2026-05-17-12-00-00-1-final-copy",
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await deleteLogs("/var/log/my-app", {
|
|
365
|
+
partition: "2026-05-17-12-00-00-1-final",
|
|
366
|
+
groupKey: "jobs.queue",
|
|
367
|
+
level: "warn",
|
|
368
|
+
olderThanDays: 7,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
console.log(await getPartitionInfo("/var/log/my-app", "2026-05-17-12-00-00-1-final"));
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
For live loggers, the package now exposes two layers:
|
|
375
|
+
|
|
376
|
+
- `promotePartition()` stays the lower-level explicit primitive. By default it still errors on target conflicts unless you pass `merge: true` or `ifExists`.
|
|
377
|
+
- `finalizePartition()` is the higher-level lifecycle helper for idempotent temp-to-final transitions.
|
|
378
|
+
|
|
379
|
+
`finalizePartition()` returns structured outcomes instead of forcing application code to catch expected conflicts:
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
const result = await log.finalizePartition("2026-05-17-12-00-00-1-final", {
|
|
383
|
+
ifExists: "switch",
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
result.action;
|
|
387
|
+
// "renamed" | "merged" | "switched" | "activated-target"
|
|
388
|
+
// "marked-permanent" | "already-finalized"
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Conflict policies:
|
|
392
|
+
|
|
393
|
+
- `ifExists: "error"` keeps strict low-level behavior
|
|
394
|
+
- `ifExists: "merge"` merges the active source partition into the existing target
|
|
395
|
+
- `ifExists: "switch"` activates the existing target without merging the current source
|
|
396
|
+
|
|
397
|
+
For advanced callers that still use the lower-level primitives directly, `getPartitionErrorCode()` and `isPartitionError()` can inspect partition lifecycle errors without parsing raw message strings.
|
|
398
|
+
|
|
399
|
+
### Async Writes, Flush, and Close
|
|
420
400
|
|
|
421
401
|
File writes are async queued by default.
|
|
422
402
|
|
|
@@ -452,7 +432,7 @@ const log = createLog({
|
|
|
452
432
|
});
|
|
453
433
|
```
|
|
454
434
|
|
|
455
|
-
|
|
435
|
+
### Retention and Rolling
|
|
456
436
|
|
|
457
437
|
Defaults:
|
|
458
438
|
|
|
@@ -484,7 +464,7 @@ When a file exceeds the configured size, the logger rolls to the next sequence i
|
|
|
484
464
|
2026-05-03-13-00-00-3-info.jsonl
|
|
485
465
|
```
|
|
486
466
|
|
|
487
|
-
|
|
467
|
+
### Redaction and Serializers
|
|
488
468
|
|
|
489
469
|
Common sensitive keys are redacted by default: password, token, secret, authorization, cookie, api_key, and related variants.
|
|
490
470
|
|
|
@@ -506,33 +486,7 @@ log.info("account.update", "saved", {
|
|
|
506
486
|
});
|
|
507
487
|
```
|
|
508
488
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
```ts
|
|
512
|
-
const jobs = log.group("jobs.queue");
|
|
513
|
-
jobs.info("started", { jobId: "job_1" });
|
|
514
|
-
|
|
515
|
-
const worker = log.withScope("worker", "jobs.queue", 2);
|
|
516
|
-
worker.error("failed", { jobId: "job_1" });
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
## Express-Style Middleware
|
|
520
|
-
|
|
521
|
-
`requestLogger()` is compatible with Express-style middleware and attaches `req.log` by default.
|
|
522
|
-
|
|
523
|
-
```ts
|
|
524
|
-
app.use(log.requestLogger({
|
|
525
|
-
group: "http.request",
|
|
526
|
-
idHeader: "x-request-id",
|
|
527
|
-
}));
|
|
528
|
-
|
|
529
|
-
app.get("/", (req, res) => {
|
|
530
|
-
req.log.info("handled");
|
|
531
|
-
res.end("ok");
|
|
532
|
-
});
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
## Query Saved Logs
|
|
489
|
+
### Query Saved Logs
|
|
536
490
|
|
|
537
491
|
Old query names are not supported anymore. Use `getLogsForDir()`, `getAllLogs()`, and `getAllLogsAcrossPartitions()`. Do not use `getEntriesForDir()` or `getAll()`.
|
|
538
492
|
|
|
@@ -574,7 +528,7 @@ console.log(merged.logs);
|
|
|
574
528
|
console.log(merged.metadata.partitions.items);
|
|
575
529
|
```
|
|
576
530
|
|
|
577
|
-
|
|
531
|
+
### Sampling
|
|
578
532
|
|
|
579
533
|
```ts
|
|
580
534
|
const log = createLog({
|
|
@@ -586,26 +540,91 @@ const selective = createLog({
|
|
|
586
540
|
});
|
|
587
541
|
```
|
|
588
542
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
```ts
|
|
592
|
-
import { logStream } from "@trebired/logger";
|
|
593
|
-
|
|
594
|
-
logStream.on("log", (entry, context) => {
|
|
595
|
-
// context.dir is the active log directory, if one is configured
|
|
596
|
-
});
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
## Development
|
|
543
|
+
### Development
|
|
600
544
|
|
|
601
545
|
```sh
|
|
602
546
|
bun install
|
|
603
547
|
bun run demo
|
|
604
|
-
bun test
|
|
605
548
|
bun run typecheck
|
|
606
549
|
bun run build
|
|
607
550
|
```
|
|
608
551
|
|
|
552
|
+
Committed `*.spec.ts` and `*.spec.tsx` files are banned by Code Discipline, so this package does not expose a test script.
|
|
553
|
+
|
|
609
554
|
`bun run demo` starts a small dummy system that keeps logging until interrupted. It exercises grouped and scoped loggers, custom levels, redaction, request middleware, live stream events, local querying, and write stats. It writes throwaway logs into the repo under `.demo-logs/dummy`. Microslop Windows is not supported.
|
|
610
555
|
|
|
611
|
-
The
|
|
556
|
+
The published package exports compiled files from `dist`. Publishing runs `typecheck` and package verification through `prepublishOnly`.
|
|
557
|
+
|
|
558
|
+
## Public API
|
|
559
|
+
|
|
560
|
+
### Core API
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
const log = createLog({
|
|
564
|
+
dir: "/var/log/my-app",
|
|
565
|
+
save: true,
|
|
566
|
+
console: true,
|
|
567
|
+
timeZone: "America/New_York",
|
|
568
|
+
source: "api",
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
log.debug("app.boot", "config loaded");
|
|
572
|
+
log.info("app.boot", "ready");
|
|
573
|
+
log.success("job.import", "finished", { rows: 1200 });
|
|
574
|
+
log.warn("http.request", "slow request", { took_ms: 842 });
|
|
575
|
+
log.fail("job.import", "failed validation");
|
|
576
|
+
log.error("app.runtime", "uncaught error");
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
`save` defaults to `true` when `dir` is provided. If no `dir` is provided, the logger can still emit console output and live stream events.
|
|
580
|
+
|
|
581
|
+
If you log without passing a group, the logger always uses `"default"`.
|
|
582
|
+
|
|
583
|
+
`@trebired/logger` runs on both Bun. It may print one-time package notices for runtime-specific guidance or important future package messages. For example, Pass `quiet: true` to suppress package notices:
|
|
584
|
+
|
|
585
|
+
```ts
|
|
586
|
+
const log = createLog({
|
|
587
|
+
quiet: true,
|
|
588
|
+
});
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
When `quiet` is not `true`, the package also prints a one-time startup greeting using the same console logger style as normal entries.
|
|
592
|
+
|
|
593
|
+
`timeZone` is a top-level logger option because it controls the actual local moment used for saved file names and console timestamps. It defaults to the host timezone, then falls back to `America/New_York`.
|
|
594
|
+
|
|
595
|
+
Console output is configurable. `level` and `message` are always shown; timestamp, group, and metadata can be hidden. `console.locale` only controls display formatting. When `locale` is omitted or invalid, the logger passes `undefined` to `Intl.DateTimeFormat`, so JavaScript uses the runtime or system default locale:
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
const log = createLog({
|
|
599
|
+
timeZone: "America/New_York",
|
|
600
|
+
console: {
|
|
601
|
+
colors: true,
|
|
602
|
+
timestamp: true,
|
|
603
|
+
group: false,
|
|
604
|
+
metadata: false,
|
|
605
|
+
locale: "en-US",
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Use `timeZone` for the local hour and `console.locale` for the display style.
|
|
611
|
+
|
|
612
|
+
Server runtimes also auto-discover a top-level `tb.logger.json` file by walking up from `process.cwd()`. When present, it overrides console group visibility in both development and production without affecting saved logs:
|
|
613
|
+
|
|
614
|
+
```json
|
|
615
|
+
{
|
|
616
|
+
"hideConsoleGroups": ["blog.post", "http.request"]
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
Each configured group hides both the exact group and descendants such as `blog.post.comment`. Invalid files print a warning and are ignored.
|
|
621
|
+
|
|
622
|
+
European dot-date locales such as `cs-CZ` and `de-DE` are formatted consistently as `03.05.2026, 15:59:23`.
|
|
623
|
+
|
|
624
|
+
## What It Does Not Do
|
|
625
|
+
|
|
626
|
+
This package does not:
|
|
627
|
+
|
|
628
|
+
- replace a full observability stack
|
|
629
|
+
- force one transport, server framework, or deployment model
|
|
630
|
+
- infer product-specific redaction policy
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"console_visibility.d.ts","sourceRoot":"","sources":["../../src/config/console_visibility.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"console_visibility.d.ts","sourceRoot":"","sources":["../../src/config/console_visibility.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,gBAAgB,mBAAmB,CAAC;AAG1C,KAAK,gCAAgC,GAAG;IACtC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,CAAC;AAEF,KAAK,uBAAuB,GAAG,gCAAgC,GAAG;IAChE,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CACpC,CAAC;AAoFF,iBAAS,gCAAgC,CAAC,QAAQ,SAAgB,GAAG,gCAAgC,CAWpG;AAgBD,iBAAS,8BAA8B,CAAC,QAAQ,SAAgB,GAAG,uBAAuB,CAGzF;AAED,OAAO,EACL,gBAAgB,EAChB,gCAAgC,EAChC,8BAA8B,GAC/B,CAAC;AACF,YAAY,EACV,uBAAuB,EACvB,gCAAgC,GACjC,CAAC"}
|
|
@@ -3,17 +3,19 @@ import path from "node:path";
|
|
|
3
3
|
import { normGroup } from "../groups.js";
|
|
4
4
|
import { resolveNativeConsoleVisibilityConfig } from "../storage/backend/native.js";
|
|
5
5
|
import { isPlainObject } from "../utils/values.js";
|
|
6
|
+
import { buildPackageLogGroup } from "../package-metadata.js";
|
|
6
7
|
const CONFIG_FILE_NAME = "tb.logger.json";
|
|
8
|
+
const LOGGER_LOG_GROUP = buildPackageLogGroup();
|
|
7
9
|
function warningForInvalidShape(filePath) {
|
|
8
|
-
return `[
|
|
10
|
+
return `[${LOGGER_LOG_GROUP}] invalid ${CONFIG_FILE_NAME} at ${filePath}: expected an object with a hideConsoleGroups string array`;
|
|
9
11
|
}
|
|
10
12
|
function warningForReadError(filePath, error) {
|
|
11
13
|
const detail = error instanceof Error ? error.message : String(error);
|
|
12
|
-
return `[
|
|
14
|
+
return `[${LOGGER_LOG_GROUP}] failed to read ${CONFIG_FILE_NAME} at ${filePath}: ${detail}`;
|
|
13
15
|
}
|
|
14
16
|
function warningForParseError(filePath, error) {
|
|
15
17
|
const detail = error instanceof Error ? error.message : String(error);
|
|
16
|
-
return `[
|
|
18
|
+
return `[${LOGGER_LOG_GROUP}] invalid JSON in ${CONFIG_FILE_NAME} at ${filePath}: ${detail}`;
|
|
17
19
|
}
|
|
18
20
|
function normalizeHideConsoleGroups(input) {
|
|
19
21
|
const out = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"console_visibility.js","sourceRoot":"","sources":["../../src/config/console_visibility.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,oCAAoC,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"console_visibility.js","sourceRoot":"","sources":["../../src/config/console_visibility.ts"],"names":[],"mappings":"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"}
|