@telorun/kernel 0.30.2 → 0.32.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.
@@ -1,4 +1,4 @@
1
- import { executeInvokeStep, RuntimeError } from "@telorun/sdk";
1
+ import { executeInvokeStep, getRefIdentity, RuntimeError } from "@telorun/sdk";
2
2
  import type {
3
3
  BootTarget,
4
4
  ControllerPolicy,
@@ -432,6 +432,29 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
432
432
  return `${realModule}.${suffix}`;
433
433
  }
434
434
 
435
+ protected override resolveKindSafe(kind: string): string {
436
+ // `resolveKind` throws for unqualified / ungated kinds — an expected signal,
437
+ // not a failure: a capability probe that can't resolve falls back to the raw
438
+ // kind (lookup misses → treated as non-service → keeps its ALS scope).
439
+ try {
440
+ return this.resolveKind(kind);
441
+ } catch {
442
+ return kind;
443
+ }
444
+ }
445
+
446
+ protected override traceRootScope(): Record<string, unknown> {
447
+ // Mask secret values (keep keys so availability is visible, never the value).
448
+ const secrets: Record<string, unknown> = {};
449
+ for (const key of Object.keys(this._secrets)) secrets[key] = "[secret]";
450
+ return {
451
+ variables: this._variables,
452
+ secrets,
453
+ resources: this._resources,
454
+ ports: this._ports,
455
+ };
456
+ }
457
+
435
458
  private _rebuildContext(): void {
436
459
  this._context = {
437
460
  variables: this._variables,
@@ -473,26 +496,84 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
473
496
  await super.run(name, ctx);
474
497
  }
475
498
 
476
- async runTargets(ctx?: InvokeContext) {
499
+ async runTargets(ctx?: InvokeContext, appName?: string) {
500
+ // Open a trace span for the application's boot run so the app itself is a
501
+ // trace participant: every target dispatches under `targetCtx`, parenting to
502
+ // this span instead of becoming a detached root. The span is gated on tracing
503
+ // (zero cost otherwise) and rides the boot cancellation token unchanged.
504
+ const tracing = this.tracer?.enabled === true;
505
+ const appSpanId = tracing ? this.tracer!.next() : undefined;
506
+ // The boot run roots a fresh trace; targets inherit it via `targetCtx`.
507
+ const appTraceId = tracing ? this.tracer!.newTraceId() : undefined;
508
+ const appRootScope = tracing ? this.traceRootScope() : undefined;
509
+ const appLabel = appName ?? "application";
510
+ const appSpan = (
511
+ phase: "start" | "end",
512
+ outcome: "ok" | "failed" | "cancelled" | undefined,
513
+ ) =>
514
+ this.tracePayload(
515
+ "Telo.Application",
516
+ appLabel,
517
+ appSpanId,
518
+ undefined,
519
+ appTraceId,
520
+ "run",
521
+ phase,
522
+ outcome,
523
+ phase === "end" && appRootScope ? { context: appRootScope } : {},
524
+ );
525
+ const targetCtx: InvokeContext | undefined =
526
+ tracing && ctx
527
+ ? {
528
+ cancellation: ctx.cancellation,
529
+ invocationId: appSpanId,
530
+ parentInvocationId: undefined,
531
+ traceId: appTraceId,
532
+ }
533
+ : ctx;
534
+
477
535
  const steps: Record<string, unknown> = {};
478
536
  const stepCtx: InvokeStepContext = {
479
537
  expandValue: (value, context) => this.expandWith(value, context),
480
- invoke: (kind, name, inputs) => this.invoke(kind, name, inputs, ctx),
538
+ invoke: (kind, name, inputs) => this.invoke(kind, name, inputs, targetCtx),
481
539
  invokeResolved: (kind, name, instance, inputs) =>
482
- this.invokeResolved(kind, name, instance, inputs, ctx),
540
+ this.invokeResolved(kind, name, instance, inputs, targetCtx),
483
541
  resolveImportedInstance: (alias, name) => this.resolveImportedInstance(alias, name),
484
542
  };
485
- // Mirror the local-run gate: refuse a target reached after the boot run was
486
- // cancelled, then run the pre-resolved instance directly.
487
- const runResolvedInstance = async (inst: ResourceInstance, label: string) => {
488
- const token = ctx?.cancellation;
489
- if (token?.isCancelled) {
490
- throw new RuntimeError(
491
- "ERR_INVOKE_CANCELLED",
492
- `Run ${label} was cancelled${token.reason ? `: ${token.reason}` : ""}`,
543
+ // Route a pre-resolved boot target through the instrumented chokepoint
544
+ // (`runResolved`) instead of calling `instance.run()` directly, so it emits a
545
+ // run span nested under the app. Kind/name come from the `!ref` identity the
546
+ // kernel stamped at injection, falling back to a positional label.
547
+ const runResolvedInstance = async (inst: ResourceInstance, kind: string, name: string) =>
548
+ this.runResolved(kind, name, inst, targetCtx);
549
+
550
+ if (tracing) await this.emit(`${appLabel}.Running`, appSpan("start", undefined));
551
+ try {
552
+ await this.dispatchTargets(stepCtx, steps, runResolvedInstance, targetCtx);
553
+ if (tracing) await this.emit(`${appLabel}.Run`, appSpan("end", "ok"));
554
+ } catch (err) {
555
+ if (tracing) {
556
+ const cancelled = (err as { code?: unknown })?.code === "ERR_INVOKE_CANCELLED";
557
+ await this.emit(
558
+ `${appLabel}.${cancelled ? "RunCancelled" : "RunFailed"}`,
559
+ appSpan("end", cancelled ? "cancelled" : "failed"),
493
560
  );
494
561
  }
495
- await inst.run!(ctx);
562
+ throw err;
563
+ }
564
+ }
565
+
566
+ private async dispatchTargets(
567
+ stepCtx: InvokeStepContext,
568
+ steps: Record<string, unknown>,
569
+ runResolvedInstance: (inst: ResourceInstance, kind: string, name: string) => Promise<void>,
570
+ ctx?: InvokeContext,
571
+ ) {
572
+ // Recover the kind+name the kernel stamped onto a `!ref`-injected instance,
573
+ // so the run span is properly labelled; fall back to a positional name.
574
+ const idOf = (inst: ResourceInstance, fallbackName: string) => {
575
+ const id = getRefIdentity(inst as object);
576
+ return { kind: id?.kind ?? "", name: id?.name ?? fallbackName };
496
577
  };
497
578
  for (let i = 0; i < this.targets.length; i++) {
498
579
  const target = this.targets[i]!;
@@ -524,7 +605,8 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
524
605
  // Phase 5 injection may have replaced the ref slot with the live
525
606
  // instance; run it directly.
526
607
  if (isRunnableInstance(ref)) {
527
- await runResolvedInstance(ref, `target[${i}]`);
608
+ const id = idOf(ref, `target[${i}]`);
609
+ await runResolvedInstance(ref, id.kind, id.name);
528
610
  } else {
529
611
  const r =
530
612
  ref && typeof ref === "object"
@@ -537,7 +619,7 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
537
619
  `Boot target '${r.alias}.${r.name}' is not a runnable exported instance`,
538
620
  );
539
621
  }
540
- await runResolvedInstance(inst, `${r.alias}.${r.name}`);
622
+ await runResolvedInstance(inst, getRefIdentity(inst as object)?.kind ?? "", r.name);
541
623
  } else {
542
624
  await this.run(typeof ref === "string" ? ref : r!.name, ctx);
543
625
  }
@@ -550,7 +632,8 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
550
632
  // so the common runtime shape here is a pre-resolved ResourceInstance;
551
633
  // fall back to the structural ref forms when injection left them in place.
552
634
  if (isRunnableInstance(target)) {
553
- await runResolvedInstance(target, `target[${i}]`);
635
+ const id = idOf(target, `target[${i}]`);
636
+ await runResolvedInstance(target, id.kind, id.name);
554
637
  continue;
555
638
  }
556
639
  const bare = target as { name?: unknown; alias?: unknown };
@@ -561,7 +644,7 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
561
644
  `Boot target '${bare.alias}.${bare.name}' is not a runnable exported instance`,
562
645
  );
563
646
  }
564
- await runResolvedInstance(inst, `${bare.alias}.${bare.name}`);
647
+ await runResolvedInstance(inst, getRefIdentity(inst as object)?.kind ?? "", bare.name);
565
648
  continue;
566
649
  }
567
650
  if (typeof bare.name === "string") {
@@ -12,6 +12,8 @@ import {
12
12
  type InvokeContext,
13
13
  type LoadOptions,
14
14
  type ModuleContext,
15
+ type OpenSpan,
16
+ type OpenSpanOptions,
15
17
  type ParsedArgs,
16
18
  type TypeRule,
17
19
  } from "@telorun/sdk";
@@ -155,6 +157,10 @@ export class ResourceContextImpl implements ResourceContext {
155
157
  return createCancellationSource();
156
158
  }
157
159
 
160
+ openSpan(base: InvokeContext | undefined, opts: OpenSpanOptions): Promise<OpenSpan> {
161
+ return this.moduleContext.openSpan(base, opts);
162
+ }
163
+
158
164
  invoke<TInputs>(kind: string, name: string, inputs: TInputs): Promise<any> {
159
165
  return this.moduleContext.invoke(kind, name, inputs);
160
166
  }
@@ -223,7 +229,7 @@ export class ResourceContextImpl implements ResourceContext {
223
229
  // analyzer's field-map walker descends `oneOf`/`anyOf` variant
224
230
  // properties but intentionally early-returns on `$ref` (see
225
231
  // `analyzer/nodejs/src/reference-field-map.ts`). Enabling the `$ref`
226
- // descent regresses the kernel's `<Kind>.<Name>.Invoked` event
232
+ // descent regresses the kernel's `<name>.Invoked` event
227
233
  // emission for kinds (notably `Run.Sequence`) whose controllers
228
234
  // call `instance.invoke()` directly on Phase-5-injected instances;
229
235
  // the walker fix needs to land together with routing those callers
package/src/tracing.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import type { Tracer } from "@telorun/sdk";
3
+
4
+ /**
5
+ * The kernel's invocation tracer: a monotonic counter plus the `enabled` gate a
6
+ * debug consumer flips on attach (`Kernel.setTracing`). One instance per kernel,
7
+ * shared by reference across the whole context tree, so invocation ids are unique
8
+ * within the run and `enabled` toggles everywhere at once.
9
+ *
10
+ * When `enabled` is `false` (the default), `invoke` skips id minting and the extra
11
+ * ALS scope entirely — tracing costs nothing until someone is watching.
12
+ */
13
+ export class KernelTracer implements Tracer {
14
+ enabled = false;
15
+ #next = 0;
16
+
17
+ next(): number {
18
+ this.#next += 1;
19
+ return this.#next;
20
+ }
21
+
22
+ /** A fresh OTel-compatible 16-byte hex trace id. Globally unique, so a trace
23
+ * stays identifiable once it crosses process boundaries. */
24
+ newTraceId(): string {
25
+ return randomBytes(16).toString("hex");
26
+ }
27
+ }