@viktorbarzin/log 0.0.0-stage → 0.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/README.md +37 -2
- package/dist/index.d.ts +29 -0
- package/dist/index.js +134 -0
- package/package.json +35 -5
package/README.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @viktorbarzin/log
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A JSON-lines logger for the platform's log contract: one object per line on
|
|
4
|
+
stderr with `ts` (Python `isoformat`, `+00:00`), `level`, `logger`, `msg`,
|
|
5
|
+
then the call's extras (which never overwrite those four keys), then `exc`
|
|
6
|
+
for an error. No dependencies; works on Bun and Node.
|
|
7
|
+
|
|
8
|
+
<!-- typecheck-preamble
|
|
9
|
+
declare const err: unknown;
|
|
10
|
+
-->
|
|
11
|
+
```ts
|
|
12
|
+
import { getLogger, installJsonConsole } from "@viktorbarzin/log";
|
|
13
|
+
|
|
14
|
+
const log = getLogger("myapp.sync");
|
|
15
|
+
log.info("sync served", { user: "someone", lists: 3 });
|
|
16
|
+
log.exception("sync failed", err, { path: "/api/sync" });
|
|
17
|
+
|
|
18
|
+
installJsonConsole(); // console.* from the framework becomes JSON lines too (logger "console")
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{"ts":"2026-10-16T04:05:06.789000+00:00","level":"INFO","logger":"myapp.sync","msg":"sync served","user":"someone","lists":3}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
| Export | |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `getLogger(name)` | `debug`, `info`, `warning`, `error`, `exception(msg, err, extra?)`, `record(level, msg, extra?, err?)` |
|
|
30
|
+
| `parseLogLevel(raw)` | `WARN` → `WARNING`; unknown or unset → `INFO` |
|
|
31
|
+
| `setLogLevel(level)` | the threshold; starts from `LOG_LEVEL` |
|
|
32
|
+
| `setLogSink(sink)` | send lines elsewhere (tests); returns a restore function |
|
|
33
|
+
| `installJsonConsole(target?)` | route `console.*` through the logger; an `Error` argument goes under `exc`; returns a restore function |
|
|
34
|
+
| `formatRecord`, `pythonIsoTimestamp` | the formatting, exported for tests |
|
|
35
|
+
|
|
36
|
+
The sink and threshold are one per process (kept on `globalThis`), so a
|
|
37
|
+
server bundled twice still logs through one sink. `LOG_LEVEL` is the only
|
|
38
|
+
environment variable read.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type LogLevel = "DEBUG" | "INFO" | "WARNING" | "ERROR";
|
|
2
|
+
export type LogExtra = Record<string, unknown>;
|
|
3
|
+
export type LogSink = (line: string) => void;
|
|
4
|
+
/** `WARN` means WARNING; anything unknown or unset is INFO. */
|
|
5
|
+
export declare function parseLogLevel(raw: string | undefined): LogLevel;
|
|
6
|
+
/** Python's `datetime.isoformat()` for an aware UTC datetime: `...T04:05:06.789000+00:00`. */
|
|
7
|
+
export declare function pythonIsoTimestamp(date: Date): string;
|
|
8
|
+
/** Send lines somewhere other than stderr (tests). Returns a restore function. */
|
|
9
|
+
export declare function setLogSink(sink: LogSink): () => void;
|
|
10
|
+
export declare function setLogLevel(level: LogLevel): void;
|
|
11
|
+
export declare function formatRecord(level: LogLevel, logger: string, msg: string, extra?: LogExtra, err?: unknown, now?: Date): string;
|
|
12
|
+
export interface Logger {
|
|
13
|
+
debug(msg: string, extra?: LogExtra): void;
|
|
14
|
+
info(msg: string, extra?: LogExtra): void;
|
|
15
|
+
warning(msg: string, extra?: LogExtra): void;
|
|
16
|
+
error(msg: string, extra?: LogExtra): void;
|
|
17
|
+
/** ERROR with the error's stack under `exc`, like Python's logger.exception. */
|
|
18
|
+
exception(msg: string, err: unknown, extra?: LogExtra): void;
|
|
19
|
+
/** Any level, with an optional error under `exc`. */
|
|
20
|
+
record(level: LogLevel, msg: string, extra?: LogExtra, err?: unknown): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function getLogger(name: string): Logger;
|
|
23
|
+
/**
|
|
24
|
+
* Route console.* through the JSON-lines logger (logger "console"), so the
|
|
25
|
+
* framework's own output (Nitro's error handler, srvx's banner, uncaught
|
|
26
|
+
* errors) is JSON lines too. An Error argument goes under `exc` with its
|
|
27
|
+
* stack, as one line. Returns a function that puts the old methods back.
|
|
28
|
+
*/
|
|
29
|
+
export declare function installJsonConsole(target?: Console): () => void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-lines logging, the platform's log contract: one JSON object per line
|
|
3
|
+
* on stderr with `ts` (Python isoformat, `+00:00`), `level`, `logger`, `msg`,
|
|
4
|
+
* then the extras passed at the call site, then `exc` for an error. Extras
|
|
5
|
+
* never overwrite the four standard keys. Loki and the dashboards parse this
|
|
6
|
+
* shape.
|
|
7
|
+
*
|
|
8
|
+
* The sink and threshold are one per process, kept on globalThis: a server
|
|
9
|
+
* bundled twice (Nitro's entry plus Start's SSR graph) still has one of each.
|
|
10
|
+
* The initial threshold comes from LOG_LEVEL, the only variable read here.
|
|
11
|
+
*/
|
|
12
|
+
import { format } from "node:util";
|
|
13
|
+
const LEVEL_RANK = {
|
|
14
|
+
DEBUG: 10,
|
|
15
|
+
INFO: 20,
|
|
16
|
+
WARNING: 30,
|
|
17
|
+
ERROR: 40,
|
|
18
|
+
};
|
|
19
|
+
/** `WARN` means WARNING; anything unknown or unset is INFO. */
|
|
20
|
+
export function parseLogLevel(raw) {
|
|
21
|
+
const upper = (raw ?? "").trim().toUpperCase();
|
|
22
|
+
if (upper === "WARN")
|
|
23
|
+
return "WARNING";
|
|
24
|
+
return upper in LEVEL_RANK ? upper : "INFO";
|
|
25
|
+
}
|
|
26
|
+
function state() {
|
|
27
|
+
const store = globalThis;
|
|
28
|
+
const slot = Symbol.for("@viktorbarzin:log");
|
|
29
|
+
let found = store[slot];
|
|
30
|
+
if (found === undefined) {
|
|
31
|
+
found = {
|
|
32
|
+
sink: (line) => {
|
|
33
|
+
process.stderr.write(`${line}\n`);
|
|
34
|
+
},
|
|
35
|
+
threshold: LEVEL_RANK[parseLogLevel(process.env.LOG_LEVEL)],
|
|
36
|
+
};
|
|
37
|
+
store[slot] = found;
|
|
38
|
+
}
|
|
39
|
+
return found;
|
|
40
|
+
}
|
|
41
|
+
/** Python's `datetime.isoformat()` for an aware UTC datetime: `...T04:05:06.789000+00:00`. */
|
|
42
|
+
export function pythonIsoTimestamp(date) {
|
|
43
|
+
return date.toISOString().replace("Z", "000+00:00");
|
|
44
|
+
}
|
|
45
|
+
/** Send lines somewhere other than stderr (tests). Returns a restore function. */
|
|
46
|
+
export function setLogSink(sink) {
|
|
47
|
+
const current = state();
|
|
48
|
+
const previous = current.sink;
|
|
49
|
+
current.sink = sink;
|
|
50
|
+
return () => {
|
|
51
|
+
current.sink = previous;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function setLogLevel(level) {
|
|
55
|
+
state().threshold = LEVEL_RANK[level];
|
|
56
|
+
}
|
|
57
|
+
function jsonReplacer(_key, value) {
|
|
58
|
+
// Python's json.dumps(default=str) stringifies what JSON cannot carry.
|
|
59
|
+
if (typeof value === "bigint")
|
|
60
|
+
return value.toString();
|
|
61
|
+
if (value instanceof Error)
|
|
62
|
+
return String(value);
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function formatError(err) {
|
|
66
|
+
if (err instanceof Error)
|
|
67
|
+
return err.stack ?? `${err.name}: ${err.message}`;
|
|
68
|
+
return String(err);
|
|
69
|
+
}
|
|
70
|
+
export function formatRecord(level, logger, msg, extra, err, now = new Date()) {
|
|
71
|
+
const payload = {
|
|
72
|
+
ts: pythonIsoTimestamp(now),
|
|
73
|
+
level,
|
|
74
|
+
logger,
|
|
75
|
+
msg,
|
|
76
|
+
};
|
|
77
|
+
if (extra) {
|
|
78
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
79
|
+
if (!(key in payload))
|
|
80
|
+
payload[key] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (err !== undefined)
|
|
84
|
+
payload.exc = formatError(err);
|
|
85
|
+
return JSON.stringify(payload, jsonReplacer);
|
|
86
|
+
}
|
|
87
|
+
export function getLogger(name) {
|
|
88
|
+
const emit = (level, msg, extra, err) => {
|
|
89
|
+
const current = state();
|
|
90
|
+
if (LEVEL_RANK[level] < current.threshold)
|
|
91
|
+
return;
|
|
92
|
+
current.sink(formatRecord(level, name, msg, extra, err));
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
debug: (msg, extra) => emit("DEBUG", msg, extra),
|
|
96
|
+
info: (msg, extra) => emit("INFO", msg, extra),
|
|
97
|
+
warning: (msg, extra) => emit("WARNING", msg, extra),
|
|
98
|
+
error: (msg, extra) => emit("ERROR", msg, extra),
|
|
99
|
+
exception: (msg, err, extra) => emit("ERROR", msg, extra, err),
|
|
100
|
+
record: emit,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const CONSOLE_LEVELS = {
|
|
104
|
+
error: "ERROR",
|
|
105
|
+
warn: "WARNING",
|
|
106
|
+
log: "INFO",
|
|
107
|
+
info: "INFO",
|
|
108
|
+
debug: "DEBUG",
|
|
109
|
+
trace: "DEBUG",
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Route console.* through the JSON-lines logger (logger "console"), so the
|
|
113
|
+
* framework's own output (Nitro's error handler, srvx's banner, uncaught
|
|
114
|
+
* errors) is JSON lines too. An Error argument goes under `exc` with its
|
|
115
|
+
* stack, as one line. Returns a function that puts the old methods back.
|
|
116
|
+
*/
|
|
117
|
+
export function installJsonConsole(target = console) {
|
|
118
|
+
const consoleLog = getLogger("console");
|
|
119
|
+
const methods = target;
|
|
120
|
+
const saved = new Map();
|
|
121
|
+
for (const [method, level] of Object.entries(CONSOLE_LEVELS)) {
|
|
122
|
+
saved.set(method, methods[method]);
|
|
123
|
+
methods[method] = (...args) => {
|
|
124
|
+
const err = args.find((arg) => arg instanceof Error);
|
|
125
|
+
const rest = args.filter((arg) => arg !== err);
|
|
126
|
+
const msg = rest.length > 0 ? format(...rest) : err ? String(err) : "";
|
|
127
|
+
consoleLog.record(level, msg, undefined, err);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return () => {
|
|
131
|
+
for (const [method, fn] of saved)
|
|
132
|
+
methods[method] = fn;
|
|
133
|
+
};
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@viktorbarzin/log",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "JSON-lines logger for the homelab app platform: ts, level, logger, msg, extras, exc.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"engines": {
|
|
20
|
+
"bun": ">=1.4.3"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/ViktorBarzin/app-kit.git",
|
|
25
|
+
"directory": "packages/log"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/ViktorBarzin/app-kit/tree/master/packages/log#readme",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"provenance": true
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -b",
|
|
34
|
+
"test": "bun test"
|
|
35
|
+
}
|
|
36
|
+
}
|