@telorun/kernel 0.49.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/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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { parseDurationMs } from "@telorun/sdk";
|
|
2
|
+
import { blockUnsupportedMessage, DEFAULT_BUFFER_POLICY, type SinkBufferPolicy } from "../../logging/log-sink.js";
|
|
3
|
+
import { RuntimeError } from "@telorun/sdk";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared sink-manifest reading — `kernel/specs/logging.md` §10.3, §12.1.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Sink identity for drop accounting (§10.4): the resource name for a `!ref`,
|
|
10
|
+
* or the synthetic name inline extraction assigned, which already encodes kind
|
|
11
|
+
* plus position. */
|
|
12
|
+
export function sinkIdFor(resource: { metadata?: { name?: string }; kind?: string }): string {
|
|
13
|
+
return resource.metadata?.name ?? resource.kind ?? "<sink>";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read the buffering policy an async sink inherits from `Telo.LogSink`.
|
|
18
|
+
*
|
|
19
|
+
* `on_full: block` is rejected here rather than silently degraded. Blocking
|
|
20
|
+
* means suspending the producer until the buffer drains, which requires the
|
|
21
|
+
* drain to progress *while* the producer is suspended — true on a runtime with
|
|
22
|
+
* real threads, false on a single-threaded event loop, where suspending the
|
|
23
|
+
* producer suspends the consumer too. There, `block` is a deadlock.
|
|
24
|
+
*
|
|
25
|
+
* Degrading to `drop_new` would be worse than erroring: someone who writes
|
|
26
|
+
* `block` on an audit sink is saying "I would rather go slow than lose a
|
|
27
|
+
* record", and silently handing back the opposite guarantee means they discover
|
|
28
|
+
* it from a gap in an audit trail rather than from a diagnostic.
|
|
29
|
+
*/
|
|
30
|
+
export function bufferPolicyFor(resource: {
|
|
31
|
+
metadata?: { name?: string };
|
|
32
|
+
kind?: string;
|
|
33
|
+
buffer?: number;
|
|
34
|
+
on_full?: string;
|
|
35
|
+
flush_interval?: string;
|
|
36
|
+
}): SinkBufferPolicy {
|
|
37
|
+
const onFull = resource.on_full ?? DEFAULT_BUFFER_POLICY.onFull;
|
|
38
|
+
|
|
39
|
+
if (onFull === "block") {
|
|
40
|
+
throw new RuntimeError(
|
|
41
|
+
"ERR_LOG_SINK_ON_FULL_UNSUPPORTED",
|
|
42
|
+
blockUnsupportedMessage(sinkIdFor(resource)),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
buffer: resource.buffer ?? DEFAULT_BUFFER_POLICY.buffer,
|
|
48
|
+
onFull: onFull as SinkBufferPolicy["onFull"],
|
|
49
|
+
flushIntervalMs: resource.flush_interval
|
|
50
|
+
? parseDurationMs(resource.flush_interval, DEFAULT_BUFFER_POLICY.flushIntervalMs)
|
|
51
|
+
: DEFAULT_BUFFER_POLICY.flushIntervalMs,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -2,6 +2,7 @@ import { AnalysisRegistry, DiagnosticSeverity, foldIntegrity, parseExportEntry,
|
|
|
2
2
|
import type { ResourceInstance } from "@telorun/sdk";
|
|
3
3
|
import { RuntimeError } from "@telorun/sdk";
|
|
4
4
|
import type { BuiltinControllerContext } from "../../internal-context.js";
|
|
5
|
+
import { buildScopeConfig, type LoggingManifestBlock } from "../../logging/kernel-logging.js";
|
|
5
6
|
import { ModuleContext } from "../../module-context.js";
|
|
6
7
|
import { isDefaultPolicy, normalizeRuntime } from "../../runtime-registry.js";
|
|
7
8
|
|
|
@@ -171,6 +172,32 @@ export async function create(
|
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
// Resolve this import's effective logging configuration once, here, and stamp
|
|
176
|
+
// it as a plain value on the child context — the same shape as the controller
|
|
177
|
+
// policy above.
|
|
178
|
+
//
|
|
179
|
+
// The threshold attaches to the *import* rather than to a map keyed by module
|
|
180
|
+
// name because module names collide (`std/sql` and `acme/sql` share
|
|
181
|
+
// `metadata.name: sql`; the same module imported twice is two subsystems with
|
|
182
|
+
// one name), while an alias is already uniqueness-enforced as a hard
|
|
183
|
+
// DUPLICATE_IMPORT_ALIAS diagnostic. Resolving here means a leaf reads its
|
|
184
|
+
// threshold as an O(1) lookup with no walk up the import chain at emit time —
|
|
185
|
+
// and it is the same scalar a guest runtime caches across an FFI boundary, so
|
|
186
|
+
// scoping and the FFI threshold cache are one mechanism. See §9 and §12.2.
|
|
187
|
+
const parentScope =
|
|
188
|
+
(ctx.moduleContext as unknown as ModuleContext).getLoggingConfig?.() ??
|
|
189
|
+
ctx.kernelLoggingRootScope();
|
|
190
|
+
const scopePath = parentScope.scope ? `${parentScope.scope}.${alias}` : alias;
|
|
191
|
+
const importLogging = resource.logging
|
|
192
|
+
? (ctx.expandValue(resource.logging, {}) as LoggingManifestBlock)
|
|
193
|
+
: undefined;
|
|
194
|
+
(child as ModuleContext).setLoggingConfig({
|
|
195
|
+
...buildScopeConfig(importLogging, parentScope),
|
|
196
|
+
scope: scopePath,
|
|
197
|
+
module: targetModule,
|
|
198
|
+
secretValues: child.secretValues,
|
|
199
|
+
});
|
|
200
|
+
|
|
174
201
|
for (const manifest of manifests) {
|
|
175
202
|
child.registerManifest(manifest);
|
|
176
203
|
}
|
|
@@ -142,6 +142,7 @@ class ResourceDefinition implements ResourceInstance {
|
|
|
142
142
|
const loader = new ControllerLoader({
|
|
143
143
|
entryUrl: ctx.getEntryUrl(),
|
|
144
144
|
installRoot: ctx.getInstallRoot(),
|
|
145
|
+
log: ctx.log,
|
|
145
146
|
});
|
|
146
147
|
// Eager resolve — verify the controller is hostable now (so a broken
|
|
147
148
|
// `controllers:` candidate fails fast at boot), but defer the expensive
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { formatSpanCounter } from "./logging/span-id.js";
|
|
2
3
|
import {
|
|
3
4
|
getRefIdentity,
|
|
4
5
|
isCompiledValue,
|
|
@@ -164,6 +165,19 @@ export function buildResolvedProperties(
|
|
|
164
165
|
*/
|
|
165
166
|
const cancellationStore = new AsyncLocalStorage<InvokeContext>();
|
|
166
167
|
|
|
168
|
+
/**
|
|
169
|
+
* The ambient dispatch context, for §7.2's automatic trace correlation: a record
|
|
170
|
+
* emitted inside an active span carries that span's ids without the controller
|
|
171
|
+
* passing anything.
|
|
172
|
+
*
|
|
173
|
+
* `AsyncLocalStorage.run()` is used throughout rather than `enterWith()`, which
|
|
174
|
+
* transitions for the remainder of the entire synchronous execution and leaks
|
|
175
|
+
* context into subsequent event handlers on the same tick (§9.1).
|
|
176
|
+
*/
|
|
177
|
+
export function ambientInvokeContext(): InvokeContext | undefined {
|
|
178
|
+
return cancellationStore.getStore();
|
|
179
|
+
}
|
|
180
|
+
|
|
167
181
|
type Walker = (ctx: Record<string, unknown>) => unknown;
|
|
168
182
|
|
|
169
183
|
/** Compile a manifest subtree into a tightly-bound walker closure. The returned
|
|
@@ -687,25 +701,83 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
687
701
|
* 2. Tear down own resource instances in reverse registration order,
|
|
688
702
|
* emitting a Teardown event for each via the injected emit callback.
|
|
689
703
|
*/
|
|
704
|
+
// eslint-disable-next-line @typescript-eslint/member-ordering
|
|
690
705
|
async teardownResources(): Promise<void> {
|
|
691
706
|
this.state = "Draining";
|
|
707
|
+
const failures: Array<{ resource: string; error: unknown }> = [];
|
|
708
|
+
|
|
692
709
|
for (const child of [...this.children].reverse()) {
|
|
693
|
-
|
|
710
|
+
try {
|
|
711
|
+
await child.teardownResources();
|
|
712
|
+
} catch (err) {
|
|
713
|
+
failures.push({ resource: "(child context)", error: err });
|
|
714
|
+
}
|
|
694
715
|
}
|
|
695
|
-
|
|
696
|
-
for (const [key, { resource, instance }] of
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
716
|
+
|
|
717
|
+
for (const [key, { resource, instance }] of this.teardownOrder()) {
|
|
718
|
+
const label = `${resource.kind}.${resource.metadata.name}`;
|
|
719
|
+
try {
|
|
720
|
+
if (instance.teardown) await instance.teardown();
|
|
721
|
+
} catch (err) {
|
|
722
|
+
// Aggregate rather than abort. A single throwing resource used to
|
|
723
|
+
// abandon every resource after it in the cascade — including the log
|
|
724
|
+
// sinks, which are pinned last precisely so they outlive everything that
|
|
725
|
+
// might log while shutting down. Losing the shutdown flush to an
|
|
726
|
+
// unrelated failure is exactly the silent loss §10.5 forbids.
|
|
727
|
+
failures.push({ resource: label, error: err });
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
await this.emit(`${label}.Teardown`, {
|
|
731
|
+
resource: {
|
|
732
|
+
kind: resource.kind,
|
|
733
|
+
name: resource.metadata.name,
|
|
734
|
+
id: this.resourceId(resource.kind, resource.metadata.name),
|
|
735
|
+
},
|
|
736
|
+
...(this.owner ? { owner: this.owner } : {}),
|
|
737
|
+
});
|
|
738
|
+
} catch (err) {
|
|
739
|
+
failures.push({ resource: `${label} (Teardown event)`, error: err });
|
|
740
|
+
}
|
|
706
741
|
this.resourceInstances.delete(key);
|
|
707
742
|
}
|
|
743
|
+
|
|
708
744
|
this.state = "Teardown";
|
|
745
|
+
|
|
746
|
+
if (failures.length > 0) {
|
|
747
|
+
throw new RuntimeError(
|
|
748
|
+
"ERR_TEARDOWN_FAILED",
|
|
749
|
+
`${failures.length} resource(s) failed during teardown`,
|
|
750
|
+
failures.map(({ resource, error }) => ({
|
|
751
|
+
severity: "error" as const,
|
|
752
|
+
message: error instanceof Error ? error.message : String(error),
|
|
753
|
+
resource,
|
|
754
|
+
})),
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Resource instances in teardown order: ascending `teardownPriority`, with the
|
|
761
|
+
* base reverse-insertion order preserved within each priority tier.
|
|
762
|
+
*
|
|
763
|
+
* The base order is reverse *insertion*, which is reverse init order in the
|
|
764
|
+
* happy path — but the init loop is a multi-pass retry, so a resource that
|
|
765
|
+
* failed its first pass lands later in the map than its topological rank
|
|
766
|
+
* implies. That makes the dependency graph an unreliable way to say "last".
|
|
767
|
+
* A resource that must reliably outlive the rest declares it directly via
|
|
768
|
+
* `teardownPriority` (log sinks set `TEARDOWN_LAST`), so the generic teardown
|
|
769
|
+
* path orders by a declared number rather than sniffing any one subsystem's
|
|
770
|
+
* instance shape.
|
|
771
|
+
*/
|
|
772
|
+
private teardownOrder(): Array<[string, { resource: any; instance: any }]> {
|
|
773
|
+
const entries = [...this.resourceInstances.entries()].reverse();
|
|
774
|
+
// Stable sort by priority (default 0); Array.prototype.sort is stable, so
|
|
775
|
+
// the reverse-insertion order survives within each tier.
|
|
776
|
+
return entries.sort(
|
|
777
|
+
([, a], [, b]) =>
|
|
778
|
+
((a.instance as { teardownPriority?: number })?.teardownPriority ?? 0) -
|
|
779
|
+
((b.instance as { teardownPriority?: number })?.teardownPriority ?? 0),
|
|
780
|
+
);
|
|
709
781
|
}
|
|
710
782
|
|
|
711
783
|
transientChild(context: Record<string, any>): EvaluationContext {
|
|
@@ -785,7 +857,9 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
785
857
|
* event *name* stays human-meaningful (`<name>.Invoked`) for bus subscribers;
|
|
786
858
|
* everything a debug consumer needs to rebuild the call tree rides here, so the
|
|
787
859
|
* consumer never parses the dotted name. `spanId`/`parentSpanId` are the
|
|
788
|
-
* tracer's `invocationId`/`parentInvocationId
|
|
860
|
+
* tracer's `invocationId`/`parentInvocationId`, rendered as 16 lowercase hex
|
|
861
|
+
* characters at this boundary (present only while tracing) so a span joins its
|
|
862
|
+
* log records by string equality — see kernel/specs/logging.md §7.1;
|
|
789
863
|
* `ref` carries the kind+name the name no longer encodes; `detail` is the
|
|
790
864
|
* per-capability data (inputs/outputs, error fields, cancellation reason).
|
|
791
865
|
*/
|
|
@@ -813,8 +887,12 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
813
887
|
): Record<string, unknown> {
|
|
814
888
|
return {
|
|
815
889
|
traceId,
|
|
816
|
-
|
|
817
|
-
|
|
890
|
+
// The counter stays a cheap integer internally; hex is a fixed-width
|
|
891
|
+
// render performed only here, at the encoding boundary, and only for
|
|
892
|
+
// spans actually being emitted (§7.1 forbids formatting eagerly at span
|
|
893
|
+
// creation). The salt keeps ids unique across processes in one trace.
|
|
894
|
+
spanId: spanId === undefined ? undefined : formatSpanCounter(spanId),
|
|
895
|
+
parentSpanId: parentSpanId === undefined ? undefined : formatSpanCounter(parentSpanId),
|
|
818
896
|
capability,
|
|
819
897
|
phase,
|
|
820
898
|
...(outcome !== undefined ? { outcome } : {}),
|
package/src/index.ts
CHANGED
|
@@ -40,3 +40,57 @@ export { ManifestRegistry as Registry } from "./registry.js";
|
|
|
40
40
|
export { ResourceURI } from "./resource-uri.js";
|
|
41
41
|
export type { RuntimeDiagnostic } from "@telorun/sdk";
|
|
42
42
|
|
|
43
|
+
// Structured logging — the runtime half of kernel/specs/logging.md. The record
|
|
44
|
+
// model, severity scale, and `Logger` surface live in `@telorun/sdk`; these are
|
|
45
|
+
// the pipeline, the encodings, the sinks, and the policies that gate emission.
|
|
46
|
+
export {
|
|
47
|
+
BOOTSTRAP_SINK_ID,
|
|
48
|
+
ConsoleSink,
|
|
49
|
+
DEBUG_WIRE_SINK_ID,
|
|
50
|
+
DebugWireSink,
|
|
51
|
+
DEFAULT_ATTRIBUTE_LIMITS,
|
|
52
|
+
DEFAULT_BUFFER_POLICY,
|
|
53
|
+
DEFAULT_CENSOR,
|
|
54
|
+
DropRegistry,
|
|
55
|
+
FileSink,
|
|
56
|
+
LoggingPipeline,
|
|
57
|
+
PIPELINE_SINK_ID,
|
|
58
|
+
RecordBuffer,
|
|
59
|
+
Sampler,
|
|
60
|
+
base64Bytes,
|
|
61
|
+
blockUnsupportedMessage,
|
|
62
|
+
compileRedactionPolicy,
|
|
63
|
+
createBootstrapWriter,
|
|
64
|
+
decideColor,
|
|
65
|
+
encodeJson,
|
|
66
|
+
encodeJsonLine,
|
|
67
|
+
encodePretty,
|
|
68
|
+
encodePrettyLine,
|
|
69
|
+
formatSpanCounter,
|
|
70
|
+
formatSpanId,
|
|
71
|
+
formatTraceParent,
|
|
72
|
+
normalizeAttributes,
|
|
73
|
+
normalizeSpanId,
|
|
74
|
+
normalizeTraceId,
|
|
75
|
+
parseTraceParent,
|
|
76
|
+
redactAttributes,
|
|
77
|
+
redactError,
|
|
78
|
+
saltSpanId,
|
|
79
|
+
toErrorValue,
|
|
80
|
+
toJsonProfile,
|
|
81
|
+
} from "./logging/index.js";
|
|
82
|
+
export type {
|
|
83
|
+
AttributeLimits,
|
|
84
|
+
ColorSetting,
|
|
85
|
+
DropCause,
|
|
86
|
+
DropReport,
|
|
87
|
+
LogSinkInstance,
|
|
88
|
+
OnFull,
|
|
89
|
+
RedactionPolicy,
|
|
90
|
+
SamplingConfig,
|
|
91
|
+
ScopeConfig,
|
|
92
|
+
SinkBufferPolicy,
|
|
93
|
+
TraceContext,
|
|
94
|
+
} from "./logging/index.js";
|
|
95
|
+
export { KernelLogging } from "./logging/kernel-logging.js";
|
|
96
|
+
export type { LoggingHost } from "./logging/logging-host.js";
|
package/src/internal-context.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResourceContext } from "@telorun/sdk";
|
|
2
|
+
import type { ScopeConfig } from "./logging/scope-config.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Context interface used by built-in kernel controllers (Telo.Application /
|
|
@@ -22,4 +23,7 @@ export interface BuiltinControllerContext extends ResourceContext {
|
|
|
22
23
|
* source-chain `resolveRelative`. Identical to what `loadGraph` used
|
|
23
24
|
* internally — so the produced URL agrees with the loader's caches. */
|
|
24
25
|
resolveImportUrl(fromSource: string, importSource: string): string;
|
|
26
|
+
/** The root Application's resolved logging configuration — the base an
|
|
27
|
+
* import's own `logging:` block cascades over (§12.2). */
|
|
28
|
+
kernelLoggingRootScope(): ScopeConfig;
|
|
25
29
|
}
|
package/src/kernel.ts
CHANGED
|
@@ -33,6 +33,10 @@ import { ControllerRegistry } from "./controller-registry.js";
|
|
|
33
33
|
import { EventBus } from "./events.js";
|
|
34
34
|
import { hostEnv, lockControllerEnv } from "./host-env.js";
|
|
35
35
|
import { KernelTracer } from "./tracing.js";
|
|
36
|
+
import { KernelLogging, type LoggingManifestBlock } from "./logging/kernel-logging.js";
|
|
37
|
+
import type { ScopeConfig } from "./logging/scope-config.js";
|
|
38
|
+
import { formatSpanCounter } from "./logging/span-id.js";
|
|
39
|
+
import { ambientInvokeContext } from "./evaluation-context.js";
|
|
36
40
|
import { ModuleContext } from "./module-context.js";
|
|
37
41
|
import { ResourceContextImpl } from "./resource-context.js";
|
|
38
42
|
import { nodeCelHandlers } from "./cel-handlers.js";
|
|
@@ -148,12 +152,36 @@ export class Kernel implements IKernel {
|
|
|
148
152
|
readonly env: Record<string, string | undefined>;
|
|
149
153
|
readonly argv: string[];
|
|
150
154
|
readonly registryUrl: string | undefined;
|
|
155
|
+
/** Structured logging for this kernel — the pipeline, its sinks, and the
|
|
156
|
+
* scoped loggers handed to controllers as `ctx.log`. Live from construction
|
|
157
|
+
* so loader and parse diagnostics have somewhere to go (§12.3); a nested
|
|
158
|
+
* kernel inherits the parent's sink configuration through the injected
|
|
159
|
+
* streams, so a test harness can capture child output (§13.1). */
|
|
160
|
+
readonly logging: KernelLogging;
|
|
151
161
|
|
|
152
162
|
constructor(options: KernelOptions) {
|
|
153
163
|
this.stdin = options.stdin ?? process.stdin;
|
|
154
164
|
this.stdout = options.stdout ?? process.stdout;
|
|
155
165
|
this.stderr = options.stderr ?? process.stderr;
|
|
156
166
|
this.env = options.env ?? hostEnv();
|
|
167
|
+
this.logging = new KernelLogging({
|
|
168
|
+
env: this.env,
|
|
169
|
+
stdout: this.stdout,
|
|
170
|
+
stderr: this.stderr,
|
|
171
|
+
});
|
|
172
|
+
// The validator is constructed outside the kernel's stdio scope, so it is
|
|
173
|
+
// handed the kernel logger explicitly rather than writing to process.stderr.
|
|
174
|
+
this.sharedSchemaValidator.setLogger(this.logging.kernelLogger());
|
|
175
|
+
// §7.2: a record emitted inside an active dispatch span carries that span's
|
|
176
|
+
// ids automatically — a controller never passes them. The ids come from the
|
|
177
|
+
// same counter the trace wire uses, rendered here at the encoding boundary.
|
|
178
|
+
this.logging.setTraceContextProvider(() => {
|
|
179
|
+
const ambient = ambientInvokeContext();
|
|
180
|
+
if (!ambient?.traceId || ambient.invocationId === undefined) return undefined;
|
|
181
|
+
const spanId = formatSpanCounter(ambient.invocationId);
|
|
182
|
+
if (!spanId) return undefined;
|
|
183
|
+
return { traceId: ambient.traceId, spanId };
|
|
184
|
+
});
|
|
157
185
|
this.argv = options.argv ?? [];
|
|
158
186
|
this.registryUrl = options.registryUrl;
|
|
159
187
|
// Resolution sources come from the transport registry, so a scheme-owning
|
|
@@ -297,6 +325,18 @@ export class Kernel implements IKernel {
|
|
|
297
325
|
"Telo.Import",
|
|
298
326
|
await import("./controllers/module/import-controller.js"),
|
|
299
327
|
);
|
|
328
|
+
// The mandatory sinks live in the kernel rather than in a standard-library
|
|
329
|
+
// module: §16 requires every conforming runtime to implement both, so
|
|
330
|
+
// shipping them as an installable module would make conformance depend on
|
|
331
|
+
// whether that module happened to be installed.
|
|
332
|
+
this.controllers.registerController(
|
|
333
|
+
"Telo.ConsoleSink",
|
|
334
|
+
await import("./controllers/logging/console-sink-controller.js"),
|
|
335
|
+
);
|
|
336
|
+
this.controllers.registerController(
|
|
337
|
+
"Telo.FileSink",
|
|
338
|
+
await import("./controllers/logging/file-sink-controller.js"),
|
|
339
|
+
);
|
|
300
340
|
}
|
|
301
341
|
|
|
302
342
|
/**
|
|
@@ -398,7 +438,9 @@ export class Kernel implements IKernel {
|
|
|
398
438
|
);
|
|
399
439
|
}
|
|
400
440
|
for (const d of analysisGraph.versionDiagnostics) {
|
|
401
|
-
if (d.code === "MODULE_VERSION_HOISTED")
|
|
441
|
+
if (d.code === "MODULE_VERSION_HOISTED") {
|
|
442
|
+
this.logging.kernelLogger().warn(d.message, { "telo.diagnostic.code": d.code });
|
|
443
|
+
}
|
|
402
444
|
}
|
|
403
445
|
const staticManifests = flattenForAnalyzer(analysisGraph);
|
|
404
446
|
this.staticManifests = staticManifests;
|
|
@@ -461,9 +503,7 @@ export class Kernel implements IKernel {
|
|
|
461
503
|
try {
|
|
462
504
|
await writeAnalysisStamp("", analysisSignature, manifestsDir);
|
|
463
505
|
} catch (err) {
|
|
464
|
-
this.
|
|
465
|
-
`[telo:kernel] analysis stamp write failed: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
466
|
-
);
|
|
506
|
+
this.logging.kernelLogger().warn("analysis stamp write failed", undefined, { error: err });
|
|
467
507
|
}
|
|
468
508
|
}
|
|
469
509
|
|
|
@@ -601,9 +641,34 @@ export class Kernel implements IKernel {
|
|
|
601
641
|
port,
|
|
602
642
|
protocol: portDecls[name]?.protocol === "udp" ? "udp" : "tcp",
|
|
603
643
|
}));
|
|
644
|
+
|
|
645
|
+
this.applyLoggingConfig(rootApplicationManifest as Record<string, any>);
|
|
604
646
|
}
|
|
605
647
|
}
|
|
606
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Adopt the manifest's `logging:` block — §12.3's handover from the bootstrap
|
|
651
|
+
* default to the declared configuration.
|
|
652
|
+
*
|
|
653
|
+
* Deliberately runs *after* `variables` / `secrets` resolve, because the only
|
|
654
|
+
* sanctioned way to derive a level from the host environment is a `variables:`
|
|
655
|
+
* entry read with `!cel`: there is no `TELO_LOG_LEVEL`, so the expression must
|
|
656
|
+
* have something to resolve against by the time it is evaluated.
|
|
657
|
+
*
|
|
658
|
+
* Manifest secrets are handed to the pipeline here so they redact with no
|
|
659
|
+
* configuration at all (§14) — the same set, cascading down the same import
|
|
660
|
+
* graph, that supplies the scope threshold.
|
|
661
|
+
*/
|
|
662
|
+
private applyLoggingConfig(rootApplicationManifest: Record<string, any>): void {
|
|
663
|
+
const raw = rootApplicationManifest["logging"];
|
|
664
|
+
if (raw === undefined) {
|
|
665
|
+
this.logging.applyRootConfig(undefined, this.rootContext.secretValues);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const expanded = this.rootContext.expandWith(raw, {}) as LoggingManifestBlock;
|
|
669
|
+
this.logging.applyRootConfig(expanded, this.rootContext.secretValues);
|
|
670
|
+
}
|
|
671
|
+
|
|
607
672
|
/**
|
|
608
673
|
* Resolved inbound ports from the root Application's `ports:` block, available
|
|
609
674
|
* after {@link load}. Each carries the resolved integer and its declared
|
|
@@ -642,10 +707,14 @@ export class Kernel implements IKernel {
|
|
|
642
707
|
// return earlier — so analysis/editor are unaffected. The denied set is
|
|
643
708
|
// process-global and additive across in-process kernels.
|
|
644
709
|
lockControllerEnv(this._declaredEnvKeys, (key) => {
|
|
645
|
-
this.
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
710
|
+
this.logging
|
|
711
|
+
.kernelLogger()
|
|
712
|
+
.warn(
|
|
713
|
+
`controller read process.env.${key} directly — ${key} is a declared binding; ` +
|
|
714
|
+
`read it through ctx.env or its variable/secret, not raw process.env.`,
|
|
715
|
+
{ "telo.env.key": key },
|
|
716
|
+
{ eventName: "telo.env.declared_binding_bypassed" },
|
|
717
|
+
);
|
|
649
718
|
});
|
|
650
719
|
|
|
651
720
|
// Call register hooks for controllers actually loaded at this point (built-ins).
|
|
@@ -689,11 +758,26 @@ export class Kernel implements IKernel {
|
|
|
689
758
|
}
|
|
690
759
|
|
|
691
760
|
await this.rootContext.initializeResources();
|
|
761
|
+
|
|
762
|
+
// Every declared sink has now attached, so the bootstrap buffer has done its
|
|
763
|
+
// job. A consumer connecting later — the debug wire — wants the live stream,
|
|
764
|
+
// not the whole process history, so replay stops here rather than persisting
|
|
765
|
+
// for the process lifetime. The sink-counting and the tree walk are logging
|
|
766
|
+
// logic, so they live on KernelLogging; the kernel just hands it the graph.
|
|
767
|
+
this.logging.sealBootstrap(this.staticManifests);
|
|
768
|
+
|
|
692
769
|
await this.eventBus.emit("Kernel.Initialized", {});
|
|
693
770
|
|
|
694
771
|
this._isBooted = true;
|
|
695
772
|
}
|
|
696
773
|
|
|
774
|
+
/** Every module context's resolved logging configuration, keyed by its dotted
|
|
775
|
+
* import-alias path. Delegates to {@link KernelLogging.scopesFrom}; see there
|
|
776
|
+
* for what §12.2 uses it for. */
|
|
777
|
+
loggingScopes(): Map<string, ScopeConfig> {
|
|
778
|
+
return this.logging.scopesFrom(this.rootContext);
|
|
779
|
+
}
|
|
780
|
+
|
|
697
781
|
/**
|
|
698
782
|
* Run the manifest's `targets` (Telo.Service / Telo.Runnable instances).
|
|
699
783
|
* Emits Kernel.Starting before, Kernel.Started after.
|
|
@@ -749,6 +833,12 @@ export class Kernel implements IKernel {
|
|
|
749
833
|
if (this.rootContext) {
|
|
750
834
|
await this.rootContext.teardownResources();
|
|
751
835
|
}
|
|
836
|
+
// Sinks tear down last (they are pinned in the teardown order), so by this
|
|
837
|
+
// point their own teardown has already flushed and closed them. This drains
|
|
838
|
+
// anything emitted during teardown itself and reports outstanding drop
|
|
839
|
+
// accounting, so a run that ends while still dropping does not lose its
|
|
840
|
+
// final count (§10.4, §10.5).
|
|
841
|
+
await this.logging.shutdown();
|
|
752
842
|
// Drop the load-time graph so a teardown'd kernel doesn't pin every
|
|
753
843
|
// manifest file's text in memory (LoadedFile retains the parsed
|
|
754
844
|
// documents + the original YAML bytes). Reusing the kernel after
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { SEVERITY } from "@telorun/sdk";
|
|
2
|
+
import { ConsoleSink } from "./console-sink.js";
|
|
3
|
+
import type { LogSinkInstance } from "./log-sink.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The pre-manifest console writer — `kernel/specs/logging.md` §12.3.
|
|
7
|
+
*
|
|
8
|
+
* Records emitted before the manifest is parsed — loader and parse diagnostics —
|
|
9
|
+
* cannot consult a `logging:` block that does not yet exist. During that phase
|
|
10
|
+
* the runtime uses a fixed default of `info` on an internal writer, and switches
|
|
11
|
+
* to the declared configuration as soon as the manifest resolves. This is the
|
|
12
|
+
* only phase not manifest-governed, and it is not configurable by other means.
|
|
13
|
+
*
|
|
14
|
+
* The writer is kernel-internal and is deliberately **not** a
|
|
15
|
+
* `Telo.ConsoleSink` resource — it exists precisely because no resource can yet.
|
|
16
|
+
* It is also what makes D3 hold: because the pre-manifest window is covered
|
|
17
|
+
* unconditionally, declared sinks are free to be resources that attach later,
|
|
18
|
+
* with buffered records replayed into them.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const BOOTSTRAP_SINK_ID = "<bootstrap>";
|
|
22
|
+
|
|
23
|
+
export function createBootstrapWriter(options: {
|
|
24
|
+
env: Record<string, string | undefined>;
|
|
25
|
+
stdout: NodeJS.WritableStream;
|
|
26
|
+
stderr: NodeJS.WritableStream;
|
|
27
|
+
}): LogSinkInstance {
|
|
28
|
+
return new ConsoleSink({
|
|
29
|
+
sinkId: BOOTSTRAP_SINK_ID,
|
|
30
|
+
level: SEVERITY.info,
|
|
31
|
+
destination: "stderr",
|
|
32
|
+
encoding: "auto",
|
|
33
|
+
color: "auto",
|
|
34
|
+
env: options.env,
|
|
35
|
+
stdout: options.stdout,
|
|
36
|
+
stderr: options.stderr,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console color decision — `kernel/specs/logging.md` §11.2.
|
|
3
|
+
*
|
|
4
|
+
* The precedence order is normative and must be implemented exactly. Steps 2–7
|
|
5
|
+
* apply only under `color: auto`; `always` / `never` short-circuit at step 1.
|
|
6
|
+
*
|
|
7
|
+
* These environment inputs are **not** a second configuration channel and do not
|
|
8
|
+
* contradict D6. `NO_COLOR`, `FORCE_COLOR`, `TERM`, and `isatty()` describe the
|
|
9
|
+
* terminal's capability and the operator's preference, not the application's
|
|
10
|
+
* desired state: `auto` means "detect the environment", and the manifest remains
|
|
11
|
+
* the sole authority over *what* is logged — these affect only how it is
|
|
12
|
+
* painted.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type ColorSetting = "auto" | "always" | "never";
|
|
16
|
+
|
|
17
|
+
export interface ColorDecisionInput {
|
|
18
|
+
/** The manifest's `color:` setting. */
|
|
19
|
+
setting: ColorSetting;
|
|
20
|
+
/** The environment to consult. Always the real host environment, never the
|
|
21
|
+
* guardrail proxy — these are host capability signals, not bindings. */
|
|
22
|
+
env: Record<string, string | undefined>;
|
|
23
|
+
/** Whether the **sink's actual output descriptor** is a TTY. A console sink on
|
|
24
|
+
* `stdout` and another on `stderr` can decide differently, and that is
|
|
25
|
+
* correct — this is never the process's descriptor by proxy. */
|
|
26
|
+
isTTY: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function decideColor(input: ColorDecisionInput): boolean {
|
|
30
|
+
const { setting, env, isTTY } = input;
|
|
31
|
+
|
|
32
|
+
// 1. An explicit manifest setting wins outright.
|
|
33
|
+
if (setting === "always") return true;
|
|
34
|
+
if (setting === "never") return false;
|
|
35
|
+
|
|
36
|
+
// 2. NO_COLOR: presence and non-emptiness matter, the value does not. Testing
|
|
37
|
+
// mere presence is a widespread bug — `NO_COLOR=""` must NOT disable color.
|
|
38
|
+
if (isNonEmpty(env["NO_COLOR"])) return false;
|
|
39
|
+
|
|
40
|
+
// 3. FORCE_COLOR: "0" disables, any other non-empty value enables.
|
|
41
|
+
const forceColor = env["FORCE_COLOR"];
|
|
42
|
+
if (isNonEmpty(forceColor)) return forceColor !== "0";
|
|
43
|
+
|
|
44
|
+
// 4. CLICOLOR_FORCE present and not "0" enables.
|
|
45
|
+
const clicolorForce = env["CLICOLOR_FORCE"];
|
|
46
|
+
if (clicolorForce !== undefined && clicolorForce !== "0") return true;
|
|
47
|
+
|
|
48
|
+
// 5. CLICOLOR=0 disables.
|
|
49
|
+
if (env["CLICOLOR"] === "0") return false;
|
|
50
|
+
|
|
51
|
+
// 6. A dumb terminal cannot render color.
|
|
52
|
+
if (env["TERM"] === "dumb") return false;
|
|
53
|
+
|
|
54
|
+
// 7. Otherwise follow the descriptor. Note there is deliberately no CI-variable
|
|
55
|
+
// branch: forcing color on merely because a CI variable is present is a
|
|
56
|
+
// widespread bug in existing libraries, not a convention to copy.
|
|
57
|
+
return isTTY;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isNonEmpty(value: string | undefined): boolean {
|
|
61
|
+
return value !== undefined && value !== "";
|
|
62
|
+
}
|