@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.
Files changed (194) hide show
  1. package/dist/controller-loader.d.ts +6 -1
  2. package/dist/controller-loader.d.ts.map +1 -1
  3. package/dist/controller-loader.js +2 -0
  4. package/dist/controller-loader.js.map +1 -1
  5. package/dist/controller-loaders/bundle-builder.d.ts +2 -1
  6. package/dist/controller-loaders/bundle-builder.d.ts.map +1 -1
  7. package/dist/controller-loaders/bundle-builder.js +14 -9
  8. package/dist/controller-loaders/bundle-builder.js.map +1 -1
  9. package/dist/controller-loaders/npm-loader.d.ts +8 -2
  10. package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
  11. package/dist/controller-loaders/npm-loader.js +19 -10
  12. package/dist/controller-loaders/npm-loader.js.map +1 -1
  13. package/dist/controllers/logging/console-sink-controller.d.ts +15 -0
  14. package/dist/controllers/logging/console-sink-controller.d.ts.map +1 -0
  15. package/dist/controllers/logging/console-sink-controller.js +40 -0
  16. package/dist/controllers/logging/console-sink-controller.js.map +1 -0
  17. package/dist/controllers/logging/file-sink-controller.d.ts +11 -0
  18. package/dist/controllers/logging/file-sink-controller.d.ts.map +1 -0
  19. package/dist/controllers/logging/file-sink-controller.js +33 -0
  20. package/dist/controllers/logging/file-sink-controller.js.map +1 -0
  21. package/dist/controllers/logging/sink-identity.d.ts +37 -0
  22. package/dist/controllers/logging/sink-identity.d.ts.map +1 -0
  23. package/dist/controllers/logging/sink-identity.js +40 -0
  24. package/dist/controllers/logging/sink-identity.js.map +1 -0
  25. package/dist/controllers/module/import-controller.d.ts.map +1 -1
  26. package/dist/controllers/module/import-controller.js +25 -0
  27. package/dist/controllers/module/import-controller.js.map +1 -1
  28. package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
  29. package/dist/controllers/resource-definition/resource-definition-controller.js +1 -0
  30. package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
  31. package/dist/evaluation-context.d.ts +27 -1
  32. package/dist/evaluation-context.d.ts.map +1 -1
  33. package/dist/evaluation-context.js +84 -16
  34. package/dist/evaluation-context.js.map +1 -1
  35. package/dist/index.d.ts +4 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +5 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/internal-context.d.ts +4 -0
  40. package/dist/internal-context.d.ts.map +1 -1
  41. package/dist/kernel.d.ts +26 -0
  42. package/dist/kernel.d.ts.map +1 -1
  43. package/dist/kernel.js +78 -5
  44. package/dist/kernel.js.map +1 -1
  45. package/dist/logging/bootstrap-writer.d.ts +23 -0
  46. package/dist/logging/bootstrap-writer.d.ts.map +1 -0
  47. package/dist/logging/bootstrap-writer.js +31 -0
  48. package/dist/logging/bootstrap-writer.js.map +1 -0
  49. package/dist/logging/color-precedence.d.ts +27 -0
  50. package/dist/logging/color-precedence.d.ts.map +1 -0
  51. package/dist/logging/color-precedence.js +47 -0
  52. package/dist/logging/color-precedence.js.map +1 -0
  53. package/dist/logging/console-sink.d.ts +45 -0
  54. package/dist/logging/console-sink.d.ts.map +1 -0
  55. package/dist/logging/console-sink.js +56 -0
  56. package/dist/logging/console-sink.js.map +1 -0
  57. package/dist/logging/debug-wire-sink.d.ts +38 -0
  58. package/dist/logging/debug-wire-sink.d.ts.map +1 -0
  59. package/dist/logging/debug-wire-sink.js +85 -0
  60. package/dist/logging/debug-wire-sink.js.map +1 -0
  61. package/dist/logging/drop-accounting.d.ts +34 -0
  62. package/dist/logging/drop-accounting.d.ts.map +1 -0
  63. package/dist/logging/drop-accounting.js +98 -0
  64. package/dist/logging/drop-accounting.js.map +1 -0
  65. package/dist/logging/encode-json.d.ts +28 -0
  66. package/dist/logging/encode-json.d.ts.map +1 -0
  67. package/dist/logging/encode-json.js +86 -0
  68. package/dist/logging/encode-json.js.map +1 -0
  69. package/dist/logging/encode-pretty.d.ts +7 -0
  70. package/dist/logging/encode-pretty.d.ts.map +1 -0
  71. package/dist/logging/encode-pretty.js +104 -0
  72. package/dist/logging/encode-pretty.js.map +1 -0
  73. package/dist/logging/file-sink.d.ts +33 -0
  74. package/dist/logging/file-sink.d.ts.map +1 -0
  75. package/dist/logging/file-sink.js +72 -0
  76. package/dist/logging/file-sink.js.map +1 -0
  77. package/dist/logging/index.d.ts +39 -0
  78. package/dist/logging/index.d.ts.map +1 -0
  79. package/dist/logging/index.js +27 -0
  80. package/dist/logging/index.js.map +1 -0
  81. package/dist/logging/kernel-logging.d.ts +110 -0
  82. package/dist/logging/kernel-logging.d.ts.map +1 -0
  83. package/dist/logging/kernel-logging.js +199 -0
  84. package/dist/logging/kernel-logging.js.map +1 -0
  85. package/dist/logging/log-sink.d.ts +9 -0
  86. package/dist/logging/log-sink.d.ts.map +1 -0
  87. package/dist/logging/log-sink.js +8 -0
  88. package/dist/logging/log-sink.js.map +1 -0
  89. package/dist/logging/logging-host.d.ts +14 -0
  90. package/dist/logging/logging-host.d.ts.map +1 -0
  91. package/dist/logging/logging-host.js +27 -0
  92. package/dist/logging/logging-host.js.map +1 -0
  93. package/dist/logging/logging-pipeline.d.ts +89 -0
  94. package/dist/logging/logging-pipeline.d.ts.map +1 -0
  95. package/dist/logging/logging-pipeline.js +349 -0
  96. package/dist/logging/logging-pipeline.js.map +1 -0
  97. package/dist/logging/normalize-attributes.d.ts +48 -0
  98. package/dist/logging/normalize-attributes.d.ts.map +1 -0
  99. package/dist/logging/normalize-attributes.js +140 -0
  100. package/dist/logging/normalize-attributes.js.map +1 -0
  101. package/dist/logging/record-buffer.d.ts +24 -0
  102. package/dist/logging/record-buffer.d.ts.map +1 -0
  103. package/dist/logging/record-buffer.js +78 -0
  104. package/dist/logging/record-buffer.js.map +1 -0
  105. package/dist/logging/redact-attributes.d.ts +45 -0
  106. package/dist/logging/redact-attributes.d.ts.map +1 -0
  107. package/dist/logging/redact-attributes.js +152 -0
  108. package/dist/logging/redact-attributes.js.map +1 -0
  109. package/dist/logging/sampler.d.ts +34 -0
  110. package/dist/logging/sampler.d.ts.map +1 -0
  111. package/dist/logging/sampler.js +52 -0
  112. package/dist/logging/sampler.js.map +1 -0
  113. package/dist/logging/scope-config.d.ts +32 -0
  114. package/dist/logging/scope-config.d.ts.map +1 -0
  115. package/dist/logging/scope-config.js +2 -0
  116. package/dist/logging/scope-config.js.map +1 -0
  117. package/dist/logging/span-id.d.ts +24 -0
  118. package/dist/logging/span-id.d.ts.map +1 -0
  119. package/dist/logging/span-id.js +85 -0
  120. package/dist/logging/span-id.js.map +1 -0
  121. package/dist/logging/to-error-value.d.ts +3 -0
  122. package/dist/logging/to-error-value.d.ts.map +1 -0
  123. package/dist/logging/to-error-value.js +79 -0
  124. package/dist/logging/to-error-value.js.map +1 -0
  125. package/dist/logging/trace-parent.d.ts +54 -0
  126. package/dist/logging/trace-parent.d.ts.map +1 -0
  127. package/dist/logging/trace-parent.js +92 -0
  128. package/dist/logging/trace-parent.js.map +1 -0
  129. package/dist/manifest-schemas.d.ts +1 -1
  130. package/dist/manifest-schemas.d.ts.map +1 -1
  131. package/dist/manifest-schemas.js +12 -0
  132. package/dist/manifest-schemas.js.map +1 -1
  133. package/dist/module-context.d.ts +16 -0
  134. package/dist/module-context.d.ts.map +1 -1
  135. package/dist/module-context.js +6 -0
  136. package/dist/module-context.js.map +1 -1
  137. package/dist/resource-context.d.ts +15 -1
  138. package/dist/resource-context.d.ts.map +1 -1
  139. package/dist/resource-context.js +45 -0
  140. package/dist/resource-context.js.map +1 -1
  141. package/dist/schema-compiled-values.d.ts.map +1 -1
  142. package/dist/schema-compiled-values.js +7 -0
  143. package/dist/schema-compiled-values.js.map +1 -1
  144. package/dist/schema-validator.d.ts +8 -1
  145. package/dist/schema-validator.d.ts.map +1 -1
  146. package/dist/schema-validator.js +13 -3
  147. package/dist/schema-validator.js.map +1 -1
  148. package/dist/transports/oci/oci-client.d.ts.map +1 -1
  149. package/dist/transports/oci/oci-client.js +13 -3
  150. package/dist/transports/oci/oci-client.js.map +1 -1
  151. package/dist/transports/registry-transport.d.ts.map +1 -1
  152. package/dist/transports/registry-transport.js +15 -5
  153. package/dist/transports/registry-transport.js.map +1 -1
  154. package/package.json +3 -3
  155. package/src/controller-loader.ts +7 -1
  156. package/src/controller-loaders/bundle-builder.ts +18 -10
  157. package/src/controller-loaders/npm-loader.ts +32 -11
  158. package/src/controllers/logging/console-sink-controller.ts +46 -0
  159. package/src/controllers/logging/file-sink-controller.ts +40 -0
  160. package/src/controllers/logging/sink-identity.ts +53 -0
  161. package/src/controllers/module/import-controller.ts +27 -0
  162. package/src/controllers/resource-definition/resource-definition-controller.ts +1 -0
  163. package/src/evaluation-context.ts +93 -15
  164. package/src/index.ts +54 -0
  165. package/src/internal-context.ts +4 -0
  166. package/src/kernel.ts +98 -8
  167. package/src/logging/bootstrap-writer.ts +38 -0
  168. package/src/logging/color-precedence.ts +62 -0
  169. package/src/logging/console-sink.ts +86 -0
  170. package/src/logging/debug-wire-sink.ts +85 -0
  171. package/src/logging/drop-accounting.ts +108 -0
  172. package/src/logging/encode-json.ts +98 -0
  173. package/src/logging/encode-pretty.ts +117 -0
  174. package/src/logging/file-sink.ts +87 -0
  175. package/src/logging/index.ts +65 -0
  176. package/src/logging/kernel-logging.ts +266 -0
  177. package/src/logging/log-sink.ts +18 -0
  178. package/src/logging/logging-host.ts +33 -0
  179. package/src/logging/logging-pipeline.ts +465 -0
  180. package/src/logging/normalize-attributes.ts +228 -0
  181. package/src/logging/record-buffer.ts +72 -0
  182. package/src/logging/redact-attributes.ts +168 -0
  183. package/src/logging/sampler.ts +76 -0
  184. package/src/logging/scope-config.ts +32 -0
  185. package/src/logging/span-id.ts +84 -0
  186. package/src/logging/to-error-value.ts +89 -0
  187. package/src/logging/trace-parent.ts +110 -0
  188. package/src/manifest-schemas.ts +12 -0
  189. package/src/module-context.ts +23 -0
  190. package/src/resource-context.ts +48 -0
  191. package/src/schema-compiled-values.ts +6 -0
  192. package/src/schema-validator.ts +20 -5
  193. package/src/transports/oci/oci-client.ts +13 -3
  194. 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
+ }