@telorun/sdk 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/index.d.ts CHANGED
@@ -17,6 +17,10 @@ export * from "./resource-context.js";
17
17
  export * from "./resource-instance.js";
18
18
  export * from "./resource-manifest.js";
19
19
  export * from "./invoke-error.js";
20
+ export * from "./log-record.js";
21
+ export * from "./log-sink.js";
22
+ export * from "./log-severity.js";
23
+ export * from "./logger.js";
20
24
  export * from "./network-fetch.js";
21
25
  export * from "./runtime-error.js";
22
26
  export * from "./runtime-event.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,UAAU,CAAC;AACzB,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,UAAU,CAAC;AACzB,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -17,6 +17,10 @@ export * from "./resource-context.js";
17
17
  export * from "./resource-instance.js";
18
18
  export * from "./resource-manifest.js";
19
19
  export * from "./invoke-error.js";
20
+ export * from "./log-record.js";
21
+ export * from "./log-sink.js";
22
+ export * from "./log-severity.js";
23
+ export * from "./logger.js";
20
24
  export * from "./network-fetch.js";
21
25
  export * from "./runtime-error.js";
22
26
  export * from "./runtime-event.js";
@@ -0,0 +1,72 @@
1
+ import type { SeverityNumber } from "./log-severity.js";
2
+ /**
3
+ * The Telo log record model — `kernel/specs/logging.md` §4. Maps 1:1 onto an
4
+ * OpenTelemetry `LogRecord`; encodings (§11) determine spelling and MUST NOT add
5
+ * or remove semantics.
6
+ *
7
+ * The one deliberate deviation from OTel is {@link LogRecord.message}: OTel's
8
+ * `Body` is an `AnyValue` and may be structured, while Telo requires a string and
9
+ * routes structured data to `attributes`. That keeps the console encoding total —
10
+ * every record has a renderable headline — and matches slog, pino, and zap.
11
+ */
12
+ /** The attribute value type (§6.1). `null` is a valid value and is preserved. */
13
+ export type AnyValue = string | boolean | number | bigint | Uint8Array | null | AnyValue[] | {
14
+ [key: string]: AnyValue;
15
+ };
16
+ export type LogAttributes = Record<string, AnyValue>;
17
+ /** Structured error (§4.2). The `cause` chain is bounded per §6.3. */
18
+ export interface ErrorValue {
19
+ /** Error class or code, e.g. `ERR_INVOKE_CANCELLED`. */
20
+ type: string;
21
+ message: string;
22
+ /** Multi-line, unmodified. */
23
+ stack?: string;
24
+ cause?: ErrorValue;
25
+ }
26
+ /** The emitting Telo resource (§7.3). `id` is the full hierarchical id, which is
27
+ * what distinguishes two instances of the same templated kind. */
28
+ export interface ResourceRef {
29
+ kind: string;
30
+ name: string;
31
+ id?: string;
32
+ }
33
+ export interface LogRecord {
34
+ /** Nanoseconds since the Unix epoch, by the origin clock. */
35
+ timestamp: bigint;
36
+ /** When the runtime observed the event, when that differs from `timestamp`
37
+ * (a bridged third-party logger, §13.3). */
38
+ observedTimestamp?: bigint;
39
+ severityNumber: SeverityNumber;
40
+ /** Canonical short name, or the original source spelling when bridging. */
41
+ severityText: string;
42
+ /** May be empty; never absent. */
43
+ message: string;
44
+ attributes?: LogAttributes;
45
+ /** 32 lowercase hex chars. */
46
+ traceId?: string;
47
+ /** 16 lowercase hex chars. Never present without `traceId`. */
48
+ spanId?: string;
49
+ /** Bit 0 = sampled, bit 1 reserved (§7.5), bits 2–7 zero. */
50
+ traceFlags?: number;
51
+ resource?: ResourceRef;
52
+ /** Module name of the emitter. Not unique — see `scope`. */
53
+ module?: string;
54
+ /** Dotted import-alias path identifying which *instance* emitted the record
55
+ * (`Api.Domain.Db`). Absent for the root Application's own resources. */
56
+ scope?: string;
57
+ /** Identifies a class of event; max 256 chars. */
58
+ eventName?: string;
59
+ error?: ErrorValue;
60
+ /** Non-zero when §6.3 limits truncated attributes. */
61
+ droppedAttributesCount?: number;
62
+ }
63
+ export declare function nowUnixNano(): bigint;
64
+ /** Epoch nanoseconds for a millisecond-resolution instant — used when bridging a
65
+ * third-party record that carries a `Date` or epoch-millis timestamp. */
66
+ export declare function unixNanoFromMillis(epochMillis: number): bigint;
67
+ /**
68
+ * RFC 3339, UTC, nanosecond precision, `Z` suffix — the `time` key of the `json`
69
+ * encoding (§11.1).
70
+ */
71
+ export declare function formatUnixNano(timestamp: bigint): string;
72
+ //# sourceMappingURL=log-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-record.d.ts","sourceRoot":"","sources":["../src/log-record.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;;;;;;;GASG;AAEH,iFAAiF;AACjF,MAAM,MAAM,QAAQ,GAChB,MAAM,GACN,OAAO,GACP,MAAM,GACN,MAAM,GACN,UAAU,GACV,IAAI,GACJ,QAAQ,EAAE,GACV;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAA;CAAE,CAAC;AAEhC,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAErD,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAED;mEACmE;AACnE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,SAAS;IACxB,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB;iDAC6C;IAC7C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAC;IACrB,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,aAAa,CAAC;IAC3B,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;8EAC0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,sDAAsD;IACtD,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAoBD,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED;0EAC0E;AAC1E,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAKxD"}
@@ -0,0 +1,34 @@
1
+ // Written as `BigInt(...)` rather than as `1_000_000n` literals: this module is
2
+ // consumed from source by the browser-targeted editor, whose tsconfig targets
3
+ // below ES2020 and cannot parse the literal syntax.
4
+ const NANOS_PER_MS = BigInt(1_000_000);
5
+ const NANOS_PER_SECOND = BigInt(1_000_000_000);
6
+ /**
7
+ * Node has no true nanosecond wall clock: `Date` is millisecond-resolution and
8
+ * `hrtime.bigint()` is monotonic rather than epoch-anchored. The best available
9
+ * is the performance origin plus the monotonic offset, which yields microsecond
10
+ * resolution zero-padded to nine digits. Format-conformant with §11.1; the extra
11
+ * three digits are always zero.
12
+ *
13
+ * The origin is captured once as a bigint so the addition never routes a
14
+ * 16-significant-digit value through a float64 and loses the low microseconds.
15
+ */
16
+ const ORIGIN_NANOS = BigInt(Math.round(performance.timeOrigin * 1e6));
17
+ export function nowUnixNano() {
18
+ return ORIGIN_NANOS + BigInt(Math.round(performance.now() * 1e6));
19
+ }
20
+ /** Epoch nanoseconds for a millisecond-resolution instant — used when bridging a
21
+ * third-party record that carries a `Date` or epoch-millis timestamp. */
22
+ export function unixNanoFromMillis(epochMillis) {
23
+ return BigInt(Math.round(epochMillis)) * NANOS_PER_MS;
24
+ }
25
+ /**
26
+ * RFC 3339, UTC, nanosecond precision, `Z` suffix — the `time` key of the `json`
27
+ * encoding (§11.1).
28
+ */
29
+ export function formatUnixNano(timestamp) {
30
+ const seconds = timestamp / NANOS_PER_SECOND;
31
+ const nanos = timestamp - seconds * NANOS_PER_SECOND;
32
+ const isoSeconds = new Date(Number(seconds) * 1000).toISOString().slice(0, 19);
33
+ return `${isoSeconds}.${nanos.toString().padStart(9, "0")}Z`;
34
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The OpenTelemetry `SeverityNumber` scale (1–24, stable), which Telo adopts
3
+ * verbatim — see `kernel/specs/logging.md` §5.
4
+ *
5
+ * Higher is more severe. All comparison, filtering, and threshold logic uses the
6
+ * number; severity *text* is presentation only and MUST NOT be compared. The
7
+ * full 24-value range stays valid on the wire so records bridged from a
8
+ * third-party logger survive a round-trip with their original spelling intact.
9
+ */
10
+ /** An OTel SeverityNumber. `0` (UNSPECIFIED) is never emitted by a Telo runtime. */
11
+ export type SeverityNumber = number;
12
+ /** The six levels Telo names. Each is the floor of its four-value OTel range. */
13
+ export declare const SEVERITY: {
14
+ readonly trace: 1;
15
+ readonly debug: 5;
16
+ readonly info: 9;
17
+ readonly warn: 13;
18
+ readonly error: 17;
19
+ readonly fatal: 21;
20
+ };
21
+ export type LevelName = keyof typeof SEVERITY;
22
+ export declare const LEVEL_NAMES: readonly LevelName[];
23
+ /** The severity at or above which a record describes an error (§5.1). This is
24
+ * the portable error predicate; runtimes expose it rather than re-deriving it. */
25
+ export declare const ERROR_SEVERITY_FLOOR = 17;
26
+ /**
27
+ * The range floor for a severity number — the canonical level a value maps onto.
28
+ * Out-of-range values clamp into 1–24 rather than producing `0`, which §5.1
29
+ * forbids emitting.
30
+ */
31
+ export declare function severityFloor(severity: SeverityNumber): number;
32
+ /** Canonical short name (`TRACE`…`FATAL`) for a severity number. */
33
+ export declare function severityText(severity: SeverityNumber): string;
34
+ /** `true` when the record describes an error (§5.1). */
35
+ export declare function isErrorSeverity(severity: SeverityNumber): boolean;
36
+ /** Resolve a manifest `level:` name to its severity number. */
37
+ export declare function severityForLevel(level: LevelName): number;
38
+ /**
39
+ * Map a level name of unknown provenance onto the scale. A name Telo does not
40
+ * recognize yields `undefined` so the caller can preserve the original spelling
41
+ * in `severity_text` while landing the number on a range floor (§5.1).
42
+ */
43
+ export declare function parseLevelName(name: string): number | undefined;
44
+ /**
45
+ * Go's `log/slog` documents that subtracting 9 from an OTel severity converts it
46
+ * to the slog range — an exact, officially sanctioned relation, so a Go runtime
47
+ * uses arithmetic rather than a table (§5.2). Exposed here so the conformance
48
+ * vectors can assert the relation from the Node side too.
49
+ */
50
+ export declare const SLOG_OFFSET = 9;
51
+ export declare function pinoLevelForSeverity(severity: SeverityNumber): number;
52
+ /** `undefined` for a pino level Telo does not name, so the caller preserves the
53
+ * source spelling and falls back to the nearest floor. */
54
+ export declare function severityForPinoLevel(level: number): number | undefined;
55
+ //# sourceMappingURL=log-severity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-severity.d.ts","sourceRoot":"","sources":["../src/log-severity.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,oFAAoF;AACpF,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC;AAEpC,iFAAiF;AACjF,eAAO,MAAM,QAAQ;;;;;;;CAOX,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,QAAQ,CAAC;AAE9C,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAAyD,CAAC;AAEtG;mFACmF;AACnF,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAavC;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAQ9D;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAE7D;AAED,wDAAwD;AACxD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAEjE;AAED,+DAA+D;AAC/D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAU/D;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,IAAI,CAAC;AAwB7B,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAErE;AAED;2DAC2D;AAC3D,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEtE"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The OpenTelemetry `SeverityNumber` scale (1–24, stable), which Telo adopts
3
+ * verbatim — see `kernel/specs/logging.md` §5.
4
+ *
5
+ * Higher is more severe. All comparison, filtering, and threshold logic uses the
6
+ * number; severity *text* is presentation only and MUST NOT be compared. The
7
+ * full 24-value range stays valid on the wire so records bridged from a
8
+ * third-party logger survive a round-trip with their original spelling intact.
9
+ */
10
+ /** The six levels Telo names. Each is the floor of its four-value OTel range. */
11
+ export const SEVERITY = {
12
+ trace: 1,
13
+ debug: 5,
14
+ info: 9,
15
+ warn: 13,
16
+ error: 17,
17
+ fatal: 21,
18
+ };
19
+ export const LEVEL_NAMES = ["trace", "debug", "info", "warn", "error", "fatal"];
20
+ /** The severity at or above which a record describes an error (§5.1). This is
21
+ * the portable error predicate; runtimes expose it rather than re-deriving it. */
22
+ export const ERROR_SEVERITY_FLOOR = 17;
23
+ const FLOORS = [1, 5, 9, 13, 17, 21];
24
+ const TEXT_BY_FLOOR = {
25
+ 1: "TRACE",
26
+ 5: "DEBUG",
27
+ 9: "INFO",
28
+ 13: "WARN",
29
+ 17: "ERROR",
30
+ 21: "FATAL",
31
+ };
32
+ /**
33
+ * The range floor for a severity number — the canonical level a value maps onto.
34
+ * Out-of-range values clamp into 1–24 rather than producing `0`, which §5.1
35
+ * forbids emitting.
36
+ */
37
+ export function severityFloor(severity) {
38
+ const clamped = severity < 1 ? 1 : severity > 24 ? 24 : Math.trunc(severity);
39
+ let floor = FLOORS[0];
40
+ for (const candidate of FLOORS) {
41
+ if (candidate <= clamped)
42
+ floor = candidate;
43
+ else
44
+ break;
45
+ }
46
+ return floor;
47
+ }
48
+ /** Canonical short name (`TRACE`…`FATAL`) for a severity number. */
49
+ export function severityText(severity) {
50
+ return TEXT_BY_FLOOR[severityFloor(severity)];
51
+ }
52
+ /** `true` when the record describes an error (§5.1). */
53
+ export function isErrorSeverity(severity) {
54
+ return severity >= ERROR_SEVERITY_FLOOR;
55
+ }
56
+ /** Resolve a manifest `level:` name to its severity number. */
57
+ export function severityForLevel(level) {
58
+ return SEVERITY[level];
59
+ }
60
+ /**
61
+ * Map a level name of unknown provenance onto the scale. A name Telo does not
62
+ * recognize yields `undefined` so the caller can preserve the original spelling
63
+ * in `severity_text` while landing the number on a range floor (§5.1).
64
+ */
65
+ export function parseLevelName(name) {
66
+ const key = name.trim().toLowerCase();
67
+ // Own-property check, not `in`: `in` also matches inherited members, so
68
+ // `parseLevelName("toString")` would otherwise return a Function and defeat
69
+ // the `?? fallback` at every call site. Written as `hasOwnProperty.call`
70
+ // rather than `Object.hasOwn` because this module is consumed from source by
71
+ // the browser-targeted editor, whose tsconfig targets below ES2022.
72
+ return Object.prototype.hasOwnProperty.call(SEVERITY, key)
73
+ ? SEVERITY[key]
74
+ : undefined;
75
+ }
76
+ /**
77
+ * Go's `log/slog` documents that subtracting 9 from an OTel severity converts it
78
+ * to the slog range — an exact, officially sanctioned relation, so a Go runtime
79
+ * uses arithmetic rather than a table (§5.2). Exposed here so the conformance
80
+ * vectors can assert the relation from the Node side too.
81
+ */
82
+ export const SLOG_OFFSET = 9;
83
+ /**
84
+ * pino's scale is 10× and offset, with no arithmetic relation to OTel, so §5.2
85
+ * requires a table. Used by the Fastify logger replacement (§13.3).
86
+ */
87
+ const PINO_BY_SEVERITY = {
88
+ 1: 10,
89
+ 5: 20,
90
+ 9: 30,
91
+ 13: 40,
92
+ 17: 50,
93
+ 21: 60,
94
+ };
95
+ const SEVERITY_BY_PINO = {
96
+ 10: 1,
97
+ 20: 5,
98
+ 30: 9,
99
+ 40: 13,
100
+ 50: 17,
101
+ 60: 21,
102
+ };
103
+ export function pinoLevelForSeverity(severity) {
104
+ return PINO_BY_SEVERITY[severityFloor(severity)];
105
+ }
106
+ /** `undefined` for a pino level Telo does not name, so the caller preserves the
107
+ * source spelling and falls back to the nearest floor. */
108
+ export function severityForPinoLevel(level) {
109
+ return SEVERITY_BY_PINO[level];
110
+ }
@@ -0,0 +1,91 @@
1
+ import type { LogRecord } from "./log-record.js";
2
+ /**
3
+ * The `Telo.Sink` capability contract — `kernel/specs/logging.md` §10.
4
+ *
5
+ * This lives in the SDK rather than the kernel because §10.2 makes the sink set
6
+ * **open to the ecosystem**: a third party ships a sink by publishing a module
7
+ * whose kind extends `Telo.LogSink`. That module is an ordinary module author's
8
+ * artifact, so the contract it implements belongs on the module-author surface,
9
+ * not behind a kernel-internal import.
10
+ *
11
+ * The logger writes to a sink through this contract directly and **never**
12
+ * through `ctx.invoke`: per-record dispatch is far too slow for a logging hot
13
+ * path, and the dispatch chokepoint emits trace events, so logging through it
14
+ * would generate telemetry from inside the telemetry path.
15
+ *
16
+ * The contract is deliberately payload-opaque — no filtering, no encoding —
17
+ * which is what lets a future `Telo.TraceSink` reuse the capability with a
18
+ * different record type. Log-specific configuration lives on the
19
+ * `Telo.LogSink` abstract instead.
20
+ */
21
+ export type DropCause = "buffer_full" | "sampled" | "encode_failure" | "sink_error";
22
+ /** Policy for a saturated buffer (§10.3). A runtime that cannot honour `block`
23
+ * rejects the manifest at load rather than silently substituting a dropping
24
+ * policy. */
25
+ export type OnFull = "block" | "drop_new" | "drop_old";
26
+ export interface SinkBufferPolicy {
27
+ /** Bounded. Never unbounded. */
28
+ buffer: number;
29
+ onFull: OnFull;
30
+ /** Max time a record may sit buffered, in milliseconds. */
31
+ flushIntervalMs: number;
32
+ }
33
+ export declare const DEFAULT_BUFFER_POLICY: SinkBufferPolicy;
34
+ export interface LogSinkInstance {
35
+ /** Identity for drop accounting (§10.4): the resource name for a `!ref`, or
36
+ * kind plus position for an inline definition. */
37
+ readonly sinkId: string;
38
+ /** This sink's own fan-out filter, applied *after* the record is created. It
39
+ * never decides whether a record is created at all — that is the pipeline's
40
+ * minimum-level gate. */
41
+ readonly level: number;
42
+ /**
43
+ * Whether the sink can be drained to its destination from inside a
44
+ * synchronous call, with no scheduler turn. A file descriptor write can; a
45
+ * network round-trip cannot, and neither can a transport living on another
46
+ * thread — the producer cannot drain a queue it does not own.
47
+ *
48
+ * A capability tier, not a language carve-out: the same rule makes an OTLP
49
+ * sink best-effort in Rust and Go, where blocking a producer thread is
50
+ * possible but still would not make a round-trip synchronous.
51
+ */
52
+ readonly syncFlushable: boolean;
53
+ /** Accept a record. MUST NOT throw — a sink failure is reported out-of-band
54
+ * and counted, never propagated to the caller (§8.4). */
55
+ write(record: LogRecord): void;
56
+ /** Drain asynchronously. */
57
+ flush(): Promise<void>;
58
+ /** Drain to completion before returning. A no-op when {@link syncFlushable}
59
+ * is `false`; the `fatal` path initiates those sinks' flushes without
60
+ * waiting, because blocking on a sink it cannot synchronously drain is a
61
+ * deadlock on an event loop, not durability. */
62
+ flushSync(): void;
63
+ /** Release the destination. Called during teardown, after the final flush. */
64
+ close(): Promise<void>;
65
+ }
66
+ /**
67
+ * The pipeline surface a sink controller reaches for — attach, detach, resolve a
68
+ * level, count a drop. Deliberately narrow: everything else about the pipeline
69
+ * stays private to the runtime, so a third-party sink depends on this and
70
+ * nothing deeper.
71
+ */
72
+ export interface LoggingHost {
73
+ attach(sink: LogSinkInstance): void;
74
+ detach(sink: LogSinkInstance): void;
75
+ /** Resolve a sink's declared `level:` to a severity number, falling back to
76
+ * the effective scope threshold when the sink declares none (§12.1). */
77
+ levelFor(level: string | undefined): number;
78
+ /** Count `count` dropped records against this sink so §10.4's accounting stays
79
+ * complete. `count` defaults to 1; a sink that loses a whole batch at once
80
+ * (an OTLP export failure) passes the batch size so the total is not
81
+ * undercounted to one-per-failure. */
82
+ recordDrop(sinkId: string, cause: DropCause, count?: number): void;
83
+ }
84
+ /** The diagnostic §10.3 requires when a runtime cannot honour `on_full: block`.
85
+ * Rejecting is deliberate: `on_full` exists so an operator can state durability
86
+ * intent, and silently substituting a dropping policy hands back the opposite
87
+ * guarantee — discovered from a gap in an audit trail rather than from an
88
+ * error. */
89
+ export declare function blockUnsupportedMessage(sinkId: string): string;
90
+ export declare const BLOCK_UNSUPPORTED = "ERR_LOG_SINK_ON_FULL_UNSUPPORTED";
91
+ //# sourceMappingURL=log-sink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-sink.d.ts","sourceRoot":"","sources":["../src/log-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,SAAS,GAAG,gBAAgB,GAAG,YAAY,CAAC;AAEpF;;cAEc;AACd,MAAM,MAAM,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,UAAU,CAAC;AAEvD,MAAM,WAAW,gBAAgB;IAC/B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,eAAO,MAAM,qBAAqB,EAAE,gBAInC,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B;uDACmD;IACnD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB;;8BAE0B;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAEhC;8DAC0D;IAC1D,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IAE/B,4BAA4B;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB;;;qDAGiD;IACjD,SAAS,IAAI,IAAI,CAAC;IAElB,8EAA8E;IAC9E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,MAAM,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC;6EACyE;IACzE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;IAC5C;;;2CAGuC;IACvC,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpE;AAED;;;;aAIa;AACb,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAM9D;AAED,eAAO,MAAM,iBAAiB,qCAAqC,CAAC"}
@@ -0,0 +1,16 @@
1
+ export const DEFAULT_BUFFER_POLICY = {
2
+ buffer: 8192,
3
+ onFull: "drop_new",
4
+ flushIntervalMs: 1000,
5
+ };
6
+ /** The diagnostic §10.3 requires when a runtime cannot honour `on_full: block`.
7
+ * Rejecting is deliberate: `on_full` exists so an operator can state durability
8
+ * intent, and silently substituting a dropping policy hands back the opposite
9
+ * guarantee — discovered from a gap in an audit trail rather than from an
10
+ * error. */
11
+ export function blockUnsupportedMessage(sinkId) {
12
+ return (`Sink "${sinkId}": on_full: block is not supported by this runtime ` +
13
+ `(single-threaded event loop — blocking the producer would stall the writer). ` +
14
+ `Use \`drop_new\` or \`drop_old\`, or move this sink to a worker thread.`);
15
+ }
16
+ export const BLOCK_UNSUPPORTED = "ERR_LOG_SINK_ON_FULL_UNSUPPORTED";
@@ -0,0 +1,85 @@
1
+ import type { AnyValue, LogAttributes } from "./log-record.js";
2
+ import type { SeverityNumber } from "./log-severity.js";
3
+ /**
4
+ * The logger surface every Telo runtime exposes — `kernel/specs/logging.md` §8.
5
+ *
6
+ * Reached ambiently as `ctx.log`. The logger is ambient rather than a resource
7
+ * because it must work before any resource initializes; its *sinks* are
8
+ * resources, which is what keeps the destination set open to the ecosystem.
9
+ */
10
+ /**
11
+ * A value resolved only on the emit path (§8.2) — slog's `LogValuer`, zap's
12
+ * `ObjectMarshaler`, `tracing`'s `Value`. A deferred value attached to a
13
+ * suppressed record is never resolved, so an expensive rendering costs nothing
14
+ * below the threshold. This is RECOMMENDED sugar and does **not** substitute for
15
+ * {@link Logger.enabled}, which is the only mechanism that avoids evaluating a
16
+ * call's *arguments*.
17
+ */
18
+ export interface LogValuer {
19
+ toLogValue(): AnyValue;
20
+ }
21
+ export type LogAttributeInput = AnyValue | LogValuer;
22
+ export type LogAttributesInput = Record<string, LogAttributeInput>;
23
+ /** Per-record extras that are top-level record fields rather than attributes.
24
+ * Kept out of the attribute map so they cannot collide with a reserved key. */
25
+ export interface LogOptions {
26
+ /** Any thrown value. Normalized to the record's `error` (§4.2), with the
27
+ * `cause` chain bounded per §6.3. */
28
+ error?: unknown;
29
+ /** Identifies a class of event; max 256 chars. Bridges to the event bus. */
30
+ eventName?: string;
31
+ /** When the event occurred, if earlier than the moment `log()` was called —
32
+ * set by a bridge, which also stamps `observedTimestamp` (§13.3). */
33
+ timestamp?: bigint;
34
+ /** The original source spelling of the level, preserved when bridging a level
35
+ * Telo does not name (§5.1). Defaults to the canonical short name. */
36
+ severityText?: string;
37
+ }
38
+ export interface Logger {
39
+ /**
40
+ * Whether a record at this severity would reach any sink. The load-bearing
41
+ * performance primitive: guard an expensive call with it so the *arguments*
42
+ * are never evaluated.
43
+ *
44
+ * Never blocks, never throws. The result is **not static** — it changes when
45
+ * configuration changes or a sink attaches or detaches (§12.4), so callers
46
+ * re-check per emission rather than caching a boolean.
47
+ */
48
+ enabled(severity: SeverityNumber): boolean;
49
+ /**
50
+ * Emit a record. Never throws, under any condition, including sink failure —
51
+ * a sink failure is reported on the fallback diagnostic stream and counted,
52
+ * never propagated and never swallowed (§8.4).
53
+ */
54
+ log(severity: SeverityNumber, message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
55
+ /**
56
+ * A child logger whose bound attributes are merged into every record it emits.
57
+ * Record attributes override bound attributes. Binding is O(1) amortized: the
58
+ * merge happens once here, never per record.
59
+ */
60
+ with(attributes: LogAttributesInput): Logger;
61
+ /** Drain every attached sink. Bounded by the caller; see §10.5. */
62
+ flush(): Promise<void>;
63
+ trace(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
64
+ debug(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
65
+ info(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
66
+ warn(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
67
+ error(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
68
+ /**
69
+ * Emits at severity 21. Severity never implies control flow (§5, D5): `fatal`
70
+ * does **not** terminate the process, exit, or panic — it triggers an
71
+ * immediate flush, synchronous on every sink that supports it and best-effort
72
+ * on the rest (§10.5).
73
+ */
74
+ fatal(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
75
+ }
76
+ /** Resolve a {@link LogValuer} if the value is one, else pass it through. */
77
+ export declare function isLogValuer(value: unknown): value is LogValuer;
78
+ /** A logger that discards everything. Used where a logger is structurally
79
+ * required before one is available, and by tests that assert silence. */
80
+ export declare const NOOP_LOGGER: Logger;
81
+ /** The bound-attribute merge of {@link Logger.with}, exposed so a runtime's
82
+ * child-logger implementation and its conformance vectors share one definition
83
+ * of "record attributes win". */
84
+ export declare function mergeBoundAttributes(bound: LogAttributes | undefined, record: LogAttributes | undefined): LogAttributes | undefined;
85
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;;;;GAMG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,UAAU,IAAI,QAAQ,CAAC;CACxB;AAED,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,SAAS,CAAC;AACrD,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AAEnE;gFACgF;AAChF,MAAM,WAAW,UAAU;IACzB;0CACsC;IACtC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;0EACsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;2EACuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,MAAM;IACrB;;;;;;;;OAQG;IACH,OAAO,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC;IAE3C;;;;OAIG;IACH,GAAG,CACD,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,kBAAkB,EAC/B,OAAO,CAAC,EAAE,UAAU,GACnB,IAAI,CAAC;IAER;;;;OAIG;IACH,IAAI,CAAC,UAAU,EAAE,kBAAkB,GAAG,MAAM,CAAC;IAE7C,mEAAmE;IACnE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACpF,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACpF,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACnF,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACnF,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACpF;;;;;OAKG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CACrF;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAM9D;AAED;0EAC0E;AAC1E,eAAO,MAAM,WAAW,EAAE,MAWzB,CAAC;AAEF;;kCAEkC;AAClC,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,aAAa,GAAG,SAAS,EAChC,MAAM,EAAE,aAAa,GAAG,SAAS,GAChC,aAAa,GAAG,SAAS,CAI3B"}
package/dist/logger.js ADDED
@@ -0,0 +1,30 @@
1
+ /** Resolve a {@link LogValuer} if the value is one, else pass it through. */
2
+ export function isLogValuer(value) {
3
+ return (typeof value === "object" &&
4
+ value !== null &&
5
+ typeof value.toLogValue === "function");
6
+ }
7
+ /** A logger that discards everything. Used where a logger is structurally
8
+ * required before one is available, and by tests that assert silence. */
9
+ export const NOOP_LOGGER = {
10
+ enabled: () => false,
11
+ log: () => { },
12
+ with: () => NOOP_LOGGER,
13
+ flush: async () => { },
14
+ trace: () => { },
15
+ debug: () => { },
16
+ info: () => { },
17
+ warn: () => { },
18
+ error: () => { },
19
+ fatal: () => { },
20
+ };
21
+ /** The bound-attribute merge of {@link Logger.with}, exposed so a runtime's
22
+ * child-logger implementation and its conformance vectors share one definition
23
+ * of "record attributes win". */
24
+ export function mergeBoundAttributes(bound, record) {
25
+ if (!bound)
26
+ return record;
27
+ if (!record)
28
+ return bound;
29
+ return { ...bound, ...record };
30
+ }
@@ -1,5 +1,7 @@
1
1
  import type { CancellationSource, InvokeContext, OpenSpan, OpenSpanOptions } from "./cancellation.js";
2
2
  import { ControllerContext } from "./controller-context.js";
3
+ import type { Logger } from "./logger.js";
4
+ import type { LoggingHost } from "./log-sink.js";
3
5
  import { ControllerPolicy } from "./controller-policy.js";
4
6
  import { EvaluationContext } from "./evaluation-context.js";
5
7
  import { ModuleContext } from "./module-context.js";
@@ -119,6 +121,28 @@ export interface ResourceContext extends ControllerContext {
119
121
  * manifests. Use this when you need the full kind surface area visible from
120
122
  * the module. */
121
123
  loadManifests(url: string): Promise<ResourceManifest[]>;
124
+ /**
125
+ * The structured logger for this resource — `kernel/specs/logging.md` §13.2.
126
+ *
127
+ * Ambient rather than a resource (D3), because it must work before any
128
+ * resource initializes. Records are automatically stamped with this
129
+ * resource's identity, its module, its import-alias scope, and the active
130
+ * dispatch span's trace and span ids — a controller never passes those.
131
+ *
132
+ * A controller emits diagnostics **only** through this. Writing to
133
+ * stdout/stderr for diagnostic purposes is forbidden; writing to stdout as
134
+ * *data* (as the `Console` module does) is a separate, legitimate concern and
135
+ * is unaffected.
136
+ */
137
+ readonly log: Logger;
138
+ /**
139
+ * Sink attach/detach and drop accounting — the surface a `Telo.Sink`
140
+ * controller needs and nothing else. §10.2 keeps the sink set open to the
141
+ * ecosystem, so a third-party sink module reaches the pipeline through this
142
+ * rather than through a kernel-internal import. Ordinary controllers use
143
+ * {@link log}.
144
+ */
145
+ readonly logging: LoggingHost;
122
146
  readonly moduleContext: ModuleContext;
123
147
  readonly env: Record<string, string | undefined>;
124
148
  readonly stdin: NodeJS.ReadableStream;
@@ -1 +1 @@
1
- {"version":3,"file":"resource-context.d.ts","sourceRoot":"","sources":["../src/resource-context.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resource-context.d.ts","sourceRoot":"","sources":["../src/resource-context.ts"],"names":[],"mappings":"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"}
@@ -6,7 +6,24 @@ export type ResourceInstance<TInput = Record<string, any>, TOutput = any> = Part
6
6
  init?(ctx?: ResourceContext): Promise<void>;
7
7
  teardown?(): void | Promise<void>;
8
8
  snapshot?(): Record<string, any> | Promise<Record<string, any>>;
9
+ /**
10
+ * Teardown ordering hint. Instances tear down in ascending priority — a
11
+ * higher number means *later*. Default `0`; within one priority the base
12
+ * order (reverse init) is preserved.
13
+ *
14
+ * This exists because the base order is reverse *insertion* order, which the
15
+ * multi-pass init retry can perturb, so a resource that must reliably outlive
16
+ * the rest at shutdown cannot express that through the dependency graph. Log
17
+ * sinks set {@link TEARDOWN_LAST} so they flush after every resource that
18
+ * might log while shutting down — a generic mechanism, not a logging-specific
19
+ * carve-out in the teardown path.
20
+ */
21
+ teardownPriority?: number;
9
22
  };
23
+ /** Teardown-last priority (see {@link ResourceInstance.teardownPriority}). Log
24
+ * sinks use it so anything logging during its own teardown still reaches a live
25
+ * destination. */
26
+ export declare const TEARDOWN_LAST = 1000;
10
27
  /** The kind+name an instance was resolved from. */
11
28
  export interface RefIdentity {
12
29
  kind: string;
@@ -1 +1 @@
1
- {"version":3,"file":"resource-instance.d.ts","sourceRoot":"","sources":["../src/resource-instance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,MAAM,gBAAgB,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,GAAG,IAAI,OAAO,CACjF,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAC3B,GACC,OAAO,CAAC,QAAQ,CAAC,GACjB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,GAAG;IAC3B,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,QAAQ,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,QAAQ,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;CACjE,CAAC;AAEJ,mDAAmD;AACnD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,OAAO,MAAuC,CAAC;AAE1E;sFACsF;AACtF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CASnF;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAExE"}
1
+ {"version":3,"file":"resource-instance.d.ts","sourceRoot":"","sources":["../src/resource-instance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,MAAM,gBAAgB,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,GAAG,IAAI,OAAO,CACjF,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAC3B,GACC,OAAO,CAAC,QAAQ,CAAC,GACjB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,GAAG;IAC3B,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,QAAQ,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,QAAQ,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAChE;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEJ;;mBAEmB;AACnB,eAAO,MAAM,aAAa,OAAO,CAAC;AAElC,mDAAmD;AACnD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,OAAO,MAAuC,CAAC;AAE1E;sFACsF;AACtF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CASnF;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAExE"}
@@ -1,3 +1,7 @@
1
+ /** Teardown-last priority (see {@link ResourceInstance.teardownPriority}). Log
2
+ * sinks use it so anything logging during its own teardown still reaches a live
3
+ * destination. */
4
+ export const TEARDOWN_LAST = 1000;
1
5
  /**
2
6
  * Non-enumerable identity tag the kernel stamps on a live instance when it
3
7
  * injects a resolved `!ref` into a slot. A consumer that holds only the bare
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/sdk",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "Telo SDK - Public API for Telo module authors.",
5
5
  "keywords": [
6
6
  "telo",
package/src/index.ts CHANGED
@@ -17,6 +17,10 @@ export * from "./resource-context.js";
17
17
  export * from "./resource-instance.js";
18
18
  export * from "./resource-manifest.js";
19
19
  export * from "./invoke-error.js";
20
+ export * from "./log-record.js";
21
+ export * from "./log-sink.js";
22
+ export * from "./log-severity.js";
23
+ export * from "./logger.js";
20
24
  export * from "./network-fetch.js";
21
25
  export * from "./runtime-error.js";
22
26
  export * from "./runtime-event.js";
@@ -0,0 +1,113 @@
1
+ import type { SeverityNumber } from "./log-severity.js";
2
+
3
+ /**
4
+ * The Telo log record model — `kernel/specs/logging.md` §4. Maps 1:1 onto an
5
+ * OpenTelemetry `LogRecord`; encodings (§11) determine spelling and MUST NOT add
6
+ * or remove semantics.
7
+ *
8
+ * The one deliberate deviation from OTel is {@link LogRecord.message}: OTel's
9
+ * `Body` is an `AnyValue` and may be structured, while Telo requires a string and
10
+ * routes structured data to `attributes`. That keeps the console encoding total —
11
+ * every record has a renderable headline — and matches slog, pino, and zap.
12
+ */
13
+
14
+ /** The attribute value type (§6.1). `null` is a valid value and is preserved. */
15
+ export type AnyValue =
16
+ | string
17
+ | boolean
18
+ | number
19
+ | bigint
20
+ | Uint8Array
21
+ | null
22
+ | AnyValue[]
23
+ | { [key: string]: AnyValue };
24
+
25
+ export type LogAttributes = Record<string, AnyValue>;
26
+
27
+ /** Structured error (§4.2). The `cause` chain is bounded per §6.3. */
28
+ export interface ErrorValue {
29
+ /** Error class or code, e.g. `ERR_INVOKE_CANCELLED`. */
30
+ type: string;
31
+ message: string;
32
+ /** Multi-line, unmodified. */
33
+ stack?: string;
34
+ cause?: ErrorValue;
35
+ }
36
+
37
+ /** The emitting Telo resource (§7.3). `id` is the full hierarchical id, which is
38
+ * what distinguishes two instances of the same templated kind. */
39
+ export interface ResourceRef {
40
+ kind: string;
41
+ name: string;
42
+ id?: string;
43
+ }
44
+
45
+ export interface LogRecord {
46
+ /** Nanoseconds since the Unix epoch, by the origin clock. */
47
+ timestamp: bigint;
48
+ /** When the runtime observed the event, when that differs from `timestamp`
49
+ * (a bridged third-party logger, §13.3). */
50
+ observedTimestamp?: bigint;
51
+ severityNumber: SeverityNumber;
52
+ /** Canonical short name, or the original source spelling when bridging. */
53
+ severityText: string;
54
+ /** May be empty; never absent. */
55
+ message: string;
56
+ attributes?: LogAttributes;
57
+ /** 32 lowercase hex chars. */
58
+ traceId?: string;
59
+ /** 16 lowercase hex chars. Never present without `traceId`. */
60
+ spanId?: string;
61
+ /** Bit 0 = sampled, bit 1 reserved (§7.5), bits 2–7 zero. */
62
+ traceFlags?: number;
63
+ resource?: ResourceRef;
64
+ /** Module name of the emitter. Not unique — see `scope`. */
65
+ module?: string;
66
+ /** Dotted import-alias path identifying which *instance* emitted the record
67
+ * (`Api.Domain.Db`). Absent for the root Application's own resources. */
68
+ scope?: string;
69
+ /** Identifies a class of event; max 256 chars. */
70
+ eventName?: string;
71
+ error?: ErrorValue;
72
+ /** Non-zero when §6.3 limits truncated attributes. */
73
+ droppedAttributesCount?: number;
74
+ }
75
+
76
+ // Written as `BigInt(...)` rather than as `1_000_000n` literals: this module is
77
+ // consumed from source by the browser-targeted editor, whose tsconfig targets
78
+ // below ES2020 and cannot parse the literal syntax.
79
+ const NANOS_PER_MS = BigInt(1_000_000);
80
+ const NANOS_PER_SECOND = BigInt(1_000_000_000);
81
+
82
+ /**
83
+ * Node has no true nanosecond wall clock: `Date` is millisecond-resolution and
84
+ * `hrtime.bigint()` is monotonic rather than epoch-anchored. The best available
85
+ * is the performance origin plus the monotonic offset, which yields microsecond
86
+ * resolution zero-padded to nine digits. Format-conformant with §11.1; the extra
87
+ * three digits are always zero.
88
+ *
89
+ * The origin is captured once as a bigint so the addition never routes a
90
+ * 16-significant-digit value through a float64 and loses the low microseconds.
91
+ */
92
+ const ORIGIN_NANOS = BigInt(Math.round(performance.timeOrigin * 1e6));
93
+
94
+ export function nowUnixNano(): bigint {
95
+ return ORIGIN_NANOS + BigInt(Math.round(performance.now() * 1e6));
96
+ }
97
+
98
+ /** Epoch nanoseconds for a millisecond-resolution instant — used when bridging a
99
+ * third-party record that carries a `Date` or epoch-millis timestamp. */
100
+ export function unixNanoFromMillis(epochMillis: number): bigint {
101
+ return BigInt(Math.round(epochMillis)) * NANOS_PER_MS;
102
+ }
103
+
104
+ /**
105
+ * RFC 3339, UTC, nanosecond precision, `Z` suffix — the `time` key of the `json`
106
+ * encoding (§11.1).
107
+ */
108
+ export function formatUnixNano(timestamp: bigint): string {
109
+ const seconds = timestamp / NANOS_PER_SECOND;
110
+ const nanos = timestamp - seconds * NANOS_PER_SECOND;
111
+ const isoSeconds = new Date(Number(seconds) * 1000).toISOString().slice(0, 19);
112
+ return `${isoSeconds}.${nanos.toString().padStart(9, "0")}Z`;
113
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The OpenTelemetry `SeverityNumber` scale (1–24, stable), which Telo adopts
3
+ * verbatim — see `kernel/specs/logging.md` §5.
4
+ *
5
+ * Higher is more severe. All comparison, filtering, and threshold logic uses the
6
+ * number; severity *text* is presentation only and MUST NOT be compared. The
7
+ * full 24-value range stays valid on the wire so records bridged from a
8
+ * third-party logger survive a round-trip with their original spelling intact.
9
+ */
10
+
11
+ /** An OTel SeverityNumber. `0` (UNSPECIFIED) is never emitted by a Telo runtime. */
12
+ export type SeverityNumber = number;
13
+
14
+ /** The six levels Telo names. Each is the floor of its four-value OTel range. */
15
+ export const SEVERITY = {
16
+ trace: 1,
17
+ debug: 5,
18
+ info: 9,
19
+ warn: 13,
20
+ error: 17,
21
+ fatal: 21,
22
+ } as const;
23
+
24
+ export type LevelName = keyof typeof SEVERITY;
25
+
26
+ export const LEVEL_NAMES: readonly LevelName[] = ["trace", "debug", "info", "warn", "error", "fatal"];
27
+
28
+ /** The severity at or above which a record describes an error (§5.1). This is
29
+ * the portable error predicate; runtimes expose it rather than re-deriving it. */
30
+ export const ERROR_SEVERITY_FLOOR = 17;
31
+
32
+ const FLOORS: readonly number[] = [1, 5, 9, 13, 17, 21];
33
+
34
+ const TEXT_BY_FLOOR: Readonly<Record<number, string>> = {
35
+ 1: "TRACE",
36
+ 5: "DEBUG",
37
+ 9: "INFO",
38
+ 13: "WARN",
39
+ 17: "ERROR",
40
+ 21: "FATAL",
41
+ };
42
+
43
+ /**
44
+ * The range floor for a severity number — the canonical level a value maps onto.
45
+ * Out-of-range values clamp into 1–24 rather than producing `0`, which §5.1
46
+ * forbids emitting.
47
+ */
48
+ export function severityFloor(severity: SeverityNumber): number {
49
+ const clamped = severity < 1 ? 1 : severity > 24 ? 24 : Math.trunc(severity);
50
+ let floor = FLOORS[0]!;
51
+ for (const candidate of FLOORS) {
52
+ if (candidate <= clamped) floor = candidate;
53
+ else break;
54
+ }
55
+ return floor;
56
+ }
57
+
58
+ /** Canonical short name (`TRACE`…`FATAL`) for a severity number. */
59
+ export function severityText(severity: SeverityNumber): string {
60
+ return TEXT_BY_FLOOR[severityFloor(severity)]!;
61
+ }
62
+
63
+ /** `true` when the record describes an error (§5.1). */
64
+ export function isErrorSeverity(severity: SeverityNumber): boolean {
65
+ return severity >= ERROR_SEVERITY_FLOOR;
66
+ }
67
+
68
+ /** Resolve a manifest `level:` name to its severity number. */
69
+ export function severityForLevel(level: LevelName): number {
70
+ return SEVERITY[level];
71
+ }
72
+
73
+ /**
74
+ * Map a level name of unknown provenance onto the scale. A name Telo does not
75
+ * recognize yields `undefined` so the caller can preserve the original spelling
76
+ * in `severity_text` while landing the number on a range floor (§5.1).
77
+ */
78
+ export function parseLevelName(name: string): number | undefined {
79
+ const key = name.trim().toLowerCase();
80
+ // Own-property check, not `in`: `in` also matches inherited members, so
81
+ // `parseLevelName("toString")` would otherwise return a Function and defeat
82
+ // the `?? fallback` at every call site. Written as `hasOwnProperty.call`
83
+ // rather than `Object.hasOwn` because this module is consumed from source by
84
+ // the browser-targeted editor, whose tsconfig targets below ES2022.
85
+ return Object.prototype.hasOwnProperty.call(SEVERITY, key)
86
+ ? SEVERITY[key as LevelName]
87
+ : undefined;
88
+ }
89
+
90
+ /**
91
+ * Go's `log/slog` documents that subtracting 9 from an OTel severity converts it
92
+ * to the slog range — an exact, officially sanctioned relation, so a Go runtime
93
+ * uses arithmetic rather than a table (§5.2). Exposed here so the conformance
94
+ * vectors can assert the relation from the Node side too.
95
+ */
96
+ export const SLOG_OFFSET = 9;
97
+
98
+ /**
99
+ * pino's scale is 10× and offset, with no arithmetic relation to OTel, so §5.2
100
+ * requires a table. Used by the Fastify logger replacement (§13.3).
101
+ */
102
+ const PINO_BY_SEVERITY: Readonly<Record<number, number>> = {
103
+ 1: 10,
104
+ 5: 20,
105
+ 9: 30,
106
+ 13: 40,
107
+ 17: 50,
108
+ 21: 60,
109
+ };
110
+
111
+ const SEVERITY_BY_PINO: Readonly<Record<number, number>> = {
112
+ 10: 1,
113
+ 20: 5,
114
+ 30: 9,
115
+ 40: 13,
116
+ 50: 17,
117
+ 60: 21,
118
+ };
119
+
120
+ export function pinoLevelForSeverity(severity: SeverityNumber): number {
121
+ return PINO_BY_SEVERITY[severityFloor(severity)]!;
122
+ }
123
+
124
+ /** `undefined` for a pino level Telo does not name, so the caller preserves the
125
+ * source spelling and falls back to the nearest floor. */
126
+ export function severityForPinoLevel(level: number): number | undefined {
127
+ return SEVERITY_BY_PINO[level];
128
+ }
@@ -0,0 +1,115 @@
1
+ import type { LogRecord } from "./log-record.js";
2
+
3
+ /**
4
+ * The `Telo.Sink` capability contract — `kernel/specs/logging.md` §10.
5
+ *
6
+ * This lives in the SDK rather than the kernel because §10.2 makes the sink set
7
+ * **open to the ecosystem**: a third party ships a sink by publishing a module
8
+ * whose kind extends `Telo.LogSink`. That module is an ordinary module author's
9
+ * artifact, so the contract it implements belongs on the module-author surface,
10
+ * not behind a kernel-internal import.
11
+ *
12
+ * The logger writes to a sink through this contract directly and **never**
13
+ * through `ctx.invoke`: per-record dispatch is far too slow for a logging hot
14
+ * path, and the dispatch chokepoint emits trace events, so logging through it
15
+ * would generate telemetry from inside the telemetry path.
16
+ *
17
+ * The contract is deliberately payload-opaque — no filtering, no encoding —
18
+ * which is what lets a future `Telo.TraceSink` reuse the capability with a
19
+ * different record type. Log-specific configuration lives on the
20
+ * `Telo.LogSink` abstract instead.
21
+ */
22
+
23
+ export type DropCause = "buffer_full" | "sampled" | "encode_failure" | "sink_error";
24
+
25
+ /** Policy for a saturated buffer (§10.3). A runtime that cannot honour `block`
26
+ * rejects the manifest at load rather than silently substituting a dropping
27
+ * policy. */
28
+ export type OnFull = "block" | "drop_new" | "drop_old";
29
+
30
+ export interface SinkBufferPolicy {
31
+ /** Bounded. Never unbounded. */
32
+ buffer: number;
33
+ onFull: OnFull;
34
+ /** Max time a record may sit buffered, in milliseconds. */
35
+ flushIntervalMs: number;
36
+ }
37
+
38
+ export const DEFAULT_BUFFER_POLICY: SinkBufferPolicy = {
39
+ buffer: 8192,
40
+ onFull: "drop_new",
41
+ flushIntervalMs: 1000,
42
+ };
43
+
44
+ export interface LogSinkInstance {
45
+ /** Identity for drop accounting (§10.4): the resource name for a `!ref`, or
46
+ * kind plus position for an inline definition. */
47
+ readonly sinkId: string;
48
+
49
+ /** This sink's own fan-out filter, applied *after* the record is created. It
50
+ * never decides whether a record is created at all — that is the pipeline's
51
+ * minimum-level gate. */
52
+ readonly level: number;
53
+
54
+ /**
55
+ * Whether the sink can be drained to its destination from inside a
56
+ * synchronous call, with no scheduler turn. A file descriptor write can; a
57
+ * network round-trip cannot, and neither can a transport living on another
58
+ * thread — the producer cannot drain a queue it does not own.
59
+ *
60
+ * A capability tier, not a language carve-out: the same rule makes an OTLP
61
+ * sink best-effort in Rust and Go, where blocking a producer thread is
62
+ * possible but still would not make a round-trip synchronous.
63
+ */
64
+ readonly syncFlushable: boolean;
65
+
66
+ /** Accept a record. MUST NOT throw — a sink failure is reported out-of-band
67
+ * and counted, never propagated to the caller (§8.4). */
68
+ write(record: LogRecord): void;
69
+
70
+ /** Drain asynchronously. */
71
+ flush(): Promise<void>;
72
+
73
+ /** Drain to completion before returning. A no-op when {@link syncFlushable}
74
+ * is `false`; the `fatal` path initiates those sinks' flushes without
75
+ * waiting, because blocking on a sink it cannot synchronously drain is a
76
+ * deadlock on an event loop, not durability. */
77
+ flushSync(): void;
78
+
79
+ /** Release the destination. Called during teardown, after the final flush. */
80
+ close(): Promise<void>;
81
+ }
82
+
83
+ /**
84
+ * The pipeline surface a sink controller reaches for — attach, detach, resolve a
85
+ * level, count a drop. Deliberately narrow: everything else about the pipeline
86
+ * stays private to the runtime, so a third-party sink depends on this and
87
+ * nothing deeper.
88
+ */
89
+ export interface LoggingHost {
90
+ attach(sink: LogSinkInstance): void;
91
+ detach(sink: LogSinkInstance): void;
92
+ /** Resolve a sink's declared `level:` to a severity number, falling back to
93
+ * the effective scope threshold when the sink declares none (§12.1). */
94
+ levelFor(level: string | undefined): number;
95
+ /** Count `count` dropped records against this sink so §10.4's accounting stays
96
+ * complete. `count` defaults to 1; a sink that loses a whole batch at once
97
+ * (an OTLP export failure) passes the batch size so the total is not
98
+ * undercounted to one-per-failure. */
99
+ recordDrop(sinkId: string, cause: DropCause, count?: number): void;
100
+ }
101
+
102
+ /** The diagnostic §10.3 requires when a runtime cannot honour `on_full: block`.
103
+ * Rejecting is deliberate: `on_full` exists so an operator can state durability
104
+ * intent, and silently substituting a dropping policy hands back the opposite
105
+ * guarantee — discovered from a gap in an audit trail rather than from an
106
+ * error. */
107
+ export function blockUnsupportedMessage(sinkId: string): string {
108
+ return (
109
+ `Sink "${sinkId}": on_full: block is not supported by this runtime ` +
110
+ `(single-threaded event loop — blocking the producer would stall the writer). ` +
111
+ `Use \`drop_new\` or \`drop_old\`, or move this sink to a worker thread.`
112
+ );
113
+ }
114
+
115
+ export const BLOCK_UNSUPPORTED = "ERR_LOG_SINK_ON_FULL_UNSUPPORTED";
package/src/logger.ts ADDED
@@ -0,0 +1,125 @@
1
+ import type { AnyValue, LogAttributes } from "./log-record.js";
2
+ import type { SeverityNumber } from "./log-severity.js";
3
+
4
+ /**
5
+ * The logger surface every Telo runtime exposes — `kernel/specs/logging.md` §8.
6
+ *
7
+ * Reached ambiently as `ctx.log`. The logger is ambient rather than a resource
8
+ * because it must work before any resource initializes; its *sinks* are
9
+ * resources, which is what keeps the destination set open to the ecosystem.
10
+ */
11
+
12
+ /**
13
+ * A value resolved only on the emit path (§8.2) — slog's `LogValuer`, zap's
14
+ * `ObjectMarshaler`, `tracing`'s `Value`. A deferred value attached to a
15
+ * suppressed record is never resolved, so an expensive rendering costs nothing
16
+ * below the threshold. This is RECOMMENDED sugar and does **not** substitute for
17
+ * {@link Logger.enabled}, which is the only mechanism that avoids evaluating a
18
+ * call's *arguments*.
19
+ */
20
+ export interface LogValuer {
21
+ toLogValue(): AnyValue;
22
+ }
23
+
24
+ export type LogAttributeInput = AnyValue | LogValuer;
25
+ export type LogAttributesInput = Record<string, LogAttributeInput>;
26
+
27
+ /** Per-record extras that are top-level record fields rather than attributes.
28
+ * Kept out of the attribute map so they cannot collide with a reserved key. */
29
+ export interface LogOptions {
30
+ /** Any thrown value. Normalized to the record's `error` (§4.2), with the
31
+ * `cause` chain bounded per §6.3. */
32
+ error?: unknown;
33
+ /** Identifies a class of event; max 256 chars. Bridges to the event bus. */
34
+ eventName?: string;
35
+ /** When the event occurred, if earlier than the moment `log()` was called —
36
+ * set by a bridge, which also stamps `observedTimestamp` (§13.3). */
37
+ timestamp?: bigint;
38
+ /** The original source spelling of the level, preserved when bridging a level
39
+ * Telo does not name (§5.1). Defaults to the canonical short name. */
40
+ severityText?: string;
41
+ }
42
+
43
+ export interface Logger {
44
+ /**
45
+ * Whether a record at this severity would reach any sink. The load-bearing
46
+ * performance primitive: guard an expensive call with it so the *arguments*
47
+ * are never evaluated.
48
+ *
49
+ * Never blocks, never throws. The result is **not static** — it changes when
50
+ * configuration changes or a sink attaches or detaches (§12.4), so callers
51
+ * re-check per emission rather than caching a boolean.
52
+ */
53
+ enabled(severity: SeverityNumber): boolean;
54
+
55
+ /**
56
+ * Emit a record. Never throws, under any condition, including sink failure —
57
+ * a sink failure is reported on the fallback diagnostic stream and counted,
58
+ * never propagated and never swallowed (§8.4).
59
+ */
60
+ log(
61
+ severity: SeverityNumber,
62
+ message: string,
63
+ attributes?: LogAttributesInput,
64
+ options?: LogOptions,
65
+ ): void;
66
+
67
+ /**
68
+ * A child logger whose bound attributes are merged into every record it emits.
69
+ * Record attributes override bound attributes. Binding is O(1) amortized: the
70
+ * merge happens once here, never per record.
71
+ */
72
+ with(attributes: LogAttributesInput): Logger;
73
+
74
+ /** Drain every attached sink. Bounded by the caller; see §10.5. */
75
+ flush(): Promise<void>;
76
+
77
+ trace(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
78
+ debug(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
79
+ info(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
80
+ warn(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
81
+ error(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
82
+ /**
83
+ * Emits at severity 21. Severity never implies control flow (§5, D5): `fatal`
84
+ * does **not** terminate the process, exit, or panic — it triggers an
85
+ * immediate flush, synchronous on every sink that supports it and best-effort
86
+ * on the rest (§10.5).
87
+ */
88
+ fatal(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
89
+ }
90
+
91
+ /** Resolve a {@link LogValuer} if the value is one, else pass it through. */
92
+ export function isLogValuer(value: unknown): value is LogValuer {
93
+ return (
94
+ typeof value === "object" &&
95
+ value !== null &&
96
+ typeof (value as LogValuer).toLogValue === "function"
97
+ );
98
+ }
99
+
100
+ /** A logger that discards everything. Used where a logger is structurally
101
+ * required before one is available, and by tests that assert silence. */
102
+ export const NOOP_LOGGER: Logger = {
103
+ enabled: () => false,
104
+ log: () => {},
105
+ with: () => NOOP_LOGGER,
106
+ flush: async () => {},
107
+ trace: () => {},
108
+ debug: () => {},
109
+ info: () => {},
110
+ warn: () => {},
111
+ error: () => {},
112
+ fatal: () => {},
113
+ };
114
+
115
+ /** The bound-attribute merge of {@link Logger.with}, exposed so a runtime's
116
+ * child-logger implementation and its conformance vectors share one definition
117
+ * of "record attributes win". */
118
+ export function mergeBoundAttributes(
119
+ bound: LogAttributes | undefined,
120
+ record: LogAttributes | undefined,
121
+ ): LogAttributes | undefined {
122
+ if (!bound) return record;
123
+ if (!record) return bound;
124
+ return { ...bound, ...record };
125
+ }
@@ -1,5 +1,7 @@
1
1
  import type { CancellationSource, InvokeContext, OpenSpan, OpenSpanOptions } from "./cancellation.js";
2
2
  import { ControllerContext } from "./controller-context.js";
3
+ import type { Logger } from "./logger.js";
4
+ import type { LoggingHost } from "./log-sink.js";
3
5
  import { ControllerPolicy } from "./controller-policy.js";
4
6
  import { EvaluationContext } from "./evaluation-context.js";
5
7
  import { ModuleContext } from "./module-context.js";
@@ -131,6 +133,28 @@ export interface ResourceContext extends ControllerContext {
131
133
  * manifests. Use this when you need the full kind surface area visible from
132
134
  * the module. */
133
135
  loadManifests(url: string): Promise<ResourceManifest[]>;
136
+ /**
137
+ * The structured logger for this resource — `kernel/specs/logging.md` §13.2.
138
+ *
139
+ * Ambient rather than a resource (D3), because it must work before any
140
+ * resource initializes. Records are automatically stamped with this
141
+ * resource's identity, its module, its import-alias scope, and the active
142
+ * dispatch span's trace and span ids — a controller never passes those.
143
+ *
144
+ * A controller emits diagnostics **only** through this. Writing to
145
+ * stdout/stderr for diagnostic purposes is forbidden; writing to stdout as
146
+ * *data* (as the `Console` module does) is a separate, legitimate concern and
147
+ * is unaffected.
148
+ */
149
+ readonly log: Logger;
150
+ /**
151
+ * Sink attach/detach and drop accounting — the surface a `Telo.Sink`
152
+ * controller needs and nothing else. §10.2 keeps the sink set open to the
153
+ * ecosystem, so a third-party sink module reaches the pipeline through this
154
+ * rather than through a kernel-internal import. Ordinary controllers use
155
+ * {@link log}.
156
+ */
157
+ readonly logging: LoggingHost;
134
158
  readonly moduleContext: ModuleContext;
135
159
  readonly env: Record<string, string | undefined>;
136
160
  readonly stdin: NodeJS.ReadableStream;
@@ -11,8 +11,26 @@ export type ResourceInstance<TInput = Record<string, any>, TOutput = any> = Part
11
11
  init?(ctx?: ResourceContext): Promise<void>;
12
12
  teardown?(): void | Promise<void>;
13
13
  snapshot?(): Record<string, any> | Promise<Record<string, any>>;
14
+ /**
15
+ * Teardown ordering hint. Instances tear down in ascending priority — a
16
+ * higher number means *later*. Default `0`; within one priority the base
17
+ * order (reverse init) is preserved.
18
+ *
19
+ * This exists because the base order is reverse *insertion* order, which the
20
+ * multi-pass init retry can perturb, so a resource that must reliably outlive
21
+ * the rest at shutdown cannot express that through the dependency graph. Log
22
+ * sinks set {@link TEARDOWN_LAST} so they flush after every resource that
23
+ * might log while shutting down — a generic mechanism, not a logging-specific
24
+ * carve-out in the teardown path.
25
+ */
26
+ teardownPriority?: number;
14
27
  };
15
28
 
29
+ /** Teardown-last priority (see {@link ResourceInstance.teardownPriority}). Log
30
+ * sinks use it so anything logging during its own teardown still reaches a live
31
+ * destination. */
32
+ export const TEARDOWN_LAST = 1000;
33
+
16
34
  /** The kind+name an instance was resolved from. */
17
35
  export interface RefIdentity {
18
36
  kind: string;