@viktorbarzin/log 0.1.0 → 1.0.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,38 +1,86 @@
1
1
  # @viktorbarzin/log
2
2
 
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.
3
+ A JSON-lines logger for the platform's log standard (app-kit ADR-0002): one
4
+ object per line on stdout, with field names from the OpenTelemetry log data
5
+ model and semantic conventions. No dependencies; works on Bun and Node.
7
6
 
8
7
  <!-- typecheck-preamble
9
8
  declare const err: unknown;
9
+ declare const request: Request;
10
+ declare const handle: (request: Request) => Promise<Response>;
10
11
  -->
11
12
  ```ts
12
- import { getLogger, installJsonConsole } from "@viktorbarzin/log";
13
+ import {
14
+ getLogger,
15
+ installJsonConsole,
16
+ setLogContext,
17
+ withLogContext,
18
+ } from "@viktorbarzin/log";
13
19
 
14
20
  const log = getLogger("myapp.sync");
15
- log.info("sync served", { user: "someone", lists: 3 });
21
+ log.info("sync served", { lists: 3, duration_ms: 12 });
16
22
  log.exception("sync failed", err, { path: "/api/sync" });
17
23
 
24
+ // Per request: every line inside carries enduser.id, trace_id and span_id.
25
+ await withLogContext({ traceId: request.headers.get("x-trace-id") ?? undefined }, async () => {
26
+ setLogContext({ enduserId: "the person's OIDC sub" });
27
+ return handle(request);
28
+ });
29
+
18
30
  installJsonConsole(); // console.* from the framework becomes JSON lines too (logger "console")
19
31
  ```
20
32
 
21
33
  ```json
22
- {"ts":"2026-10-16T04:05:06.789000+00:00","level":"INFO","logger":"myapp.sync","msg":"sync served","user":"someone","lists":3}
34
+ {"timestamp":"2026-10-16T04:05:06.789000000Z","severity_text":"INFO","severity_number":9,"body":"sync served","logger.name":"myapp.sync","service.name":"myapp","service.version":"1a2b3c4d","deployment.environment.name":"production","enduser.id":"…","lists":3,"duration_ms":12}
23
35
  ```
24
36
 
37
+ ## Fields
38
+
39
+ | Field | Meaning |
40
+ |---|---|
41
+ | `timestamp` | RFC 3339 in UTC with nine fractional digits and `Z`. JavaScript's clock has millisecond precision, so the last six digits are zero unless `formatRecord` is given nanoseconds |
42
+ | `severity_text`, `severity_number` | `DEBUG` 5, `INFO` 9, `WARN` 13, `ERROR` 17, `FATAL` 21 |
43
+ | `body` | the message |
44
+ | `logger.name` | the name given to `getLogger` |
45
+ | `service.name`, `service.version`, `deployment.environment.name` | from `setLogService`, else `OTEL_SERVICE_NAME`, `SERVICE_VERSION`, `DEPLOYMENT_ENVIRONMENT` |
46
+ | `trace_id`, `span_id`, `enduser.id` | from the current `withLogContext` scope |
47
+ | extras | the call's extras (snake_case keys by convention), after the scope's `attributes` |
48
+ | `exception.type`, `exception.message`, `exception.stacktrace` | for an error, last |
49
+
50
+ Fields with no value are left out rather than written as `null`. Extras
51
+ never overwrite the standard fields.
52
+
53
+ `logger.name` is our key. OpenTelemetry's data model records the logger
54
+ name as the instrumentation scope name, which has no attribute key of its
55
+ own in a flat JSON line, and the semantic conventions define no logger
56
+ attribute. `logger.name` keeps the dotted OpenTelemetry style and says
57
+ what the value is. The libraries for other languages should use the same
58
+ key, so that one Loki query covers every app.
59
+
25
60
  ## API
26
61
 
27
62
  | Export | |
28
63
  |---|---|
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` |
64
+ | `getLogger(name)` | `debug`, `info`, `warn` (and `warning`), `error`, `fatal`, `exception(msg, err, extra?)`, `record(level, msg, extra?, err?)` |
65
+ | `withLogContext(context, fn)` | run `fn` with `enduserId`, `traceId`, `spanId` and `attributes` on every line written inside it, async work included; scopes nest |
66
+ | `setLogContext(context)` | add fields to the current scope (for example once the session is known); false outside a scope |
67
+ | `getLogContext()` | the current scope's fields |
68
+ | `setLogService(service)` | set `name`, `version`, `environment` explicitly; returns a restore function |
69
+ | `serviceFromEnv(env?)` | the service fields from the environment |
70
+ | `parseLogLevel(raw)` | `WARNING` → `WARN`; unknown or unset → `INFO` |
71
+ | `setLogLevel(level)` | the threshold; starts from `LOG_LEVEL` (`WARNING` is accepted for `WARN`) |
32
72
  | `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 |
73
+ | `installJsonConsole(target?)` | route `console.*` through the logger; an `Error` argument goes under `exception.*`; returns a restore function |
74
+ | `formatRecord`, `formatTimestamp`, `SEVERITY_NUMBER` | the formatting, exported for tests |
75
+
76
+ The sink, threshold, service fields and context store are one per process
77
+ (kept on `globalThis`), so a server bundled twice still logs through one
78
+ sink and one context. The environment variables read are `LOG_LEVEL`,
79
+ `OTEL_SERVICE_NAME`, `SERVICE_VERSION` and `DEPLOYMENT_ENVIRONMENT`.
80
+
81
+ ## Upgrading from 0.x
35
82
 
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.
83
+ 1.0 renames every field (ADR-0002) and writes to stdout instead of stderr.
84
+ Code that calls the logger needs no change. Loki queries, dashboards and
85
+ tests that read `ts`, `level`, `logger`, `msg` or `exc` move to
86
+ `timestamp`, `severity_text`, `logger.name`, `body` and `exception.*`.
package/dist/index.d.ts CHANGED
@@ -1,29 +1,87 @@
1
- export type LogLevel = "DEBUG" | "INFO" | "WARNING" | "ERROR";
1
+ export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "FATAL";
2
+ /** What `setLogLevel` and `record` accept: a level, or `WARNING` for WARN. */
3
+ export type LogLevelName = LogLevel | "WARNING";
2
4
  export type LogExtra = Record<string, unknown>;
3
5
  export type LogSink = (line: string) => void;
4
- /** `WARN` means WARNING; anything unknown or unset is INFO. */
6
+ /** OpenTelemetry severity numbers: the first number of each range. */
7
+ export declare const SEVERITY_NUMBER: Readonly<Record<LogLevel, number>>;
8
+ /** The resource fields on every line. Unknown fields are left out. */
9
+ export interface LogService {
10
+ /** `service.name`, the app. */
11
+ name?: string | undefined;
12
+ /** `service.version`, the release. */
13
+ version?: string | undefined;
14
+ /** `deployment.environment.name`, e.g. `production` or `dev`. */
15
+ environment?: string | undefined;
16
+ }
17
+ /** Per-request fields, set with `withLogContext` and `setLogContext`. */
18
+ export interface LogContext {
19
+ /** `enduser.id`: the person's stable id (for example the OIDC `sub`). */
20
+ enduserId?: string | undefined;
21
+ /** `trace_id`, hex. */
22
+ traceId?: string | undefined;
23
+ /** `span_id`, hex. */
24
+ spanId?: string | undefined;
25
+ /** More snake_case attributes for every line in the scope. */
26
+ attributes?: LogExtra | undefined;
27
+ }
28
+ /** `WARNING` means WARN; anything unknown or unset is INFO. */
5
29
  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. */
30
+ /** The service fields from OTEL_SERVICE_NAME, SERVICE_VERSION and DEPLOYMENT_ENVIRONMENT. */
31
+ export declare function serviceFromEnv(env?: Record<string, string | undefined>): LogService;
32
+ /**
33
+ * RFC 3339 in UTC with nine fractional digits: `2026-10-16T04:05:06.789000000Z`.
34
+ * Takes a Date (millisecond precision, so the last six digits are zero) or
35
+ * nanoseconds since the epoch as a bigint (every digit kept).
36
+ */
37
+ export declare function formatTimestamp(time: Date | bigint): string;
38
+ /** Send lines somewhere other than stdout (tests). Returns a restore function. */
9
39
  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;
40
+ export declare function setLogLevel(level: LogLevelName): void;
41
+ /**
42
+ * Set the service fields explicitly; given fields replace the ones from the
43
+ * environment. Returns a restore function.
44
+ */
45
+ export declare function setLogService(service: LogService): () => void;
46
+ /**
47
+ * Run `fn` with `context` added to every log line written inside it,
48
+ * including in async work it starts. Nested scopes inherit the outer fields
49
+ * and may override them.
50
+ */
51
+ export declare function withLogContext<T>(context: LogContext, fn: () => T): T;
52
+ /**
53
+ * Add fields to the current `withLogContext` scope, for example `enduserId`
54
+ * once the session is known. The whole scope sees them, including lines
55
+ * written after this call by code that started earlier. Outside a scope it
56
+ * changes nothing and returns false.
57
+ */
58
+ export declare function setLogContext(context: LogContext): boolean;
59
+ /** The current scope's fields, or undefined outside `withLogContext`. */
60
+ export declare function getLogContext(): LogContext | undefined;
61
+ /** The service and context for one record; `formatRecord` reads the process's when omitted. */
62
+ export interface RecordScope {
63
+ service?: LogService | undefined;
64
+ context?: LogContext | undefined;
65
+ }
66
+ export declare function formatRecord(level: LogLevelName, logger: string, body: string, extra?: LogExtra, err?: unknown, now?: Date | bigint, scope?: RecordScope): string;
12
67
  export interface Logger {
13
68
  debug(msg: string, extra?: LogExtra): void;
14
69
  info(msg: string, extra?: LogExtra): void;
70
+ warn(msg: string, extra?: LogExtra): void;
71
+ /** The same as `warn`. */
15
72
  warning(msg: string, extra?: LogExtra): void;
16
73
  error(msg: string, extra?: LogExtra): void;
17
- /** ERROR with the error's stack under `exc`, like Python's logger.exception. */
74
+ fatal(msg: string, extra?: LogExtra): void;
75
+ /** ERROR with the error under `exception.*`, like Python's logger.exception. */
18
76
  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;
77
+ /** Any level, with an optional error under `exception.*`. */
78
+ record(level: LogLevelName, msg: string, extra?: LogExtra, err?: unknown): void;
21
79
  }
22
80
  export declare function getLogger(name: string): Logger;
23
81
  /**
24
82
  * Route console.* through the JSON-lines logger (logger "console"), so the
25
83
  * 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.
84
+ * errors) is JSON lines too. An Error argument goes under `exception.*`, as
85
+ * one line. Returns a function that puts the old methods back.
28
86
  */
29
87
  export declare function installJsonConsole(target?: Console): () => void;
package/dist/index.js CHANGED
@@ -1,27 +1,53 @@
1
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.
2
+ * JSON-lines logging with OpenTelemetry log data model field names (app-kit
3
+ * ADR-0002): one JSON object per line on stdout with `timestamp`,
4
+ * `severity_text`, `severity_number`, `body`, `logger.name`, the service
5
+ * fields, the request context (`trace_id`, `span_id`, `enduser.id`), then
6
+ * the extras passed at the call site, then `exception.*` for an error.
7
+ * Extras never overwrite the standard keys.
7
8
  *
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.
9
+ * The sink, threshold, service fields and the context store are one per
10
+ * process, kept on globalThis: a server bundled twice (Nitro's entry plus
11
+ * Start's SSR graph) still has one of each. The initial values come from
12
+ * LOG_LEVEL, OTEL_SERVICE_NAME, SERVICE_VERSION and DEPLOYMENT_ENVIRONMENT.
11
13
  */
14
+ import { AsyncLocalStorage } from "node:async_hooks";
12
15
  import { format } from "node:util";
13
- const LEVEL_RANK = {
14
- DEBUG: 10,
15
- INFO: 20,
16
- WARNING: 30,
17
- ERROR: 40,
16
+ /** OpenTelemetry severity numbers: the first number of each range. */
17
+ export const SEVERITY_NUMBER = {
18
+ DEBUG: 5,
19
+ INFO: 9,
20
+ WARN: 13,
21
+ ERROR: 17,
22
+ FATAL: 21,
18
23
  };
19
- /** `WARN` means WARNING; anything unknown or unset is INFO. */
24
+ function toLevel(level) {
25
+ return level === "WARNING" ? "WARN" : level;
26
+ }
27
+ /** `WARNING` means WARN; anything unknown or unset is INFO. */
20
28
  export function parseLogLevel(raw) {
21
29
  const upper = (raw ?? "").trim().toUpperCase();
22
- if (upper === "WARN")
23
- return "WARNING";
24
- return upper in LEVEL_RANK ? upper : "INFO";
30
+ if (upper === "WARNING")
31
+ return "WARN";
32
+ return upper in SEVERITY_NUMBER ? upper : "INFO";
33
+ }
34
+ function nonBlank(value) {
35
+ const trimmed = value?.trim();
36
+ return trimmed ? trimmed : undefined;
37
+ }
38
+ /** The service fields from OTEL_SERVICE_NAME, SERVICE_VERSION and DEPLOYMENT_ENVIRONMENT. */
39
+ export function serviceFromEnv(env = process.env) {
40
+ const service = {};
41
+ const name = nonBlank(env.OTEL_SERVICE_NAME);
42
+ const version = nonBlank(env.SERVICE_VERSION);
43
+ const environment = nonBlank(env.DEPLOYMENT_ENVIRONMENT);
44
+ if (name !== undefined)
45
+ service.name = name;
46
+ if (version !== undefined)
47
+ service.version = version;
48
+ if (environment !== undefined)
49
+ service.environment = environment;
50
+ return service;
25
51
  }
26
52
  function state() {
27
53
  const store = globalThis;
@@ -30,19 +56,31 @@ function state() {
30
56
  if (found === undefined) {
31
57
  found = {
32
58
  sink: (line) => {
33
- process.stderr.write(`${line}\n`);
59
+ process.stdout.write(`${line}\n`);
34
60
  },
35
- threshold: LEVEL_RANK[parseLogLevel(process.env.LOG_LEVEL)],
61
+ threshold: SEVERITY_NUMBER[parseLogLevel(process.env.LOG_LEVEL)],
62
+ service: serviceFromEnv(),
63
+ context: new AsyncLocalStorage(),
36
64
  };
37
65
  store[slot] = found;
38
66
  }
39
67
  return found;
40
68
  }
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");
69
+ const NANOS_PER_MS = 1000000n;
70
+ const NANOS_PER_SECOND = 1000000000n;
71
+ /**
72
+ * RFC 3339 in UTC with nine fractional digits: `2026-10-16T04:05:06.789000000Z`.
73
+ * Takes a Date (millisecond precision, so the last six digits are zero) or
74
+ * nanoseconds since the epoch as a bigint (every digit kept).
75
+ */
76
+ export function formatTimestamp(time) {
77
+ const nanos = typeof time === "bigint" ? time : BigInt(time.getTime()) * NANOS_PER_MS;
78
+ const seconds = nanos / NANOS_PER_SECOND;
79
+ const fraction = (nanos % NANOS_PER_SECOND).toString().padStart(9, "0");
80
+ const whole = new Date(Number(seconds) * 1000).toISOString().slice(0, 19);
81
+ return `${whole}.${fraction}Z`;
44
82
  }
45
- /** Send lines somewhere other than stderr (tests). Returns a restore function. */
83
+ /** Send lines somewhere other than stdout (tests). Returns a restore function. */
46
84
  export function setLogSink(sink) {
47
85
  const current = state();
48
86
  const previous = current.sink;
@@ -52,57 +90,153 @@ export function setLogSink(sink) {
52
90
  };
53
91
  }
54
92
  export function setLogLevel(level) {
55
- state().threshold = LEVEL_RANK[level];
93
+ state().threshold = SEVERITY_NUMBER[toLevel(level)];
94
+ }
95
+ /**
96
+ * Set the service fields explicitly; given fields replace the ones from the
97
+ * environment. Returns a restore function.
98
+ */
99
+ export function setLogService(service) {
100
+ const current = state();
101
+ const previous = current.service;
102
+ const next = { ...previous };
103
+ for (const key of ["name", "version", "environment"]) {
104
+ const value = nonBlank(service[key]);
105
+ if (value !== undefined)
106
+ next[key] = value;
107
+ }
108
+ current.service = next;
109
+ return () => {
110
+ current.service = previous;
111
+ };
112
+ }
113
+ function mergeContext(parent, child) {
114
+ const merged = { ...parent };
115
+ if (child.enduserId !== undefined)
116
+ merged.enduserId = child.enduserId;
117
+ if (child.traceId !== undefined)
118
+ merged.traceId = child.traceId;
119
+ if (child.spanId !== undefined)
120
+ merged.spanId = child.spanId;
121
+ if (child.attributes !== undefined)
122
+ merged.attributes = { ...parent?.attributes, ...child.attributes };
123
+ return merged;
124
+ }
125
+ /**
126
+ * Run `fn` with `context` added to every log line written inside it,
127
+ * including in async work it starts. Nested scopes inherit the outer fields
128
+ * and may override them.
129
+ */
130
+ export function withLogContext(context, fn) {
131
+ const store = state().context;
132
+ return store.run(mergeContext(store.getStore(), context), fn);
133
+ }
134
+ /**
135
+ * Add fields to the current `withLogContext` scope, for example `enduserId`
136
+ * once the session is known. The whole scope sees them, including lines
137
+ * written after this call by code that started earlier. Outside a scope it
138
+ * changes nothing and returns false.
139
+ */
140
+ export function setLogContext(context) {
141
+ const current = state().context.getStore();
142
+ if (current === undefined)
143
+ return false;
144
+ Object.assign(current, mergeContext(current, context));
145
+ return true;
146
+ }
147
+ /** The current scope's fields, or undefined outside `withLogContext`. */
148
+ export function getLogContext() {
149
+ const current = state().context.getStore();
150
+ return current === undefined ? undefined : { ...current };
56
151
  }
57
152
  function jsonReplacer(_key, value) {
58
- // Python's json.dumps(default=str) stringifies what JSON cannot carry.
153
+ // Stringify what JSON cannot carry instead of throwing.
59
154
  if (typeof value === "bigint")
60
155
  return value.toString();
61
156
  if (value instanceof Error)
62
157
  return String(value);
63
158
  return value;
64
159
  }
65
- function formatError(err) {
66
- if (err instanceof Error)
67
- return err.stack ?? `${err.name}: ${err.message}`;
68
- return String(err);
160
+ function exceptionFields(err) {
161
+ if (!(err instanceof Error))
162
+ return { "exception.message": String(err) };
163
+ const ctor = err.constructor?.name;
164
+ const type = ctor && ctor !== "Error" ? ctor : err.name;
165
+ return {
166
+ "exception.type": type,
167
+ "exception.message": err.message,
168
+ "exception.stacktrace": err.stack ?? `${type}: ${err.message}`,
169
+ };
69
170
  }
70
- export function formatRecord(level, logger, msg, extra, err, now = new Date()) {
171
+ const RESERVED = new Set([
172
+ "timestamp",
173
+ "severity_text",
174
+ "severity_number",
175
+ "body",
176
+ "logger.name",
177
+ "service.name",
178
+ "service.version",
179
+ "deployment.environment.name",
180
+ "trace_id",
181
+ "span_id",
182
+ "enduser.id",
183
+ "exception.type",
184
+ "exception.message",
185
+ "exception.stacktrace",
186
+ ]);
187
+ export function formatRecord(level, logger, body, extra, err, now = new Date(), scope) {
188
+ const severity = toLevel(level);
189
+ const service = scope?.service ?? state().service;
190
+ const context = scope ? scope.context : state().context.getStore();
71
191
  const payload = {
72
- ts: pythonIsoTimestamp(now),
73
- level,
74
- logger,
75
- msg,
192
+ timestamp: formatTimestamp(now),
193
+ severity_text: severity,
194
+ severity_number: SEVERITY_NUMBER[severity],
195
+ body,
196
+ "logger.name": logger,
76
197
  };
77
- if (extra) {
78
- for (const [key, value] of Object.entries(extra)) {
79
- if (!(key in payload))
80
- payload[key] = value;
81
- }
198
+ if (service.name !== undefined)
199
+ payload["service.name"] = service.name;
200
+ if (service.version !== undefined)
201
+ payload["service.version"] = service.version;
202
+ if (service.environment !== undefined)
203
+ payload["deployment.environment.name"] = service.environment;
204
+ if (context?.traceId !== undefined)
205
+ payload.trace_id = context.traceId;
206
+ if (context?.spanId !== undefined)
207
+ payload.span_id = context.spanId;
208
+ if (context?.enduserId !== undefined)
209
+ payload["enduser.id"] = context.enduserId;
210
+ const attributes = { ...context?.attributes, ...extra };
211
+ for (const [key, value] of Object.entries(attributes)) {
212
+ if (!RESERVED.has(key))
213
+ payload[key] = value;
82
214
  }
83
215
  if (err !== undefined)
84
- payload.exc = formatError(err);
216
+ Object.assign(payload, exceptionFields(err));
85
217
  return JSON.stringify(payload, jsonReplacer);
86
218
  }
87
219
  export function getLogger(name) {
88
220
  const emit = (level, msg, extra, err) => {
89
221
  const current = state();
90
- if (LEVEL_RANK[level] < current.threshold)
222
+ if (SEVERITY_NUMBER[toLevel(level)] < current.threshold)
91
223
  return;
92
224
  current.sink(formatRecord(level, name, msg, extra, err));
93
225
  };
94
226
  return {
95
227
  debug: (msg, extra) => emit("DEBUG", msg, extra),
96
228
  info: (msg, extra) => emit("INFO", msg, extra),
97
- warning: (msg, extra) => emit("WARNING", msg, extra),
229
+ warn: (msg, extra) => emit("WARN", msg, extra),
230
+ warning: (msg, extra) => emit("WARN", msg, extra),
98
231
  error: (msg, extra) => emit("ERROR", msg, extra),
232
+ fatal: (msg, extra) => emit("FATAL", msg, extra),
99
233
  exception: (msg, err, extra) => emit("ERROR", msg, extra, err),
100
234
  record: emit,
101
235
  };
102
236
  }
103
237
  const CONSOLE_LEVELS = {
104
238
  error: "ERROR",
105
- warn: "WARNING",
239
+ warn: "WARN",
106
240
  log: "INFO",
107
241
  info: "INFO",
108
242
  debug: "DEBUG",
@@ -111,8 +245,8 @@ const CONSOLE_LEVELS = {
111
245
  /**
112
246
  * Route console.* through the JSON-lines logger (logger "console"), so the
113
247
  * 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.
248
+ * errors) is JSON lines too. An Error argument goes under `exception.*`, as
249
+ * one line. Returns a function that puts the old methods back.
116
250
  */
117
251
  export function installJsonConsole(target = console) {
118
252
  const consoleLog = getLogger("console");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
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.",
3
+ "version": "1.0.0",
4
+ "description": "JSON-lines logger with OpenTelemetry log field names: timestamp, severity, body, service, trace and enduser context, exception fields.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",