@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 CHANGED
@@ -1,3 +1,38 @@
1
- # Temporary Holding Version
1
+ # @viktorbarzin/log
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
@@ -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
- "name": "@viktorbarzin/log",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }