@telorun/kernel 0.48.0 → 0.50.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/dist/controller-loader.d.ts +6 -1
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +2 -0
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-loaders/bundle-builder.d.ts +2 -1
- package/dist/controller-loaders/bundle-builder.d.ts.map +1 -1
- package/dist/controller-loaders/bundle-builder.js +14 -9
- package/dist/controller-loaders/bundle-builder.js.map +1 -1
- package/dist/controller-loaders/npm-loader.d.ts +8 -2
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +19 -10
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/controllers/logging/console-sink-controller.d.ts +15 -0
- package/dist/controllers/logging/console-sink-controller.d.ts.map +1 -0
- package/dist/controllers/logging/console-sink-controller.js +40 -0
- package/dist/controllers/logging/console-sink-controller.js.map +1 -0
- package/dist/controllers/logging/file-sink-controller.d.ts +11 -0
- package/dist/controllers/logging/file-sink-controller.d.ts.map +1 -0
- package/dist/controllers/logging/file-sink-controller.js +33 -0
- package/dist/controllers/logging/file-sink-controller.js.map +1 -0
- package/dist/controllers/logging/sink-identity.d.ts +37 -0
- package/dist/controllers/logging/sink-identity.d.ts.map +1 -0
- package/dist/controllers/logging/sink-identity.js +40 -0
- package/dist/controllers/logging/sink-identity.js.map +1 -0
- package/dist/controllers/module/import-controller.d.ts.map +1 -1
- package/dist/controllers/module/import-controller.js +25 -0
- package/dist/controllers/module/import-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +1 -0
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/evaluation-context.d.ts +27 -1
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +84 -16
- package/dist/evaluation-context.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/internal-context.d.ts +4 -0
- package/dist/internal-context.d.ts.map +1 -1
- package/dist/kernel.d.ts +26 -0
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +78 -5
- package/dist/kernel.js.map +1 -1
- package/dist/logging/bootstrap-writer.d.ts +23 -0
- package/dist/logging/bootstrap-writer.d.ts.map +1 -0
- package/dist/logging/bootstrap-writer.js +31 -0
- package/dist/logging/bootstrap-writer.js.map +1 -0
- package/dist/logging/color-precedence.d.ts +27 -0
- package/dist/logging/color-precedence.d.ts.map +1 -0
- package/dist/logging/color-precedence.js +47 -0
- package/dist/logging/color-precedence.js.map +1 -0
- package/dist/logging/console-sink.d.ts +45 -0
- package/dist/logging/console-sink.d.ts.map +1 -0
- package/dist/logging/console-sink.js +56 -0
- package/dist/logging/console-sink.js.map +1 -0
- package/dist/logging/debug-wire-sink.d.ts +38 -0
- package/dist/logging/debug-wire-sink.d.ts.map +1 -0
- package/dist/logging/debug-wire-sink.js +85 -0
- package/dist/logging/debug-wire-sink.js.map +1 -0
- package/dist/logging/drop-accounting.d.ts +34 -0
- package/dist/logging/drop-accounting.d.ts.map +1 -0
- package/dist/logging/drop-accounting.js +98 -0
- package/dist/logging/drop-accounting.js.map +1 -0
- package/dist/logging/encode-json.d.ts +28 -0
- package/dist/logging/encode-json.d.ts.map +1 -0
- package/dist/logging/encode-json.js +86 -0
- package/dist/logging/encode-json.js.map +1 -0
- package/dist/logging/encode-pretty.d.ts +7 -0
- package/dist/logging/encode-pretty.d.ts.map +1 -0
- package/dist/logging/encode-pretty.js +104 -0
- package/dist/logging/encode-pretty.js.map +1 -0
- package/dist/logging/file-sink.d.ts +33 -0
- package/dist/logging/file-sink.d.ts.map +1 -0
- package/dist/logging/file-sink.js +72 -0
- package/dist/logging/file-sink.js.map +1 -0
- package/dist/logging/index.d.ts +39 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/index.js +27 -0
- package/dist/logging/index.js.map +1 -0
- package/dist/logging/kernel-logging.d.ts +110 -0
- package/dist/logging/kernel-logging.d.ts.map +1 -0
- package/dist/logging/kernel-logging.js +199 -0
- package/dist/logging/kernel-logging.js.map +1 -0
- package/dist/logging/log-sink.d.ts +9 -0
- package/dist/logging/log-sink.d.ts.map +1 -0
- package/dist/logging/log-sink.js +8 -0
- package/dist/logging/log-sink.js.map +1 -0
- package/dist/logging/logging-host.d.ts +14 -0
- package/dist/logging/logging-host.d.ts.map +1 -0
- package/dist/logging/logging-host.js +27 -0
- package/dist/logging/logging-host.js.map +1 -0
- package/dist/logging/logging-pipeline.d.ts +89 -0
- package/dist/logging/logging-pipeline.d.ts.map +1 -0
- package/dist/logging/logging-pipeline.js +349 -0
- package/dist/logging/logging-pipeline.js.map +1 -0
- package/dist/logging/normalize-attributes.d.ts +48 -0
- package/dist/logging/normalize-attributes.d.ts.map +1 -0
- package/dist/logging/normalize-attributes.js +140 -0
- package/dist/logging/normalize-attributes.js.map +1 -0
- package/dist/logging/record-buffer.d.ts +24 -0
- package/dist/logging/record-buffer.d.ts.map +1 -0
- package/dist/logging/record-buffer.js +78 -0
- package/dist/logging/record-buffer.js.map +1 -0
- package/dist/logging/redact-attributes.d.ts +45 -0
- package/dist/logging/redact-attributes.d.ts.map +1 -0
- package/dist/logging/redact-attributes.js +152 -0
- package/dist/logging/redact-attributes.js.map +1 -0
- package/dist/logging/sampler.d.ts +34 -0
- package/dist/logging/sampler.d.ts.map +1 -0
- package/dist/logging/sampler.js +52 -0
- package/dist/logging/sampler.js.map +1 -0
- package/dist/logging/scope-config.d.ts +32 -0
- package/dist/logging/scope-config.d.ts.map +1 -0
- package/dist/logging/scope-config.js +2 -0
- package/dist/logging/scope-config.js.map +1 -0
- package/dist/logging/span-id.d.ts +24 -0
- package/dist/logging/span-id.d.ts.map +1 -0
- package/dist/logging/span-id.js +85 -0
- package/dist/logging/span-id.js.map +1 -0
- package/dist/logging/to-error-value.d.ts +3 -0
- package/dist/logging/to-error-value.d.ts.map +1 -0
- package/dist/logging/to-error-value.js +79 -0
- package/dist/logging/to-error-value.js.map +1 -0
- package/dist/logging/trace-parent.d.ts +54 -0
- package/dist/logging/trace-parent.d.ts.map +1 -0
- package/dist/logging/trace-parent.js +92 -0
- package/dist/logging/trace-parent.js.map +1 -0
- package/dist/manifest-schemas.d.ts +1 -1
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +12 -0
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/module-context.d.ts +16 -0
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +6 -0
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts +15 -1
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +45 -0
- package/dist/resource-context.js.map +1 -1
- package/dist/schema-compiled-values.d.ts.map +1 -1
- package/dist/schema-compiled-values.js +7 -0
- package/dist/schema-compiled-values.js.map +1 -1
- package/dist/schema-validator.d.ts +8 -1
- package/dist/schema-validator.d.ts.map +1 -1
- package/dist/schema-validator.js +13 -3
- package/dist/schema-validator.js.map +1 -1
- package/dist/transports/oci/oci-client.d.ts.map +1 -1
- package/dist/transports/oci/oci-client.js +13 -3
- package/dist/transports/oci/oci-client.js.map +1 -1
- package/dist/transports/registry-transport.d.ts.map +1 -1
- package/dist/transports/registry-transport.js +15 -5
- package/dist/transports/registry-transport.js.map +1 -1
- package/package.json +3 -3
- package/src/controller-loader.ts +7 -1
- package/src/controller-loaders/bundle-builder.ts +18 -10
- package/src/controller-loaders/npm-loader.ts +32 -11
- package/src/controllers/logging/console-sink-controller.ts +46 -0
- package/src/controllers/logging/file-sink-controller.ts +40 -0
- package/src/controllers/logging/sink-identity.ts +53 -0
- package/src/controllers/module/import-controller.ts +27 -0
- package/src/controllers/resource-definition/resource-definition-controller.ts +1 -0
- package/src/evaluation-context.ts +93 -15
- package/src/index.ts +54 -0
- package/src/internal-context.ts +4 -0
- package/src/kernel.ts +98 -8
- package/src/logging/bootstrap-writer.ts +38 -0
- package/src/logging/color-precedence.ts +62 -0
- package/src/logging/console-sink.ts +86 -0
- package/src/logging/debug-wire-sink.ts +85 -0
- package/src/logging/drop-accounting.ts +108 -0
- package/src/logging/encode-json.ts +98 -0
- package/src/logging/encode-pretty.ts +117 -0
- package/src/logging/file-sink.ts +87 -0
- package/src/logging/index.ts +65 -0
- package/src/logging/kernel-logging.ts +266 -0
- package/src/logging/log-sink.ts +18 -0
- package/src/logging/logging-host.ts +33 -0
- package/src/logging/logging-pipeline.ts +465 -0
- package/src/logging/normalize-attributes.ts +228 -0
- package/src/logging/record-buffer.ts +72 -0
- package/src/logging/redact-attributes.ts +168 -0
- package/src/logging/sampler.ts +76 -0
- package/src/logging/scope-config.ts +32 -0
- package/src/logging/span-id.ts +84 -0
- package/src/logging/to-error-value.ts +89 -0
- package/src/logging/trace-parent.ts +110 -0
- package/src/manifest-schemas.ts +12 -0
- package/src/module-context.ts +23 -0
- package/src/resource-context.ts +48 -0
- package/src/schema-compiled-values.ts +6 -0
- package/src/schema-validator.ts +20 -5
- package/src/transports/oci/oci-client.ts +13 -3
- package/src/transports/registry-transport.ts +23 -5
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { severityFloor, type AnyValue, type ErrorValue, type LogRecord } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `pretty` encoding — `kernel/specs/logging.md` §11.2. For humans on a
|
|
5
|
+
* terminal:
|
|
6
|
+
*
|
|
7
|
+
* 12:34:56.789 INFO Http.Server.api listening net.host.port=8080
|
|
8
|
+
*
|
|
9
|
+
* The color table is a modal synthesis across pino-pretty, zap, tint,
|
|
10
|
+
* tracing-subscriber, consola, and winston, which do not fully agree — zap alone
|
|
11
|
+
* uses magenta for DEBUG and blue for INFO, and TRACE has no consensus. It is a
|
|
12
|
+
* deliberate choice, not a standard.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const RESET = "\u001b[0m";
|
|
16
|
+
|
|
17
|
+
const LEVEL_COLOR: Readonly<Record<number, string>> = {
|
|
18
|
+
1: "\u001b[2m", // TRACE — dim
|
|
19
|
+
5: "\u001b[34m", // DEBUG — blue
|
|
20
|
+
9: "\u001b[32m", // INFO — green
|
|
21
|
+
13: "\u001b[33m", // WARN — yellow
|
|
22
|
+
17: "\u001b[31m", // ERROR — red
|
|
23
|
+
21: "\u001b[1;41m", // FATAL — bold, red background
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DIM = "\u001b[2m";
|
|
27
|
+
|
|
28
|
+
export interface PrettyEncodeOptions {
|
|
29
|
+
color: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function encodePrettyLine(record: LogRecord, options: PrettyEncodeOptions): string {
|
|
33
|
+
return `${encodePretty(record, options)}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function encodePretty(record: LogRecord, options: PrettyEncodeOptions): string {
|
|
37
|
+
const paint = options.color
|
|
38
|
+
? (code: string, text: string) => `${code}${text}${RESET}`
|
|
39
|
+
: (_code: string, text: string) => text;
|
|
40
|
+
|
|
41
|
+
const parts: string[] = [
|
|
42
|
+
formatLocalTime(record.timestamp),
|
|
43
|
+
paint(LEVEL_COLOR[severityFloor(record.severityNumber)]!, record.severityText.padEnd(5)),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
if (record.resource) parts.push(`${record.resource.kind}.${record.resource.name}`);
|
|
47
|
+
|
|
48
|
+
// The message itself is never colored by level — only the level token is.
|
|
49
|
+
parts.push(record.message);
|
|
50
|
+
|
|
51
|
+
const attributes = record.attributes;
|
|
52
|
+
if (attributes) {
|
|
53
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
54
|
+
parts.push(`${paint(DIM, key)}=${formatAttributeValue(value)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (record.droppedAttributesCount) {
|
|
59
|
+
parts.push(`${paint(DIM, "dropped_attributes_count")}=${record.droppedAttributesCount}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let line = parts.join(" ");
|
|
63
|
+
|
|
64
|
+
if (record.error) line += `\n${formatError(record.error, paint)}`;
|
|
65
|
+
|
|
66
|
+
return line;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `HH:MM:ss.SSS`, local timezone. Millisecond resolution: the sub-millisecond
|
|
70
|
+
* digits of the record's nanosecond timestamp are noise on a console line. */
|
|
71
|
+
function formatLocalTime(timestamp: bigint): string {
|
|
72
|
+
const date = new Date(Number(timestamp / 1_000_000n));
|
|
73
|
+
const hh = String(date.getHours()).padStart(2, "0");
|
|
74
|
+
const mm = String(date.getMinutes()).padStart(2, "0");
|
|
75
|
+
const ss = String(date.getSeconds()).padStart(2, "0");
|
|
76
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
77
|
+
return `${hh}:${mm}:${ss}.${ms}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatAttributeValue(value: AnyValue): string {
|
|
81
|
+
if (value === null) return "null";
|
|
82
|
+
if (value instanceof Uint8Array) return `<${value.byteLength} bytes>`;
|
|
83
|
+
if (typeof value === "object") return quoteIfNeeded(JSON.stringify(value, jsonSafe));
|
|
84
|
+
return quoteIfNeeded(String(value));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Values containing whitespace, `"`, or `=` must be quoted so the `key=value`
|
|
88
|
+
* stream stays unambiguously parseable by eye. */
|
|
89
|
+
function quoteIfNeeded(text: string): string {
|
|
90
|
+
return /[\s"=]/.test(text) ? JSON.stringify(text) : text;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function jsonSafe(_key: string, value: unknown): unknown {
|
|
94
|
+
if (typeof value === "bigint") return value.toString();
|
|
95
|
+
if (value instanceof Uint8Array) return `<${value.byteLength} bytes>`;
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Errors render after the message; the stack goes on following lines,
|
|
100
|
+
* indented, unmodified. The `cause` chain follows, already bounded by §6.3. */
|
|
101
|
+
function formatError(error: ErrorValue, paint: (code: string, text: string) => string): string {
|
|
102
|
+
const lines: string[] = [` ${paint(LEVEL_COLOR[17]!, error.type)}: ${error.message}`];
|
|
103
|
+
if (error.stack) {
|
|
104
|
+
for (const line of error.stack.split("\n")) lines.push(` ${line}`);
|
|
105
|
+
}
|
|
106
|
+
let cause = error.cause;
|
|
107
|
+
let guard = 0;
|
|
108
|
+
while (cause && guard < 16) {
|
|
109
|
+
lines.push(` ${paint(DIM, "caused by")} ${cause.type}: ${cause.message}`);
|
|
110
|
+
if (cause.stack) {
|
|
111
|
+
for (const line of cause.stack.split("\n")) lines.push(` ${line}`);
|
|
112
|
+
}
|
|
113
|
+
cause = cause.cause;
|
|
114
|
+
guard += 1;
|
|
115
|
+
}
|
|
116
|
+
return lines.join("\n");
|
|
117
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { closeSync, openSync, writeSync } from "node:fs";
|
|
2
|
+
import type { LogRecord } from "@telorun/sdk";
|
|
3
|
+
import { encodeJsonLine, type BytesEncoder } from "./encode-json.js";
|
|
4
|
+
import { encodePrettyLine } from "./encode-pretty.js";
|
|
5
|
+
import { DEFAULT_BUFFER_POLICY, type LogSinkInstance, type SinkBufferPolicy } from "./log-sink.js";
|
|
6
|
+
import { RecordBuffer } from "./record-buffer.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `Telo.FileSink` — a kernel built-in (§10.2). Asynchronous, `json` by default.
|
|
10
|
+
*
|
|
11
|
+
* Sync-flushable: a positional write is available on every target platform, so a
|
|
12
|
+
* `fatal` record is durable by the time `log()` returns. That is what makes the
|
|
13
|
+
* file sink a legitimate audit destination while an OTLP sink is not — delivery
|
|
14
|
+
* there is a network round-trip that cannot complete without yielding.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type FileEncoding = "json" | "pretty";
|
|
18
|
+
|
|
19
|
+
export interface FileSinkOptions {
|
|
20
|
+
sinkId: string;
|
|
21
|
+
level: number;
|
|
22
|
+
destination: string;
|
|
23
|
+
encoding?: FileEncoding;
|
|
24
|
+
policy?: SinkBufferPolicy;
|
|
25
|
+
onDrop: () => void;
|
|
26
|
+
encodeBytes?: BytesEncoder;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class FileSink implements LogSinkInstance {
|
|
30
|
+
readonly sinkId: string;
|
|
31
|
+
readonly level: number;
|
|
32
|
+
readonly syncFlushable = true;
|
|
33
|
+
|
|
34
|
+
readonly #buffer: RecordBuffer;
|
|
35
|
+
readonly #encode: (record: LogRecord) => string;
|
|
36
|
+
readonly #fd: number;
|
|
37
|
+
#timer: ReturnType<typeof setInterval> | undefined;
|
|
38
|
+
#closed = false;
|
|
39
|
+
|
|
40
|
+
constructor(options: FileSinkOptions) {
|
|
41
|
+
this.sinkId = options.sinkId;
|
|
42
|
+
this.level = options.level;
|
|
43
|
+
const policy = options.policy ?? DEFAULT_BUFFER_POLICY;
|
|
44
|
+
this.#buffer = new RecordBuffer(policy, options.onDrop);
|
|
45
|
+
const encoding = options.encoding ?? "json";
|
|
46
|
+
this.#encode =
|
|
47
|
+
encoding === "pretty"
|
|
48
|
+
? (record) => encodePrettyLine(record, { color: false })
|
|
49
|
+
: (record) => encodeJsonLine(record, { encodeBytes: options.encodeBytes });
|
|
50
|
+
|
|
51
|
+
this.#fd = openSync(options.destination, "a");
|
|
52
|
+
|
|
53
|
+
this.#timer = setInterval(() => this.flushSync(), policy.flushIntervalMs);
|
|
54
|
+
// A pending flush tick must never be the reason a process stays alive; the
|
|
55
|
+
// shutdown flush is what guarantees the tail is written.
|
|
56
|
+
(this.#timer as { unref?: () => void }).unref?.();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
write(record: LogRecord): void {
|
|
60
|
+
if (this.#closed) return;
|
|
61
|
+
this.#buffer.push(record);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async flush(): Promise<void> {
|
|
65
|
+
this.flushSync();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
flushSync(): void {
|
|
69
|
+
if (this.#closed) return;
|
|
70
|
+
const records = this.#buffer.drain();
|
|
71
|
+
if (records.length === 0) return;
|
|
72
|
+
let payload = "";
|
|
73
|
+
for (const record of records) payload += this.#encode(record);
|
|
74
|
+
writeSync(this.#fd, payload);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async close(): Promise<void> {
|
|
78
|
+
if (this.#closed) return;
|
|
79
|
+
this.flushSync();
|
|
80
|
+
this.#closed = true;
|
|
81
|
+
if (this.#timer) {
|
|
82
|
+
clearInterval(this.#timer);
|
|
83
|
+
this.#timer = undefined;
|
|
84
|
+
}
|
|
85
|
+
closeSync(this.#fd);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging — the Node.js implementation of `kernel/specs/logging.md`.
|
|
3
|
+
*
|
|
4
|
+
* The record model, severity scale, and `Logger` surface live in `@telorun/sdk`
|
|
5
|
+
* because module authors consume them; everything here is the runtime half —
|
|
6
|
+
* the pipeline, the encodings, the sinks, and the policies that gate emission.
|
|
7
|
+
* The redaction path grammar lives in `@telorun/analyzer` so `telo check` and
|
|
8
|
+
* the runtime share one parser rather than two that can drift.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { BOOTSTRAP_SINK_ID, createBootstrapWriter } from "./bootstrap-writer.js";
|
|
12
|
+
export { decideColor } from "./color-precedence.js";
|
|
13
|
+
export type { ColorDecisionInput, ColorSetting } from "./color-precedence.js";
|
|
14
|
+
export { ConsoleSink } from "./console-sink.js";
|
|
15
|
+
export { DEBUG_WIRE_SINK_ID, DebugWireSink } from "./debug-wire-sink.js";
|
|
16
|
+
export type { ConsoleDestination, ConsoleEncoding, ConsoleSinkOptions } from "./console-sink.js";
|
|
17
|
+
export { DropRegistry, PIPELINE_SINK_ID } from "./drop-accounting.js";
|
|
18
|
+
export type { DropReport } from "./drop-accounting.js";
|
|
19
|
+
export { base64Bytes, encodeJson, encodeJsonLine, toJsonProfile } from "./encode-json.js";
|
|
20
|
+
export type { BytesEncoder, JsonEncodeOptions } from "./encode-json.js";
|
|
21
|
+
export { encodePretty, encodePrettyLine } from "./encode-pretty.js";
|
|
22
|
+
export type { PrettyEncodeOptions } from "./encode-pretty.js";
|
|
23
|
+
export { FileSink } from "./file-sink.js";
|
|
24
|
+
export type { FileEncoding, FileSinkOptions } from "./file-sink.js";
|
|
25
|
+
export {
|
|
26
|
+
BLOCK_UNSUPPORTED,
|
|
27
|
+
blockUnsupportedMessage,
|
|
28
|
+
DEFAULT_BUFFER_POLICY,
|
|
29
|
+
} from "./log-sink.js";
|
|
30
|
+
export type { DropCause, LogSinkInstance, OnFull, SinkBufferPolicy } from "./log-sink.js";
|
|
31
|
+
export { LoggingPipeline, ROOT_SCOPE_CONFIG } from "./logging-pipeline.js";
|
|
32
|
+
export type { PipelineOptions, ScopeConfig, TraceContextProvider } from "./logging-pipeline.js";
|
|
33
|
+
export {
|
|
34
|
+
DEFAULT_ATTRIBUTE_LIMITS,
|
|
35
|
+
normalizeAttributes,
|
|
36
|
+
} from "./normalize-attributes.js";
|
|
37
|
+
export type { AttributeLimits, NormalizedAttributes, NormalizeOptions } from "./normalize-attributes.js";
|
|
38
|
+
export {
|
|
39
|
+
compileRedactionPolicy,
|
|
40
|
+
DEFAULT_CENSOR,
|
|
41
|
+
EMPTY_REDACTION_POLICY,
|
|
42
|
+
redactAttributes,
|
|
43
|
+
redactError,
|
|
44
|
+
} from "./redact-attributes.js";
|
|
45
|
+
export type { CompiledRedactionPath, RedactionPolicy } from "./redact-attributes.js";
|
|
46
|
+
export { RecordBuffer } from "./record-buffer.js";
|
|
47
|
+
export { Sampler } from "./sampler.js";
|
|
48
|
+
export type { SamplingConfig } from "./sampler.js";
|
|
49
|
+
export {
|
|
50
|
+
formatSpanCounter,
|
|
51
|
+
formatSpanId,
|
|
52
|
+
newTraceId,
|
|
53
|
+
normalizeSpanId,
|
|
54
|
+
normalizeTraceId,
|
|
55
|
+
saltSpanId,
|
|
56
|
+
} from "./span-id.js";
|
|
57
|
+
export { toErrorValue } from "./to-error-value.js";
|
|
58
|
+
export {
|
|
59
|
+
formatTraceParent,
|
|
60
|
+
parseTraceParent,
|
|
61
|
+
parseTraceState,
|
|
62
|
+
TRACE_FLAG_RANDOM,
|
|
63
|
+
TRACE_FLAG_SAMPLED,
|
|
64
|
+
} from "./trace-parent.js";
|
|
65
|
+
export type { TraceContext } from "./trace-parent.js";
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseLevelName,
|
|
3
|
+
SEVERITY,
|
|
4
|
+
type LevelName,
|
|
5
|
+
type LogAttributes,
|
|
6
|
+
type Logger,
|
|
7
|
+
type ResourceRef,
|
|
8
|
+
} from "@telorun/sdk";
|
|
9
|
+
import { BOOTSTRAP_SINK_ID, createBootstrapWriter } from "./bootstrap-writer.js";
|
|
10
|
+
import { ConsoleSink } from "./console-sink.js";
|
|
11
|
+
import { compileRedactionPolicy, EMPTY_REDACTION_POLICY } from "./redact-attributes.js";
|
|
12
|
+
import type { DropCause, LogSinkInstance } from "./log-sink.js";
|
|
13
|
+
import { createLoggingHost, type LoggingHost } from "./logging-host.js";
|
|
14
|
+
import { LoggingPipeline, type ScopeConfig, type TraceContextProvider } from "./logging-pipeline.js";
|
|
15
|
+
import type { SamplingConfig } from "./sampler.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The kernel's ownership of the logging pipeline — `kernel/specs/logging.md`
|
|
19
|
+
* §12.
|
|
20
|
+
*
|
|
21
|
+
* Sequencing is the whole job here. An internal console writer covers process
|
|
22
|
+
* start through manifest validation, because declared sinks cannot instantiate
|
|
23
|
+
* before the block that declares them has been validated. Once the manifest
|
|
24
|
+
* resolves, the declared configuration takes over: eager sinks attach, buffered
|
|
25
|
+
* records replay, and the bootstrap writer steps aside.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** The raw `logging:` block as authored, after CEL evaluation. */
|
|
29
|
+
export interface LoggingManifestBlock {
|
|
30
|
+
level?: string;
|
|
31
|
+
attributes?: LogAttributes;
|
|
32
|
+
redact?: { paths?: string[]; censor?: string; remove?: boolean };
|
|
33
|
+
sampling?: { first?: number; thereafter?: number; tick?: string; sampleErrors?: boolean };
|
|
34
|
+
sinks?: unknown[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DEFAULT_SAMPLING_TICK_MS = 1000;
|
|
38
|
+
|
|
39
|
+
/** The module-context tree as far as logging is concerned — just enough to walk
|
|
40
|
+
* children and read each one's resolved scope config, declared structurally so
|
|
41
|
+
* this file stays independent of the concrete `ModuleContext`. */
|
|
42
|
+
export interface LoggingContextNode {
|
|
43
|
+
readonly children?: readonly LoggingContextNode[];
|
|
44
|
+
getLoggingConfig?(): ScopeConfig | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** How many sinks the root Application declared. Zero means the runtime behaves
|
|
48
|
+
* exactly as if a single `Telo.ConsoleSink` were declared (§12.1), which is why
|
|
49
|
+
* the bootstrap writer is replaced by a threshold-aware one rather than left as
|
|
50
|
+
* the fixed-`info` writer. */
|
|
51
|
+
function declaredSinkCount(manifests: readonly { kind?: string }[]): number {
|
|
52
|
+
const root = manifests.find((m) => m.kind === "Telo.Application") as
|
|
53
|
+
| { logging?: { sinks?: unknown[] } }
|
|
54
|
+
| undefined;
|
|
55
|
+
return root?.logging?.sinks?.length ?? 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class KernelLogging {
|
|
59
|
+
readonly pipeline: LoggingPipeline;
|
|
60
|
+
readonly host: LoggingHost;
|
|
61
|
+
|
|
62
|
+
#rootScope: ScopeConfig;
|
|
63
|
+
#bootstrap: LogSinkInstance | undefined;
|
|
64
|
+
#sealed = false;
|
|
65
|
+
/** Kept so the zero-config console sink can be rebuilt at the resolved
|
|
66
|
+
* threshold once the manifest is known (see {@link sealBootstrap}). */
|
|
67
|
+
readonly #streams: {
|
|
68
|
+
env: Record<string, string | undefined>;
|
|
69
|
+
stdout: NodeJS.WritableStream;
|
|
70
|
+
stderr: NodeJS.WritableStream;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
constructor(options: {
|
|
74
|
+
env: Record<string, string | undefined>;
|
|
75
|
+
stdout: NodeJS.WritableStream;
|
|
76
|
+
stderr: NodeJS.WritableStream;
|
|
77
|
+
/** The process's real stderr — §8.4's fallback diagnostic stream. Kept
|
|
78
|
+
* distinct from the kernel's injectable `stderr` so a captured stream can
|
|
79
|
+
* never swallow the report that the capture itself failed. */
|
|
80
|
+
fallbackStream?: { write(chunk: string): unknown };
|
|
81
|
+
}) {
|
|
82
|
+
this.#streams = { env: options.env, stdout: options.stdout, stderr: options.stderr };
|
|
83
|
+
this.pipeline = new LoggingPipeline({
|
|
84
|
+
fallbackStream: options.fallbackStream ?? {
|
|
85
|
+
write: (chunk: string) => process.stderr.write(chunk),
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// §12.3: a fixed default of `info` until the manifest resolves. This phase
|
|
90
|
+
// is the only one not manifest-governed, and it is deliberately not
|
|
91
|
+
// configurable by any other means.
|
|
92
|
+
this.#rootScope = {
|
|
93
|
+
threshold: SEVERITY.info,
|
|
94
|
+
redaction: EMPTY_REDACTION_POLICY,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
this.#bootstrap = createBootstrapWriter(options);
|
|
98
|
+
this.pipeline.attach(this.#bootstrap);
|
|
99
|
+
|
|
100
|
+
this.host = createLoggingHost(
|
|
101
|
+
this.pipeline,
|
|
102
|
+
() => this.#rootScope.threshold,
|
|
103
|
+
(sinkId: string, cause: DropCause, count?: number) => this.recordDrop(sinkId, cause, count),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get rootScope(): ScopeConfig {
|
|
108
|
+
return this.#rootScope;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
setTraceContextProvider(provider: TraceContextProvider | undefined): void {
|
|
112
|
+
this.pipeline.setTraceContextProvider(provider);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
recordDrop(sinkId: string, cause: DropCause, count = 1): void {
|
|
116
|
+
// Routed through the pipeline so the recovery warning §10.4 requires is
|
|
117
|
+
// emitted as an ordinary record and reaches every sink.
|
|
118
|
+
this.pipeline.recordDrop(sinkId, cause, count);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Adopt the manifest's declared configuration. Called as soon as the
|
|
123
|
+
* `logging:` block has been validated — after that point the bootstrap default
|
|
124
|
+
* is no longer in force.
|
|
125
|
+
*/
|
|
126
|
+
applyRootConfig(block: LoggingManifestBlock | undefined, secretValues?: ReadonlySet<string>): void {
|
|
127
|
+
this.#rootScope = buildScopeConfig(block, {
|
|
128
|
+
threshold: SEVERITY.info,
|
|
129
|
+
redaction: EMPTY_REDACTION_POLICY,
|
|
130
|
+
secretValues,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Detach the bootstrap writer and stop holding records for replay. Called once
|
|
136
|
+
* every declared sink has attached: the buffer exists to cover the pre-attach
|
|
137
|
+
* window, and a consumer connecting later (the debug wire) wants the live
|
|
138
|
+
* stream rather than the whole process history.
|
|
139
|
+
*
|
|
140
|
+
* When the manifest declares no sinks at all, the runtime behaves exactly as
|
|
141
|
+
* if a single `Telo.ConsoleSink` were declared — "pretty logs on stderr in a
|
|
142
|
+
* terminal, JSON when piped", with no imports.
|
|
143
|
+
*
|
|
144
|
+
* That equivalence is why the bootstrap writer is *replaced* here rather than
|
|
145
|
+
* promoted in place. The bootstrap writer is pinned at `info` (§12.3), but a
|
|
146
|
+
* declared `Telo.ConsoleSink` with no explicit `level:` takes the resolved
|
|
147
|
+
* scope threshold — so promoting the fixed-`info` writer would silently ignore
|
|
148
|
+
* a root `logging.level`, in both directions (a `debug` never lowers the gate,
|
|
149
|
+
* a `warn` never raises it). The fresh sink is built at the resolved threshold,
|
|
150
|
+
* so `logging: { level: debug }` with no `sinks:` behaves as documented.
|
|
151
|
+
*/
|
|
152
|
+
sealBootstrap(manifests: readonly { kind?: string }[]): void {
|
|
153
|
+
if (this.#sealed) return;
|
|
154
|
+
this.#sealed = true;
|
|
155
|
+
|
|
156
|
+
if (this.#bootstrap) {
|
|
157
|
+
this.pipeline.detach(this.#bootstrap);
|
|
158
|
+
this.#bootstrap = undefined;
|
|
159
|
+
}
|
|
160
|
+
// Drop the replay buffer *before* attaching the zero-config sink, so the
|
|
161
|
+
// records the bootstrap writer already wrote live are not replayed into it.
|
|
162
|
+
this.pipeline.sealBootstrap();
|
|
163
|
+
|
|
164
|
+
if (declaredSinkCount(manifests) === 0) {
|
|
165
|
+
this.pipeline.attach(
|
|
166
|
+
new ConsoleSink({
|
|
167
|
+
sinkId: BOOTSTRAP_SINK_ID,
|
|
168
|
+
level: this.#rootScope.threshold,
|
|
169
|
+
destination: "stderr",
|
|
170
|
+
encoding: "auto",
|
|
171
|
+
color: "auto",
|
|
172
|
+
env: this.#streams.env,
|
|
173
|
+
stdout: this.#streams.stdout,
|
|
174
|
+
stderr: this.#streams.stderr,
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A logger scoped to a module context, stamped with a resource identity. */
|
|
181
|
+
createLogger(scope: ScopeConfig | undefined, resource?: ResourceRef): Logger {
|
|
182
|
+
return this.pipeline.createLogger(scope ?? this.#rootScope, resource);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Every module context's resolved logging configuration, keyed by its dotted
|
|
187
|
+
* import-alias path (`Api.Domain.Db`); the root is keyed `""`.
|
|
188
|
+
*
|
|
189
|
+
* This closes §12.2's loop between reading and configuring: the `scope` on a
|
|
190
|
+
* log line is exactly the path you write in the manifest to change that
|
|
191
|
+
* instance's level, and this answers "what level did that path actually
|
|
192
|
+
* resolve to" without re-deriving the cascade by hand. The tree walk is
|
|
193
|
+
* logging logic, so it lives here rather than on the orchestrator.
|
|
194
|
+
*/
|
|
195
|
+
scopesFrom(rootContext: LoggingContextNode | undefined): Map<string, ScopeConfig> {
|
|
196
|
+
const scopes = new Map<string, ScopeConfig>();
|
|
197
|
+
scopes.set("", this.#rootScope);
|
|
198
|
+
const visit = (context: LoggingContextNode): void => {
|
|
199
|
+
for (const child of context.children ?? []) {
|
|
200
|
+
const config = child.getLoggingConfig?.();
|
|
201
|
+
if (config?.scope) scopes.set(config.scope, config);
|
|
202
|
+
visit(child);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
if (rootContext) visit(rootContext);
|
|
206
|
+
return scopes;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The kernel's own logger — used for diagnostics the kernel emits about
|
|
210
|
+
* itself, which §13.1 requires to go through this pipeline rather than to
|
|
211
|
+
* `process.stderr` or `console.*`. */
|
|
212
|
+
kernelLogger(): Logger {
|
|
213
|
+
return this.pipeline.createLogger(this.#rootScope, undefined);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async shutdown(): Promise<void> {
|
|
217
|
+
await this.pipeline.close();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Merge a `logging:` block over an inherited configuration. Config cascades and
|
|
223
|
+
* may be narrowed at each hop, which is what makes a dependency you do not own
|
|
224
|
+
* diagnosable — raising `Api`'s level lifts everything beneath it without
|
|
225
|
+
* editing `Api`'s manifest (§12.2).
|
|
226
|
+
*/
|
|
227
|
+
export function buildScopeConfig(
|
|
228
|
+
block: LoggingManifestBlock | undefined,
|
|
229
|
+
inherited: ScopeConfig,
|
|
230
|
+
): ScopeConfig {
|
|
231
|
+
if (!block) return inherited;
|
|
232
|
+
|
|
233
|
+
const level = block.level ? parseLevelName(block.level as LevelName) : undefined;
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
threshold: level ?? inherited.threshold,
|
|
237
|
+
redaction: block.redact
|
|
238
|
+
? compileRedactionPolicy({
|
|
239
|
+
paths: block.redact.paths,
|
|
240
|
+
censor: block.redact.censor,
|
|
241
|
+
remove: block.redact.remove,
|
|
242
|
+
})
|
|
243
|
+
: inherited.redaction,
|
|
244
|
+
sampling: block.sampling ? toSamplingConfig(block.sampling) : inherited.sampling,
|
|
245
|
+
secretValues: inherited.secretValues,
|
|
246
|
+
scope: inherited.scope,
|
|
247
|
+
module: inherited.module,
|
|
248
|
+
attributes: block.attributes ?? inherited.attributes,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function toSamplingConfig(sampling: NonNullable<LoggingManifestBlock["sampling"]>): SamplingConfig {
|
|
253
|
+
return {
|
|
254
|
+
first: sampling.first ?? 0,
|
|
255
|
+
thereafter: sampling.thereafter ?? 0,
|
|
256
|
+
tickMs: sampling.tick ? parseTickMs(sampling.tick) : DEFAULT_SAMPLING_TICK_MS,
|
|
257
|
+
sampleErrors: sampling.sampleErrors ?? false,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function parseTickMs(tick: string): number {
|
|
262
|
+
const match = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h)\s*$/.exec(tick);
|
|
263
|
+
if (!match) return DEFAULT_SAMPLING_TICK_MS;
|
|
264
|
+
const unit = { ms: 1, s: 1000, m: 60_000, h: 3_600_000 }[match[2] as "ms" | "s" | "m" | "h"];
|
|
265
|
+
return Number(match[1]) * unit;
|
|
266
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sink contract now lives in `@telorun/sdk` — §10.2 keeps the sink set open
|
|
3
|
+
* to the ecosystem, so a third-party sink module implements it as an ordinary
|
|
4
|
+
* module author rather than importing a kernel-internal type. Re-exported here
|
|
5
|
+
* so the kernel's own imports keep one spelling.
|
|
6
|
+
*/
|
|
7
|
+
export {
|
|
8
|
+
BLOCK_UNSUPPORTED,
|
|
9
|
+
blockUnsupportedMessage,
|
|
10
|
+
DEFAULT_BUFFER_POLICY,
|
|
11
|
+
} from "@telorun/sdk";
|
|
12
|
+
export type {
|
|
13
|
+
DropCause,
|
|
14
|
+
LoggingHost,
|
|
15
|
+
LogSinkInstance,
|
|
16
|
+
OnFull,
|
|
17
|
+
SinkBufferPolicy,
|
|
18
|
+
} from "@telorun/sdk";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { parseLevelName, type DropCause, type LevelName, type LoggingHost } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
export type { LoggingHost } from "@telorun/sdk";
|
|
4
|
+
import type { LoggingPipeline } from "./logging-pipeline.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The narrow facade a sink controller sees — `kernel/specs/logging.md` §12.1.
|
|
8
|
+
*
|
|
9
|
+
* A sink is a resource, so its controller runs under an ordinary
|
|
10
|
+
* `ResourceContext`; this is the one extra surface it needs. It is deliberately
|
|
11
|
+
* small: attach, detach, resolve a level, count a drop. Everything else about
|
|
12
|
+
* the pipeline stays private to the kernel, so a third-party sink shipped as a
|
|
13
|
+
* module depends on this contract and nothing deeper.
|
|
14
|
+
*/
|
|
15
|
+
export function createLoggingHost(
|
|
16
|
+
pipeline: LoggingPipeline,
|
|
17
|
+
scopeThreshold: () => number,
|
|
18
|
+
recordDrop: (sinkId: string, cause: DropCause, count?: number) => void,
|
|
19
|
+
): LoggingHost {
|
|
20
|
+
return {
|
|
21
|
+
attach: (sink) => pipeline.attach(sink),
|
|
22
|
+
detach: (sink) => pipeline.detach(sink),
|
|
23
|
+
levelFor: (level) => {
|
|
24
|
+
if (!level) return scopeThreshold();
|
|
25
|
+
const severity = parseLevelName(level as LevelName);
|
|
26
|
+
// The schema constrains `level:` to the six named levels, so an unknown
|
|
27
|
+
// value here means the manifest bypassed validation; fall back rather than
|
|
28
|
+
// throw, because a logging misconfiguration must not break boot.
|
|
29
|
+
return severity ?? scopeThreshold();
|
|
30
|
+
},
|
|
31
|
+
recordDrop,
|
|
32
|
+
};
|
|
33
|
+
}
|