@kindgi/log 0.0.0-stage → 0.1.4

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/src/trace.ts ADDED
@@ -0,0 +1,93 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ /**
5
+ * W3C Trace Context (`traceparent`), the ids every record written inside a
6
+ * request or a run carries: honour a caller's trace, or start one, and give
7
+ * each request its own span. Pure functions on Web Crypto, so they run in
8
+ * Node and in workers alike. No spans are recorded or exported here.
9
+ */
10
+
11
+ export interface TraceContext {
12
+ /** 32 lowercase hex characters, not all zero. */
13
+ readonly traceId: string;
14
+ /** 16 lowercase hex characters, not all zero: this span. */
15
+ readonly spanId: string;
16
+ /** The span this one is a child of, when there is one. */
17
+ readonly parentSpanId?: string;
18
+ /** Trace flags, two hex characters; `01` is sampled. */
19
+ readonly flags: string;
20
+ }
21
+
22
+ const TRACEPARENT = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(?:-.*)?$/;
23
+
24
+ /**
25
+ * An incoming `traceparent`'s trace id and parent span, or `undefined`
26
+ * when the header is absent or malformed: then the caller starts a fresh
27
+ * trace (a bad header is replaced, never trusted). Version `ff` is
28
+ * invalid; a future version is read by its first four fields, as the
29
+ * spec says. All-zero ids are invalid.
30
+ */
31
+ export function parseTraceparent(
32
+ header: string | null | undefined,
33
+ ): { readonly traceId: string; readonly parentSpanId: string; readonly flags: string } | undefined {
34
+ if (typeof header !== 'string') return undefined;
35
+ // Hex must be lowercase (the spec): an uppercase header is malformed.
36
+ const m = TRACEPARENT.exec(header.trim());
37
+ if (m === null) return undefined;
38
+ const [, version, traceId, parentSpanId, flags] = m as unknown as [
39
+ string,
40
+ string,
41
+ string,
42
+ string,
43
+ string,
44
+ ];
45
+ if (version === 'ff') return undefined;
46
+ if (version === '00' && header.trim().length !== 55) return undefined;
47
+ if (/^0+$/.test(traceId) || /^0+$/.test(parentSpanId)) return undefined;
48
+ return { traceId, parentSpanId, flags };
49
+ }
50
+
51
+ function randomHex(bytes: number): string {
52
+ for (;;) {
53
+ const buf = new Uint8Array(bytes);
54
+ globalThis.crypto.getRandomValues(buf);
55
+ const hex = [...buf].map((b) => b.toString(16).padStart(2, '0')).join('');
56
+ if (!/^0+$/.test(hex)) return hex;
57
+ }
58
+ }
59
+
60
+ /** A fresh trace: a new trace id and a first span, sampled. */
61
+ export function newTraceContext(): TraceContext {
62
+ return { traceId: randomHex(16), spanId: randomHex(8), flags: '01' };
63
+ }
64
+
65
+ /** A new span in the same trace, whose parent is `parent`'s span. */
66
+ export function childSpan(parent: TraceContext): TraceContext {
67
+ return {
68
+ traceId: parent.traceId,
69
+ spanId: randomHex(8),
70
+ parentSpanId: parent.spanId,
71
+ flags: parent.flags,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * The context for a request: a child span of the caller's trace when its
77
+ * `traceparent` is valid, else a fresh trace.
78
+ */
79
+ export function traceFromHeader(header: string | null | undefined): TraceContext {
80
+ const incoming = parseTraceparent(header);
81
+ if (incoming === undefined) return newTraceContext();
82
+ return {
83
+ traceId: incoming.traceId,
84
+ spanId: randomHex(8),
85
+ parentSpanId: incoming.parentSpanId,
86
+ flags: incoming.flags,
87
+ };
88
+ }
89
+
90
+ /** The `traceparent` (and `traceresponse`) value for a context: `00-<trace>-<span>-<flags>`. */
91
+ export function formatTraceparent(ctx: TraceContext): string {
92
+ return `00-${ctx.traceId}-${ctx.spanId}-${ctx.flags}`;
93
+ }
package/src/types.ts ADDED
@@ -0,0 +1,70 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { LogLevel } from './levels.js';
5
+
6
+ /** A record's fields: values, never text pasted into the message. */
7
+ export type LogFields = Readonly<Record<string, unknown>>;
8
+
9
+ /**
10
+ * One log record, the JSON format's line. `time` (RFC 3339), `level`,
11
+ * `severity` (Cloud Logging's name for the level), `subsystem` and
12
+ * `message` are always there; the correlation fields come next when
13
+ * known, then the event's own fields. An error is `err`.
14
+ */
15
+ export interface LogRecord {
16
+ readonly time: string;
17
+ readonly level: LogLevel;
18
+ readonly severity: string;
19
+ readonly subsystem: string;
20
+ readonly message: string;
21
+ readonly traceId?: string;
22
+ readonly spanId?: string;
23
+ readonly requestId?: string;
24
+ readonly tenantId?: string;
25
+ readonly projectId?: string;
26
+ readonly runId?: string;
27
+ readonly err?: SerializedError;
28
+ readonly [field: string]: unknown;
29
+ }
30
+
31
+ /** An error, as a record carries it. `stack` at `error`, or when the logger is at `debug` or below. */
32
+ export interface SerializedError {
33
+ readonly name: string;
34
+ readonly message: string;
35
+ readonly code?: string;
36
+ readonly stack?: string;
37
+ readonly cause?: SerializedError | string;
38
+ }
39
+
40
+ /**
41
+ * How one record is written, beyond its fields.
42
+ *
43
+ * - `inMessage`: the fields its message already states. The access
44
+ * line `GET /v1/runs/:runId 200 12ms` states `method`, `route`,
45
+ * `status` and `durationMs`. The pretty format leaves them out, so
46
+ * a terminal line doesn't say everything twice; JSON keeps every
47
+ * field, for queries.
48
+ */
49
+ export interface LogOptions {
50
+ readonly inMessage?: readonly string[];
51
+ }
52
+
53
+ /**
54
+ * A logger. `child` returns one whose records all carry `bindings`
55
+ * (`log.child({ subsystem: 'leases' })`, `log.child({ runId })`); a
56
+ * child's `subsystem` replaces its parent's, so name it in full
57
+ * (`kernel.sweeper`). Pass an error as `{ err: error }`.
58
+ */
59
+ export interface Logger {
60
+ error(message: string, fields?: LogFields, options?: LogOptions): void;
61
+ warn(message: string, fields?: LogFields, options?: LogOptions): void;
62
+ info(message: string, fields?: LogFields, options?: LogOptions): void;
63
+ debug(message: string, fields?: LogFields, options?: LogOptions): void;
64
+ trace(message: string, fields?: LogFields, options?: LogOptions): void;
65
+ child(bindings: LogFields): Logger;
66
+ /** Whether a record at `level` would be written: skip building costly fields when it wouldn't. */
67
+ isLevelEnabled(level: LogLevel): boolean;
68
+ }
69
+
70
+ export type LogFormat = 'json' | 'pretty';