@codemation/host 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/{AppConfigFactory-CqKWXqOm.js → AppConfigFactory-BPp02HMv.js} +82 -5
  3. package/dist/{AppConfigFactory-CqKWXqOm.js.map → AppConfigFactory-BPp02HMv.js.map} +1 -1
  4. package/dist/{AppConfigFactory-CK28UPK0.d.ts → AppConfigFactory-Dq7ttwQ_.d.ts} +6001 -144
  5. package/dist/{AppContainerFactory-CcSGFNLW.js → AppContainerFactory-D9je1sSV.js} +2604 -155
  6. package/dist/AppContainerFactory-D9je1sSV.js.map +1 -0
  7. package/dist/{CodemationAppContext-KqDoeHqN.d.ts → CodemationAppContext-P7P-xZhQ.d.ts} +2 -2
  8. package/dist/{CodemationAuthoring.types-BP6Inucu.d.ts → CodemationAuthoring.types-OMYu7vKP.d.ts} +3 -3
  9. package/dist/{CodemationConfigNormalizer-DIAE0VHw.d.ts → CodemationConfigNormalizer-BCtBrJDe.d.ts} +2 -2
  10. package/dist/{CodemationConsumerConfigLoader-nj9kTmfJ.d.ts → CodemationConsumerConfigLoader-evvw4b_a.d.ts} +2 -2
  11. package/dist/CodemationPluginListMerger-PSTtEQjC.d.ts +674 -0
  12. package/dist/{CredentialServices-BFQD_VN1.d.ts → CredentialServices-0Hk8RFY1.d.ts} +3 -3
  13. package/dist/{CredentialServices-BPKUF8Xs.js → CredentialServices-BNBMFOPt.js} +6 -1
  14. package/dist/CredentialServices-BNBMFOPt.js.map +1 -0
  15. package/dist/{PublicFrontendBootstrapFactory-DTA1iDo0.d.ts → PublicFrontendBootstrapFactory-D0_ds7nS.d.ts} +2 -2
  16. package/dist/authoring.d.ts +3 -3
  17. package/dist/consumer.d.ts +4 -4
  18. package/dist/credentials.d.ts +3 -3
  19. package/dist/credentials.js +1 -1
  20. package/dist/devServerSidecar.d.ts +1 -1
  21. package/dist/{index-Dd6BrWyH.d.ts → index-CeS2saCe.d.ts} +105 -2
  22. package/dist/index.d.ts +12 -11
  23. package/dist/index.js +5 -5
  24. package/dist/nextServer.d.ts +8 -58
  25. package/dist/nextServer.js +6 -100
  26. package/dist/{persistenceServer-DKbFDxoS.js → persistenceServer-CA0_q0D7.js} +2 -2
  27. package/dist/{persistenceServer-DKbFDxoS.js.map → persistenceServer-CA0_q0D7.js.map} +1 -1
  28. package/dist/{persistenceServer-Y-u7lV7f.d.ts → persistenceServer-CJeu1STC.d.ts} +2 -2
  29. package/dist/persistenceServer.d.ts +5 -5
  30. package/dist/persistenceServer.js +2 -2
  31. package/dist/{server-CFpgKuVE.js → server-C_ZIEOTY.js} +4 -4
  32. package/dist/{server-CFpgKuVE.js.map → server-C_ZIEOTY.js.map} +1 -1
  33. package/dist/{server-axppTMgo.d.ts → server-Clvg5x1w.d.ts} +11 -5
  34. package/dist/server.d.ts +8 -8
  35. package/dist/server.js +5 -5
  36. package/package.json +6 -5
  37. package/prisma/migrations/20260414120000_telemetry_foundation/migration.sql +112 -0
  38. package/prisma/migrations/20260414153000_telemetry_retention_metrics_refactor/migration.sql +239 -0
  39. package/prisma/migrations.sqlite/20260414120000_telemetry_foundation/migration.sql +103 -0
  40. package/prisma/migrations.sqlite/20260414153000_telemetry_retention_metrics_refactor/migration.sql +540 -0
  41. package/prisma/schema.postgresql.prisma +100 -1
  42. package/prisma/schema.sqlite.prisma +100 -1
  43. package/scripts/generate-prisma-clients.mjs +89 -1
  44. package/src/application/contracts/TelemetryDashboardContracts.ts +113 -0
  45. package/src/application/contracts/TelemetryRunTraceContracts.ts +13 -0
  46. package/src/application/cost/FrameworkCostCatalogEntries.ts +126 -0
  47. package/src/application/queries/GetTelemetryDashboardDimensionsQuery.ts +11 -0
  48. package/src/application/queries/GetTelemetryDashboardDimensionsQueryHandler.ts +20 -0
  49. package/src/application/queries/GetTelemetryDashboardRunsQuery.ts +11 -0
  50. package/src/application/queries/GetTelemetryDashboardRunsQueryHandler.ts +20 -0
  51. package/src/application/queries/GetTelemetryDashboardSummaryQuery.ts +11 -0
  52. package/src/application/queries/GetTelemetryDashboardSummaryQueryHandler.ts +29 -0
  53. package/src/application/queries/GetTelemetryDashboardTimeseriesQuery.ts +11 -0
  54. package/src/application/queries/GetTelemetryDashboardTimeseriesQueryHandler.ts +36 -0
  55. package/src/application/queries/GetTelemetryRunTraceQuery.ts +8 -0
  56. package/src/application/queries/GetTelemetryRunTraceQueryHandler.ts +20 -0
  57. package/src/application/queries/WorkflowQueryHandlers.ts +5 -0
  58. package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +71 -26
  59. package/src/application/telemetry/CompositeTelemetryExporter.ts +13 -0
  60. package/src/application/telemetry/LazyExecutionTelemetryFactory.ts +21 -0
  61. package/src/application/telemetry/NoOpTelemetryExporter.ts +7 -0
  62. package/src/application/telemetry/OtelExecutionTelemetry.types.ts +41 -0
  63. package/src/application/telemetry/OtelExecutionTelemetryFactory.ts +56 -0
  64. package/src/application/telemetry/OtelIdentityFactory.ts +41 -0
  65. package/src/application/telemetry/RunEventBusTelemetryReporter.ts +188 -0
  66. package/src/application/telemetry/StoredExecutionTelemetry.ts +56 -0
  67. package/src/application/telemetry/StoredNodeExecutionTelemetry.ts +35 -0
  68. package/src/application/telemetry/StoredTelemetrySpanScope.ts +188 -0
  69. package/src/application/telemetry/TelemetryEnricherChain.ts +85 -0
  70. package/src/application/telemetry/TelemetryPrivacyPolicy.ts +19 -0
  71. package/src/application/telemetry/TelemetryQueryService.ts +815 -0
  72. package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +40 -0
  73. package/src/applicationTokens.ts +18 -0
  74. package/src/bootstrap/AppContainerFactory.ts +124 -1
  75. package/src/bootstrap/AppContainerLifecycle.ts +8 -0
  76. package/src/bootstrap/runtime/FrontendRuntime.ts +8 -0
  77. package/src/bootstrap/runtime/WorkerRuntime.ts +8 -0
  78. package/src/domain/runs/WorkflowRunRepository.ts +3 -1
  79. package/src/domain/telemetry/TelemetryContracts.ts +197 -0
  80. package/src/infrastructure/persistence/InMemoryRunTraceContextRepository.ts +56 -0
  81. package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +56 -0
  82. package/src/infrastructure/persistence/InMemoryTelemetryMetricPointStore.ts +97 -0
  83. package/src/infrastructure/persistence/InMemoryTelemetrySpanStore.ts +113 -0
  84. package/src/infrastructure/persistence/InMemoryWorkflowRunRepository.ts +4 -2
  85. package/src/infrastructure/persistence/PrismaRunTraceContextRepository.ts +92 -0
  86. package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +125 -0
  87. package/src/infrastructure/persistence/PrismaTelemetryMetricPointStore.ts +134 -0
  88. package/src/infrastructure/persistence/PrismaTelemetrySpanStore.ts +166 -0
  89. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +20 -7
  90. package/src/infrastructure/persistence/generated/prisma-postgresql-client/edge.js +85 -4
  91. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index-browser.js +81 -0
  92. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.d.ts +6488 -102
  93. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.js +85 -4
  94. package/src/infrastructure/persistence/generated/prisma-postgresql-client/package.json +1 -1
  95. package/src/infrastructure/persistence/generated/prisma-postgresql-client/schema.prisma +100 -0
  96. package/src/infrastructure/persistence/generated/prisma-sqlite-client/edge.js +85 -4
  97. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index-browser.js +81 -0
  98. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.d.ts +6476 -98
  99. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.js +85 -4
  100. package/src/infrastructure/persistence/generated/prisma-sqlite-client/package.json +1 -1
  101. package/src/infrastructure/persistence/generated/prisma-sqlite-client/schema.prisma +100 -0
  102. package/src/presentation/http/ApiPaths.ts +22 -0
  103. package/src/presentation/http/hono/registrars/TelemetryHonoApiRouteRegistrar.ts +19 -0
  104. package/src/presentation/http/routeHandlers/TelemetryDashboardRequestError.ts +1 -0
  105. package/src/presentation/http/routeHandlers/TelemetryHttpRouteHandler.ts +181 -0
  106. package/dist/AppContainerFactory-CcSGFNLW.js.map +0 -1
  107. package/dist/CodemationPluginListMerger-QvUa2SIt.d.ts +0 -357
  108. package/dist/CredentialServices-BPKUF8Xs.js.map +0 -1
  109. package/dist/nextServer.js.map +0 -1
@@ -5,15 +5,23 @@ import type { Logger } from "../logging/Logger";
5
5
  import { ApplicationTokens } from "../../applicationTokens";
6
6
  import type { AppConfig } from "../../presentation/config/AppConfig";
7
7
  import type { WorkflowRunRepository } from "../../domain/runs/WorkflowRunRepository";
8
+ import type {
9
+ TelemetryArtifactStore,
10
+ TelemetryMetricPointStore,
11
+ TelemetrySpanStore,
12
+ } from "../../domain/telemetry/TelemetryContracts";
8
13
  import { ServerLoggerFactory } from "../../infrastructure/logging/ServerLoggerFactory";
9
14
 
10
15
  /**
11
16
  * Periodically deletes terminal workflow runs whose age exceeds the effective retention
12
17
  * (`policySnapshot.retentionSeconds` or `CODEMATION_RUN_RETENTION_DEFAULT_SECONDS`),
13
- * and removes binary blobs referenced from run state via {@link BinaryStorage}.
18
+ * removes binary blobs referenced from run state via {@link BinaryStorage}, and
19
+ * independently prunes spans, artifacts, and metric points once their own retention
20
+ * timestamps expire.
14
21
  */
15
22
  @injectable()
16
23
  export class WorkflowRunRetentionPruneScheduler {
24
+ private static readonly defaultIntervalMs = 60 * 60 * 1_000;
17
25
  private timer: ReturnType<typeof setInterval> | undefined;
18
26
  private readonly logger: Logger;
19
27
 
@@ -21,6 +29,10 @@ export class WorkflowRunRetentionPruneScheduler {
21
29
  @inject(ApplicationTokens.Clock) private readonly clock: Clock,
22
30
  @inject(ApplicationTokens.WorkflowRunRepository) private readonly runs: WorkflowRunRepository,
23
31
  @inject(CoreTokens.BinaryStorage) private readonly binaryStorage: BinaryStorage,
32
+ @inject(ApplicationTokens.TelemetrySpanStore) private readonly telemetrySpanStore: TelemetrySpanStore,
33
+ @inject(ApplicationTokens.TelemetryArtifactStore) private readonly telemetryArtifactStore: TelemetryArtifactStore,
34
+ @inject(ApplicationTokens.TelemetryMetricPointStore)
35
+ private readonly telemetryMetricPointStore: TelemetryMetricPointStore,
24
36
  @inject(ApplicationTokens.AppConfig) private readonly appConfig: AppConfig,
25
37
  @inject(ServerLoggerFactory) loggerFactory: ServerLoggerFactory,
26
38
  ) {
@@ -28,17 +40,21 @@ export class WorkflowRunRetentionPruneScheduler {
28
40
  }
29
41
 
30
42
  start(): void {
31
- if (this.appConfig.env.CODEMATION_RUN_PRUNE_ENABLED === "false") {
43
+ if (
44
+ this.appConfig.env.CODEMATION_RUN_PRUNE_ENABLED === "false" &&
45
+ this.appConfig.env.CODEMATION_TELEMETRY_PRUNE_ENABLED === "false"
46
+ ) {
32
47
  return;
33
48
  }
34
49
  if (this.timer) {
35
50
  return;
36
51
  }
37
- const intervalMs = Number(this.appConfig.env.CODEMATION_RUN_PRUNE_INTERVAL_MS ?? 60_000);
52
+ const intervalMs = Number(
53
+ this.appConfig.env.CODEMATION_RUN_PRUNE_INTERVAL_MS ?? WorkflowRunRetentionPruneScheduler.defaultIntervalMs,
54
+ );
55
+ void this.runScheduledTick();
38
56
  this.timer = setInterval(() => {
39
- void this.runOnce().catch((error: unknown) => {
40
- this.logger.warn(`Run retention prune tick failed: ${error instanceof Error ? error.message : String(error)}`);
41
- });
57
+ void this.runScheduledTick();
42
58
  }, intervalMs);
43
59
  }
44
60
 
@@ -51,35 +67,64 @@ export class WorkflowRunRetentionPruneScheduler {
51
67
 
52
68
  /** Exposed for tests; production path is the interval started by {@link start}. */
53
69
  async runOnce(): Promise<void> {
54
- this.logger.debug("Run retention prune: starting check");
55
70
  const defaultRetentionSec = Number(this.appConfig.env.CODEMATION_RUN_RETENTION_DEFAULT_SECONDS ?? 86_400);
56
- const beforeIso = new Date(this.clock.now().getTime() - defaultRetentionSec * 1000).toISOString();
57
- const summaries = await this.runs.listRunsOlderThan?.({ beforeIso, limit: 500 });
58
- const candidates =
59
- summaries ??
60
- (await this.runs.listRuns({ limit: 500 })).filter(
61
- (summary) => summary.status === "completed" || summary.status === "failed",
62
- );
63
-
71
+ const nowIso = this.clock.now().toISOString();
64
72
  let foundCount = 0;
65
73
  let prunedCount = 0;
66
- for (const candidate of candidates) {
67
- const runId = candidate.runId as RunId;
68
- const workflowId = candidate.workflowId as WorkflowId;
69
- foundCount += 1;
74
+ if (this.appConfig.env.CODEMATION_RUN_PRUNE_ENABLED !== "false") {
75
+ const summaries = await this.runs.listRunsOlderThan?.({
76
+ nowIso,
77
+ defaultRetentionSeconds: defaultRetentionSec,
78
+ limit: 500,
79
+ });
80
+ const candidates =
81
+ summaries ??
82
+ (await this.runs.listRuns({ limit: 500 })).filter(
83
+ (summary) => summary.status === "completed" || summary.status === "failed",
84
+ );
85
+ for (const candidate of candidates) {
86
+ const runId = candidate.runId as RunId;
87
+ const workflowId = candidate.workflowId as WorkflowId;
88
+ foundCount += 1;
70
89
 
71
- const storageKeys =
72
- (await this.runs.listBinaryStorageKeys?.(runId)) ?? (await this.loadStorageKeysFromRunStateFallback(runId));
73
- for (const key of storageKeys) {
74
- await this.binaryStorage.delete(key);
90
+ const storageKeys =
91
+ (await this.runs.listBinaryStorageKeys?.(runId)) ?? (await this.loadStorageKeysFromRunStateFallback(runId));
92
+ for (const key of storageKeys) {
93
+ await this.binaryStorage.delete(key);
94
+ }
95
+ await this.runs.deleteRun(runId);
96
+ prunedCount += 1;
97
+ this.logger.debug(`Run retention prune: pruned run ${runId} for workflow ${workflowId}`);
75
98
  }
76
- await this.runs.deleteRun(runId);
77
- prunedCount += 1;
78
- this.logger.debug(`Run retention prune: pruned run ${runId} for workflow ${workflowId}`);
79
99
  }
80
100
 
101
+ let prunedSpanCount = 0;
102
+ let prunedArtifactCount = 0;
103
+ let prunedMetricCount = 0;
104
+ if (this.appConfig.env.CODEMATION_TELEMETRY_PRUNE_ENABLED !== "false") {
105
+ const telemetryLimit = Number(this.appConfig.env.CODEMATION_TELEMETRY_PRUNE_LIMIT ?? 2_000);
106
+ prunedSpanCount = await this.telemetrySpanStore.pruneExpired({ nowIso, limit: telemetryLimit });
107
+ prunedArtifactCount = await this.telemetryArtifactStore.pruneExpired({ nowIso, limit: telemetryLimit });
108
+ prunedMetricCount = await this.telemetryMetricPointStore.pruneExpired({ nowIso, limit: telemetryLimit });
109
+ }
110
+
111
+ const totalPruned = foundCount + prunedCount + prunedSpanCount + prunedArtifactCount + prunedMetricCount;
112
+ if (totalPruned === 0) {
113
+ return;
114
+ }
81
115
  this.logger.info(`Run retention prune: found ${foundCount} run(s) to prune`);
82
116
  this.logger.info(`Run retention prune: pruned ${prunedCount} run(s)`);
117
+ this.logger.info(`Run retention prune: pruned ${prunedSpanCount} telemetry span(s)`);
118
+ this.logger.info(`Run retention prune: pruned ${prunedArtifactCount} telemetry artifact(s)`);
119
+ this.logger.info(`Run retention prune: pruned ${prunedMetricCount} telemetry metric point(s)`);
120
+ }
121
+
122
+ private async runScheduledTick(): Promise<void> {
123
+ try {
124
+ await this.runOnce();
125
+ } catch (error) {
126
+ this.logger.warn(`Run retention prune tick failed: ${error instanceof Error ? error.message : String(error)}`);
127
+ }
83
128
  }
84
129
 
85
130
  private async loadStorageKeysFromRunStateFallback(runId: RunId): Promise<ReadonlyArray<string>> {
@@ -0,0 +1,13 @@
1
+ import { injectable } from "@codemation/core";
2
+ import type { TelemetryExporter, TelemetrySpanRecord } from "../../domain/telemetry/TelemetryContracts";
3
+
4
+ @injectable()
5
+ export class CompositeTelemetryExporter implements TelemetryExporter {
6
+ constructor(private readonly exporters: ReadonlyArray<TelemetryExporter>) {}
7
+
8
+ async exportSpans(spans: ReadonlyArray<TelemetrySpanRecord>): Promise<void> {
9
+ for (const exporter of this.exporters) {
10
+ await exporter.exportSpans(spans);
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,21 @@
1
+ import type {
2
+ ExecutionTelemetry,
3
+ ExecutionTelemetryFactory,
4
+ ParentExecutionRef,
5
+ PersistedRunPolicySnapshot,
6
+ } from "@codemation/core";
7
+
8
+ export class LazyExecutionTelemetryFactory implements ExecutionTelemetryFactory {
9
+ constructor(private readonly resolveFactory: () => ExecutionTelemetryFactory) {}
10
+
11
+ create(
12
+ args: Readonly<{
13
+ runId: string;
14
+ workflowId: string;
15
+ parent?: ParentExecutionRef;
16
+ policySnapshot?: PersistedRunPolicySnapshot;
17
+ }>,
18
+ ): ExecutionTelemetry {
19
+ return this.resolveFactory().create(args);
20
+ }
21
+ }
@@ -0,0 +1,7 @@
1
+ import { injectable } from "@codemation/core";
2
+ import type { TelemetryExporter, TelemetrySpanRecord } from "../../domain/telemetry/TelemetryContracts";
3
+
4
+ @injectable()
5
+ export class NoOpTelemetryExporter implements TelemetryExporter {
6
+ async exportSpans(_: ReadonlyArray<TelemetrySpanRecord>): Promise<void> {}
7
+ }
@@ -0,0 +1,41 @@
1
+ import type { PersistedRunPolicySnapshot, TelemetryAttributes } from "@codemation/core";
2
+ import type {
3
+ RunTraceContextRepository,
4
+ TelemetryArtifactStore,
5
+ TelemetryMetricPointStore,
6
+ TelemetrySpanStore,
7
+ } from "../../domain/telemetry/TelemetryContracts";
8
+ import { OtelIdentityFactory } from "./OtelIdentityFactory";
9
+ import { TelemetryEnricherChain } from "./TelemetryEnricherChain";
10
+ import { TelemetryPrivacyPolicy } from "./TelemetryPrivacyPolicy";
11
+ import { TelemetryRetentionTimestampFactory } from "./TelemetryRetentionTimestampFactory";
12
+
13
+ export type StoredExecutionTelemetryDeps = Readonly<{
14
+ traceId: string;
15
+ rootSpanId: string;
16
+ runId: string;
17
+ workflowId: string;
18
+ policySnapshot?: PersistedRunPolicySnapshot;
19
+ runTraceContextRepository: RunTraceContextRepository;
20
+ telemetrySpanStore: TelemetrySpanStore;
21
+ telemetryArtifactStore: TelemetryArtifactStore;
22
+ telemetryMetricPointStore: TelemetryMetricPointStore;
23
+ telemetryEnricherChain: TelemetryEnricherChain;
24
+ telemetryPrivacyPolicy: TelemetryPrivacyPolicy;
25
+ telemetryRetentionTimestampFactory: TelemetryRetentionTimestampFactory;
26
+ otelIdentityFactory: OtelIdentityFactory;
27
+ }>;
28
+
29
+ export type StoredSpanScopeArgs = StoredExecutionTelemetryDeps &
30
+ Readonly<{
31
+ spanId: string;
32
+ parentSpanId?: string;
33
+ nodeId?: string;
34
+ activationId?: string;
35
+ spanName: string;
36
+ spanKind: "internal" | "client";
37
+ initialAttributes?: TelemetryAttributes;
38
+ initialStartTime?: Date;
39
+ connectionInvocationId?: string;
40
+ modelName?: string;
41
+ }>;
@@ -0,0 +1,56 @@
1
+ import type { ExecutionTelemetry, ExecutionTelemetryFactory, PersistedRunPolicySnapshot } from "@codemation/core";
2
+ import { inject, injectable } from "@codemation/core";
3
+ import { ApplicationTokens } from "../../applicationTokens";
4
+ import type {
5
+ RunTraceContextRepository,
6
+ TelemetryArtifactStore,
7
+ TelemetryMetricPointStore,
8
+ TelemetrySpanStore,
9
+ } from "../../domain/telemetry/TelemetryContracts";
10
+ import { OtelIdentityFactory } from "./OtelIdentityFactory";
11
+ import { StoredExecutionTelemetry } from "./StoredExecutionTelemetry";
12
+ import { TelemetryEnricherChain } from "./TelemetryEnricherChain";
13
+ import { TelemetryPrivacyPolicy } from "./TelemetryPrivacyPolicy";
14
+ import { TelemetryRetentionTimestampFactory } from "./TelemetryRetentionTimestampFactory";
15
+
16
+ @injectable()
17
+ export class OtelExecutionTelemetryFactory implements ExecutionTelemetryFactory {
18
+ constructor(
19
+ @inject(ApplicationTokens.RunTraceContextRepository)
20
+ private readonly runTraceContextRepository: RunTraceContextRepository,
21
+ @inject(ApplicationTokens.TelemetrySpanStore)
22
+ private readonly telemetrySpanStore: TelemetrySpanStore,
23
+ @inject(ApplicationTokens.TelemetryArtifactStore)
24
+ private readonly telemetryArtifactStore: TelemetryArtifactStore,
25
+ @inject(ApplicationTokens.TelemetryMetricPointStore)
26
+ private readonly telemetryMetricPointStore: TelemetryMetricPointStore,
27
+ @inject(TelemetryEnricherChain)
28
+ private readonly telemetryEnricherChain: TelemetryEnricherChain,
29
+ @inject(TelemetryPrivacyPolicy)
30
+ private readonly telemetryPrivacyPolicy: TelemetryPrivacyPolicy,
31
+ @inject(TelemetryRetentionTimestampFactory)
32
+ private readonly telemetryRetentionTimestampFactory: TelemetryRetentionTimestampFactory,
33
+ @inject(OtelIdentityFactory)
34
+ private readonly otelIdentityFactory: OtelIdentityFactory,
35
+ ) {}
36
+
37
+ create(
38
+ args: Readonly<{ runId: string; workflowId: string; policySnapshot?: PersistedRunPolicySnapshot }>,
39
+ ): ExecutionTelemetry {
40
+ return new StoredExecutionTelemetry({
41
+ traceId: this.otelIdentityFactory.createTraceId(args.runId),
42
+ rootSpanId: this.otelIdentityFactory.createRootSpanId(args.runId),
43
+ runId: args.runId,
44
+ workflowId: args.workflowId,
45
+ policySnapshot: args.policySnapshot,
46
+ runTraceContextRepository: this.runTraceContextRepository,
47
+ telemetrySpanStore: this.telemetrySpanStore,
48
+ telemetryArtifactStore: this.telemetryArtifactStore,
49
+ telemetryMetricPointStore: this.telemetryMetricPointStore,
50
+ telemetryEnricherChain: this.telemetryEnricherChain,
51
+ telemetryPrivacyPolicy: this.telemetryPrivacyPolicy,
52
+ telemetryRetentionTimestampFactory: this.telemetryRetentionTimestampFactory,
53
+ otelIdentityFactory: this.otelIdentityFactory,
54
+ });
55
+ }
56
+ }
@@ -0,0 +1,41 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { injectable } from "@codemation/core";
3
+
4
+ @injectable()
5
+ export class OtelIdentityFactory {
6
+ createTraceId(runId: string): string {
7
+ return this.hashToHex(runId, 32);
8
+ }
9
+
10
+ createRootSpanId(runId: string): string {
11
+ return this.hashToHex(`run:${runId}`, 16);
12
+ }
13
+
14
+ createNodeSpanId(activationId: string): string {
15
+ return this.hashToHex(`activation:${activationId}`, 16);
16
+ }
17
+
18
+ createConnectionInvocationSpanId(invocationId: string): string {
19
+ return this.hashToHex(`invocation:${invocationId}`, 16);
20
+ }
21
+
22
+ createArtifactId(): string {
23
+ return randomBytes(16).toString("hex");
24
+ }
25
+
26
+ createEphemeralSpanId(): string {
27
+ return randomBytes(8).toString("hex");
28
+ }
29
+
30
+ private hashToHex(value: string, length: number): string {
31
+ const hex = createHash("sha256").update(value).digest("hex").slice(0, length);
32
+ return this.ensureNonZeroHex(hex, length);
33
+ }
34
+
35
+ private ensureNonZeroHex(hex: string, length: number): string {
36
+ if (!/^0+$/.test(hex)) {
37
+ return hex;
38
+ }
39
+ return `${"0".repeat(Math.max(0, length - 1))}1`;
40
+ }
41
+ }
@@ -0,0 +1,188 @@
1
+ import type { RunEvent, RunEventBus, RunEventSubscription } from "@codemation/core";
2
+ import { CoreTokens, inject, injectable } from "@codemation/core";
3
+ import type {
4
+ RunTraceContextRepository,
5
+ TelemetrySpanStore,
6
+ TelemetrySpanUpsert,
7
+ } from "../../domain/telemetry/TelemetryContracts";
8
+ import { ApplicationTokens } from "../../applicationTokens";
9
+ import type { WorkflowRunRepository } from "../../domain/runs/WorkflowRunRepository";
10
+ import { OtelIdentityFactory } from "./OtelIdentityFactory";
11
+ import { TelemetryEnricherChain } from "./TelemetryEnricherChain";
12
+ import { TelemetryRetentionTimestampFactory } from "./TelemetryRetentionTimestampFactory";
13
+
14
+ @injectable()
15
+ export class RunEventBusTelemetryReporter {
16
+ private subscription: RunEventSubscription | null = null;
17
+
18
+ constructor(
19
+ @inject(CoreTokens.RunEventBus)
20
+ private readonly runEventBus: RunEventBus,
21
+ @inject(ApplicationTokens.RunTraceContextRepository)
22
+ private readonly runTraceContextRepository: RunTraceContextRepository,
23
+ @inject(ApplicationTokens.WorkflowRunRepository)
24
+ private readonly workflowRunRepository: WorkflowRunRepository,
25
+ @inject(ApplicationTokens.TelemetrySpanStore)
26
+ private readonly telemetrySpanStore: TelemetrySpanStore,
27
+ @inject(TelemetryEnricherChain)
28
+ private readonly telemetryEnricherChain: TelemetryEnricherChain,
29
+ @inject(TelemetryRetentionTimestampFactory)
30
+ private readonly telemetryRetentionTimestampFactory: TelemetryRetentionTimestampFactory,
31
+ @inject(OtelIdentityFactory)
32
+ private readonly otelIdentityFactory: OtelIdentityFactory,
33
+ ) {}
34
+
35
+ async start(): Promise<void> {
36
+ if (this.subscription) {
37
+ return;
38
+ }
39
+ this.subscription = await this.runEventBus.subscribe(async (event) => {
40
+ try {
41
+ await this.handleEvent(event);
42
+ } catch {
43
+ // Telemetry must remain best-effort so workflow execution does not fail on observer persistence races.
44
+ }
45
+ });
46
+ }
47
+
48
+ async stop(): Promise<void> {
49
+ if (!this.subscription) {
50
+ return;
51
+ }
52
+ await this.subscription.close();
53
+ this.subscription = null;
54
+ }
55
+
56
+ private async handleEvent(event: RunEvent): Promise<void> {
57
+ switch (event.kind) {
58
+ case "runCreated":
59
+ await this.handleRunCreated(event);
60
+ return;
61
+ case "runSaved":
62
+ await this.handleRunSaved(event);
63
+ return;
64
+ case "nodeQueued":
65
+ case "nodeStarted":
66
+ case "nodeCompleted":
67
+ case "nodeFailed":
68
+ await this.handleNodeSnapshot(event);
69
+ return;
70
+ }
71
+ }
72
+
73
+ private async handleRunCreated(event: Extract<RunEvent, { kind: "runCreated" }>): Promise<void> {
74
+ const trace = await this.runTraceContextRepository.getOrCreate({
75
+ runId: event.runId,
76
+ workflowId: event.workflowId,
77
+ serviceName: "codemation.workflow",
78
+ });
79
+ const policySnapshot = await this.loadPolicySnapshot(event.runId);
80
+ await this.runTraceContextRepository.upsertExpiry({
81
+ runId: event.runId,
82
+ expiresAt: this.telemetryRetentionTimestampFactory.createTraceContextExpiry(policySnapshot, new Date(event.at)),
83
+ });
84
+ const enrichment = await this.telemetryEnricherChain.enrichRun(event.workflowId);
85
+ await this.telemetrySpanStore.upsert({
86
+ traceId: trace.traceId,
87
+ spanId: trace.rootSpanId,
88
+ runId: event.runId,
89
+ workflowId: event.workflowId,
90
+ name: "workflow.run",
91
+ kind: "internal",
92
+ status: "running",
93
+ startTime: event.at,
94
+ workflowFolder: enrichment.workflowFolder,
95
+ retentionExpiresAt: this.telemetryRetentionTimestampFactory.createSpanExpiry(policySnapshot, new Date(event.at)),
96
+ });
97
+ }
98
+
99
+ private async handleRunSaved(event: Extract<RunEvent, { kind: "runSaved" }>): Promise<void> {
100
+ const trace = await this.runTraceContextRepository.getOrCreate({
101
+ runId: event.runId,
102
+ workflowId: event.workflowId,
103
+ serviceName: "codemation.workflow",
104
+ });
105
+ const policySnapshot = await this.loadPolicySnapshot(event.runId);
106
+ const observedAt = new Date(event.state.finishedAt ?? event.at);
107
+ await this.runTraceContextRepository.upsertExpiry({
108
+ runId: event.runId,
109
+ expiresAt: this.telemetryRetentionTimestampFactory.createTraceContextExpiry(policySnapshot, observedAt),
110
+ });
111
+ const enrichment = await this.telemetryEnricherChain.enrichRun(event.workflowId);
112
+ const isTerminal = event.state.status === "completed" || event.state.status === "failed";
113
+ await this.telemetrySpanStore.upsert({
114
+ traceId: trace.traceId,
115
+ spanId: trace.rootSpanId,
116
+ runId: event.runId,
117
+ workflowId: event.workflowId,
118
+ name: "workflow.run",
119
+ kind: "internal",
120
+ status: event.state.status === "failed" ? "failed" : isTerminal ? "completed" : "running",
121
+ startTime: event.state.startedAt,
122
+ endTime: isTerminal ? (event.state.finishedAt ?? event.at) : undefined,
123
+ workflowFolder: enrichment.workflowFolder,
124
+ retentionExpiresAt: this.telemetryRetentionTimestampFactory.createSpanExpiry(policySnapshot, observedAt),
125
+ });
126
+ }
127
+
128
+ private async handleNodeSnapshot(
129
+ event: Extract<RunEvent, { kind: "nodeQueued" | "nodeStarted" | "nodeCompleted" | "nodeFailed" }>,
130
+ ): Promise<void> {
131
+ const trace = await this.runTraceContextRepository.getOrCreate({
132
+ runId: event.runId,
133
+ workflowId: event.workflowId,
134
+ serviceName: "codemation.workflow",
135
+ });
136
+ const policySnapshot = await this.loadPolicySnapshot(event.runId);
137
+ await this.runTraceContextRepository.upsertExpiry({
138
+ runId: event.runId,
139
+ expiresAt: this.telemetryRetentionTimestampFactory.createTraceContextExpiry(policySnapshot, new Date(event.at)),
140
+ });
141
+ const enrichment = await this.telemetryEnricherChain.enrichNode({
142
+ workflowId: event.workflowId,
143
+ nodeId: event.snapshot.nodeId,
144
+ });
145
+ await this.telemetrySpanStore.upsert(
146
+ this.createNodeSpanUpsert(event, trace.traceId, trace.rootSpanId, enrichment, policySnapshot),
147
+ );
148
+ }
149
+
150
+ private createNodeSpanUpsert(
151
+ event: Extract<RunEvent, { kind: "nodeQueued" | "nodeStarted" | "nodeCompleted" | "nodeFailed" }>,
152
+ traceId: string,
153
+ rootSpanId: string,
154
+ enrichment: Readonly<{ workflowFolder?: string; nodeType?: string; nodeRole?: string }>,
155
+ policySnapshot: Awaited<ReturnType<RunEventBusTelemetryReporter["loadPolicySnapshot"]>>,
156
+ ): TelemetrySpanUpsert {
157
+ const snapshot = event.snapshot;
158
+ const status = event.kind === "nodeFailed" ? "failed" : snapshot.finishedAt ? "completed" : "running";
159
+ const observedAt = new Date(snapshot.finishedAt ?? snapshot.startedAt ?? snapshot.queuedAt ?? event.at);
160
+ return {
161
+ traceId,
162
+ spanId: this.otelIdentityFactory.createNodeSpanId(snapshot.activationId ?? snapshot.nodeId),
163
+ parentSpanId: rootSpanId,
164
+ runId: event.runId,
165
+ workflowId: event.workflowId,
166
+ nodeId: snapshot.nodeId,
167
+ activationId: snapshot.activationId,
168
+ name: "workflow.node",
169
+ kind: "internal",
170
+ status,
171
+ statusMessage: snapshot.error?.message,
172
+ startTime: snapshot.startedAt ?? snapshot.queuedAt ?? event.at,
173
+ endTime: snapshot.finishedAt,
174
+ workflowFolder: enrichment.workflowFolder,
175
+ nodeType: enrichment.nodeType,
176
+ nodeRole: enrichment.nodeRole,
177
+ retentionExpiresAt: this.telemetryRetentionTimestampFactory.createSpanExpiry(policySnapshot, observedAt),
178
+ attributes: {
179
+ usedPinnedOutput: snapshot.usedPinnedOutput ?? undefined,
180
+ },
181
+ };
182
+ }
183
+
184
+ private async loadPolicySnapshot(runId: string) {
185
+ const state = await this.workflowRunRepository.load(runId);
186
+ return state?.policySnapshot;
187
+ }
188
+ }
@@ -0,0 +1,56 @@
1
+ import type {
2
+ ExecutionTelemetry,
3
+ NodeExecutionTelemetry,
4
+ TelemetryArtifactAttachment,
5
+ TelemetryArtifactReference,
6
+ TelemetryMetricRecord,
7
+ TelemetrySpanEventRecord,
8
+ } from "@codemation/core";
9
+ import { StoredNodeExecutionTelemetry } from "./StoredNodeExecutionTelemetry";
10
+ import { StoredTelemetrySpanScope } from "./StoredTelemetrySpanScope";
11
+ import type { StoredExecutionTelemetryDeps } from "./OtelExecutionTelemetry.types";
12
+
13
+ export class StoredExecutionTelemetry implements ExecutionTelemetry {
14
+ readonly traceId: string;
15
+ readonly spanId: string;
16
+
17
+ constructor(private readonly deps: StoredExecutionTelemetryDeps) {
18
+ this.traceId = deps.traceId;
19
+ this.spanId = deps.rootSpanId;
20
+ }
21
+
22
+ async addSpanEvent(args: TelemetrySpanEventRecord): Promise<void> {
23
+ await this.createRunScope().addSpanEvent(args);
24
+ }
25
+
26
+ async recordMetric(args: TelemetryMetricRecord): Promise<void> {
27
+ await this.createRunScope().recordMetric(args);
28
+ }
29
+
30
+ async attachArtifact(args: TelemetryArtifactAttachment): Promise<TelemetryArtifactReference> {
31
+ return await this.createRunScope().attachArtifact(args);
32
+ }
33
+
34
+ forNode(args: Readonly<{ nodeId: string; activationId: string }>): NodeExecutionTelemetry {
35
+ // eslint-disable-next-line codemation/no-manual-di-new
36
+ return new StoredNodeExecutionTelemetry({
37
+ ...this.deps,
38
+ spanId: this.deps.otelIdentityFactory.createNodeSpanId(args.activationId),
39
+ parentSpanId: this.deps.rootSpanId,
40
+ nodeId: args.nodeId,
41
+ activationId: args.activationId,
42
+ spanName: "workflow.node",
43
+ spanKind: "internal",
44
+ });
45
+ }
46
+
47
+ private createRunScope(): StoredTelemetrySpanScope {
48
+ // eslint-disable-next-line codemation/no-manual-di-new
49
+ return new StoredTelemetrySpanScope({
50
+ ...this.deps,
51
+ spanId: this.deps.rootSpanId,
52
+ spanName: "workflow.run",
53
+ spanKind: "internal",
54
+ });
55
+ }
56
+ }
@@ -0,0 +1,35 @@
1
+ import type { NodeExecutionTelemetry, TelemetryChildSpanStart, TelemetrySpanScope } from "@codemation/core";
2
+ import { GenAiTelemetryAttributeNames } from "@codemation/core";
3
+ import type { StoredSpanScopeArgs } from "./OtelExecutionTelemetry.types";
4
+ import { StoredTelemetrySpanScope } from "./StoredTelemetrySpanScope";
5
+
6
+ export class StoredNodeExecutionTelemetry extends StoredTelemetrySpanScope implements NodeExecutionTelemetry {
7
+ constructor(args: StoredSpanScopeArgs) {
8
+ super(args);
9
+ }
10
+
11
+ forNode(_: Readonly<{ nodeId: string; activationId: string }>): NodeExecutionTelemetry {
12
+ return this;
13
+ }
14
+
15
+ startChildSpan(args: TelemetryChildSpanStart): TelemetrySpanScope {
16
+ // eslint-disable-next-line codemation/no-manual-di-new
17
+ const span = new StoredTelemetrySpanScope({
18
+ ...this.deps,
19
+ spanId: this.deps.otelIdentityFactory.createEphemeralSpanId(),
20
+ parentSpanId: this.spanId,
21
+ nodeId: this.nodeId,
22
+ activationId: this.activationId,
23
+ spanName: args.name,
24
+ spanKind: args.kind ?? "internal",
25
+ initialAttributes: args.attributes,
26
+ initialStartTime: args.startedAt,
27
+ connectionInvocationId: this.toStringAttribute(
28
+ args.attributes?.["codemation.connection.invocation_id"] ?? args.attributes?.["connection.invocation_id"],
29
+ ),
30
+ modelName: this.toStringAttribute(args.attributes?.[GenAiTelemetryAttributeNames.requestModel]),
31
+ });
32
+ void span.markStarted();
33
+ return span;
34
+ }
35
+ }