@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.
- package/CHANGELOG.md +30 -0
- package/dist/{AppConfigFactory-CqKWXqOm.js → AppConfigFactory-BPp02HMv.js} +82 -5
- package/dist/{AppConfigFactory-CqKWXqOm.js.map → AppConfigFactory-BPp02HMv.js.map} +1 -1
- package/dist/{AppConfigFactory-CK28UPK0.d.ts → AppConfigFactory-Dq7ttwQ_.d.ts} +6001 -144
- package/dist/{AppContainerFactory-CcSGFNLW.js → AppContainerFactory-D9je1sSV.js} +2604 -155
- package/dist/AppContainerFactory-D9je1sSV.js.map +1 -0
- package/dist/{CodemationAppContext-KqDoeHqN.d.ts → CodemationAppContext-P7P-xZhQ.d.ts} +2 -2
- package/dist/{CodemationAuthoring.types-BP6Inucu.d.ts → CodemationAuthoring.types-OMYu7vKP.d.ts} +3 -3
- package/dist/{CodemationConfigNormalizer-DIAE0VHw.d.ts → CodemationConfigNormalizer-BCtBrJDe.d.ts} +2 -2
- package/dist/{CodemationConsumerConfigLoader-nj9kTmfJ.d.ts → CodemationConsumerConfigLoader-evvw4b_a.d.ts} +2 -2
- package/dist/CodemationPluginListMerger-PSTtEQjC.d.ts +674 -0
- package/dist/{CredentialServices-BFQD_VN1.d.ts → CredentialServices-0Hk8RFY1.d.ts} +3 -3
- package/dist/{CredentialServices-BPKUF8Xs.js → CredentialServices-BNBMFOPt.js} +6 -1
- package/dist/CredentialServices-BNBMFOPt.js.map +1 -0
- package/dist/{PublicFrontendBootstrapFactory-DTA1iDo0.d.ts → PublicFrontendBootstrapFactory-D0_ds7nS.d.ts} +2 -2
- package/dist/authoring.d.ts +3 -3
- package/dist/consumer.d.ts +4 -4
- package/dist/credentials.d.ts +3 -3
- package/dist/credentials.js +1 -1
- package/dist/devServerSidecar.d.ts +1 -1
- package/dist/{index-Dd6BrWyH.d.ts → index-CeS2saCe.d.ts} +105 -2
- package/dist/index.d.ts +12 -11
- package/dist/index.js +5 -5
- package/dist/nextServer.d.ts +8 -58
- package/dist/nextServer.js +6 -100
- package/dist/{persistenceServer-DKbFDxoS.js → persistenceServer-CA0_q0D7.js} +2 -2
- package/dist/{persistenceServer-DKbFDxoS.js.map → persistenceServer-CA0_q0D7.js.map} +1 -1
- package/dist/{persistenceServer-Y-u7lV7f.d.ts → persistenceServer-CJeu1STC.d.ts} +2 -2
- package/dist/persistenceServer.d.ts +5 -5
- package/dist/persistenceServer.js +2 -2
- package/dist/{server-CFpgKuVE.js → server-C_ZIEOTY.js} +4 -4
- package/dist/{server-CFpgKuVE.js.map → server-C_ZIEOTY.js.map} +1 -1
- package/dist/{server-axppTMgo.d.ts → server-Clvg5x1w.d.ts} +11 -5
- package/dist/server.d.ts +8 -8
- package/dist/server.js +5 -5
- package/package.json +6 -5
- package/prisma/migrations/20260414120000_telemetry_foundation/migration.sql +112 -0
- package/prisma/migrations/20260414153000_telemetry_retention_metrics_refactor/migration.sql +239 -0
- package/prisma/migrations.sqlite/20260414120000_telemetry_foundation/migration.sql +103 -0
- package/prisma/migrations.sqlite/20260414153000_telemetry_retention_metrics_refactor/migration.sql +540 -0
- package/prisma/schema.postgresql.prisma +100 -1
- package/prisma/schema.sqlite.prisma +100 -1
- package/scripts/generate-prisma-clients.mjs +89 -1
- package/src/application/contracts/TelemetryDashboardContracts.ts +113 -0
- package/src/application/contracts/TelemetryRunTraceContracts.ts +13 -0
- package/src/application/cost/FrameworkCostCatalogEntries.ts +126 -0
- package/src/application/queries/GetTelemetryDashboardDimensionsQuery.ts +11 -0
- package/src/application/queries/GetTelemetryDashboardDimensionsQueryHandler.ts +20 -0
- package/src/application/queries/GetTelemetryDashboardRunsQuery.ts +11 -0
- package/src/application/queries/GetTelemetryDashboardRunsQueryHandler.ts +20 -0
- package/src/application/queries/GetTelemetryDashboardSummaryQuery.ts +11 -0
- package/src/application/queries/GetTelemetryDashboardSummaryQueryHandler.ts +29 -0
- package/src/application/queries/GetTelemetryDashboardTimeseriesQuery.ts +11 -0
- package/src/application/queries/GetTelemetryDashboardTimeseriesQueryHandler.ts +36 -0
- package/src/application/queries/GetTelemetryRunTraceQuery.ts +8 -0
- package/src/application/queries/GetTelemetryRunTraceQueryHandler.ts +20 -0
- package/src/application/queries/WorkflowQueryHandlers.ts +5 -0
- package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +71 -26
- package/src/application/telemetry/CompositeTelemetryExporter.ts +13 -0
- package/src/application/telemetry/LazyExecutionTelemetryFactory.ts +21 -0
- package/src/application/telemetry/NoOpTelemetryExporter.ts +7 -0
- package/src/application/telemetry/OtelExecutionTelemetry.types.ts +41 -0
- package/src/application/telemetry/OtelExecutionTelemetryFactory.ts +56 -0
- package/src/application/telemetry/OtelIdentityFactory.ts +41 -0
- package/src/application/telemetry/RunEventBusTelemetryReporter.ts +188 -0
- package/src/application/telemetry/StoredExecutionTelemetry.ts +56 -0
- package/src/application/telemetry/StoredNodeExecutionTelemetry.ts +35 -0
- package/src/application/telemetry/StoredTelemetrySpanScope.ts +188 -0
- package/src/application/telemetry/TelemetryEnricherChain.ts +85 -0
- package/src/application/telemetry/TelemetryPrivacyPolicy.ts +19 -0
- package/src/application/telemetry/TelemetryQueryService.ts +815 -0
- package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +40 -0
- package/src/applicationTokens.ts +18 -0
- package/src/bootstrap/AppContainerFactory.ts +124 -1
- package/src/bootstrap/AppContainerLifecycle.ts +8 -0
- package/src/bootstrap/runtime/FrontendRuntime.ts +8 -0
- package/src/bootstrap/runtime/WorkerRuntime.ts +8 -0
- package/src/domain/runs/WorkflowRunRepository.ts +3 -1
- package/src/domain/telemetry/TelemetryContracts.ts +197 -0
- package/src/infrastructure/persistence/InMemoryRunTraceContextRepository.ts +56 -0
- package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +56 -0
- package/src/infrastructure/persistence/InMemoryTelemetryMetricPointStore.ts +97 -0
- package/src/infrastructure/persistence/InMemoryTelemetrySpanStore.ts +113 -0
- package/src/infrastructure/persistence/InMemoryWorkflowRunRepository.ts +4 -2
- package/src/infrastructure/persistence/PrismaRunTraceContextRepository.ts +92 -0
- package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +125 -0
- package/src/infrastructure/persistence/PrismaTelemetryMetricPointStore.ts +134 -0
- package/src/infrastructure/persistence/PrismaTelemetrySpanStore.ts +166 -0
- package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +20 -7
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/edge.js +85 -4
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/index-browser.js +81 -0
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.d.ts +6488 -102
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.js +85 -4
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/package.json +1 -1
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/schema.prisma +100 -0
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/edge.js +85 -4
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/index-browser.js +81 -0
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.d.ts +6476 -98
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.js +85 -4
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/package.json +1 -1
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/schema.prisma +100 -0
- package/src/presentation/http/ApiPaths.ts +22 -0
- package/src/presentation/http/hono/registrars/TelemetryHonoApiRouteRegistrar.ts +19 -0
- package/src/presentation/http/routeHandlers/TelemetryDashboardRequestError.ts +1 -0
- package/src/presentation/http/routeHandlers/TelemetryHttpRouteHandler.ts +181 -0
- package/dist/AppContainerFactory-CcSGFNLW.js.map +0 -1
- package/dist/CodemationPluginListMerger-QvUa2SIt.d.ts +0 -357
- package/dist/CredentialServices-BPKUF8Xs.js.map +0 -1
- package/dist/nextServer.js.map +0 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import { OtelIdentityFactory } from "../../application/telemetry/OtelIdentityFactory";
|
|
3
|
+
import type { RunTraceContextRepository, TelemetryTraceContext } from "../../domain/telemetry/TelemetryContracts";
|
|
4
|
+
|
|
5
|
+
@injectable()
|
|
6
|
+
export class InMemoryRunTraceContextRepository implements RunTraceContextRepository {
|
|
7
|
+
private readonly rows = new Map<string, TelemetryTraceContext>();
|
|
8
|
+
|
|
9
|
+
constructor(@inject(OtelIdentityFactory) private readonly otelIdentityFactory: OtelIdentityFactory) {}
|
|
10
|
+
|
|
11
|
+
async load(runId: string): Promise<TelemetryTraceContext | undefined> {
|
|
12
|
+
return this.rows.get(decodeURIComponent(runId));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async getOrCreate(
|
|
16
|
+
args: Readonly<{ runId: string; workflowId: string; serviceName?: string }>,
|
|
17
|
+
): Promise<TelemetryTraceContext> {
|
|
18
|
+
const key = decodeURIComponent(args.runId);
|
|
19
|
+
const existing = this.rows.get(key);
|
|
20
|
+
if (existing) {
|
|
21
|
+
return existing;
|
|
22
|
+
}
|
|
23
|
+
const created: TelemetryTraceContext = {
|
|
24
|
+
runId: key,
|
|
25
|
+
workflowId: decodeURIComponent(args.workflowId),
|
|
26
|
+
traceId: this.otelIdentityFactory.createTraceId(key),
|
|
27
|
+
rootSpanId: this.otelIdentityFactory.createRootSpanId(key),
|
|
28
|
+
serviceName: args.serviceName,
|
|
29
|
+
createdAt: new Date().toISOString(),
|
|
30
|
+
};
|
|
31
|
+
this.rows.set(key, created);
|
|
32
|
+
return created;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async upsertExpiry(args: Readonly<{ runId: string; expiresAt?: string }>): Promise<void> {
|
|
36
|
+
const key = decodeURIComponent(args.runId);
|
|
37
|
+
const existing = this.rows.get(key);
|
|
38
|
+
if (!existing) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.rows.set(key, {
|
|
42
|
+
...existing,
|
|
43
|
+
expiresAt: this.resolveLaterExpiry(existing.expiresAt, args.expiresAt),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private resolveLaterExpiry(current: string | undefined, candidate: string | undefined): string | undefined {
|
|
48
|
+
if (!current) {
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
51
|
+
if (!candidate) {
|
|
52
|
+
return current;
|
|
53
|
+
}
|
|
54
|
+
return current >= candidate ? current : candidate;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import { OtelIdentityFactory } from "../../application/telemetry/OtelIdentityFactory";
|
|
3
|
+
import type {
|
|
4
|
+
TelemetryArtifactRecord,
|
|
5
|
+
TelemetryArtifactStore,
|
|
6
|
+
TelemetryArtifactWrite,
|
|
7
|
+
} from "../../domain/telemetry/TelemetryContracts";
|
|
8
|
+
|
|
9
|
+
@injectable()
|
|
10
|
+
export class InMemoryTelemetryArtifactStore implements TelemetryArtifactStore {
|
|
11
|
+
private readonly rows = new Map<string, TelemetryArtifactRecord>();
|
|
12
|
+
|
|
13
|
+
constructor(@inject(OtelIdentityFactory) private readonly otelIdentityFactory: OtelIdentityFactory) {}
|
|
14
|
+
|
|
15
|
+
async save(record: TelemetryArtifactWrite): Promise<TelemetryArtifactRecord> {
|
|
16
|
+
const created: TelemetryArtifactRecord = {
|
|
17
|
+
artifactId: this.otelIdentityFactory.createArtifactId(),
|
|
18
|
+
traceId: record.traceId,
|
|
19
|
+
spanId: record.spanId,
|
|
20
|
+
runId: record.runId,
|
|
21
|
+
workflowId: record.workflowId,
|
|
22
|
+
nodeId: record.nodeId,
|
|
23
|
+
activationId: record.activationId,
|
|
24
|
+
kind: record.kind,
|
|
25
|
+
contentType: record.contentType,
|
|
26
|
+
previewText: record.previewText,
|
|
27
|
+
previewJson: record.previewJson,
|
|
28
|
+
payloadText: record.payloadText,
|
|
29
|
+
payloadJson: record.payloadJson,
|
|
30
|
+
bytes: record.bytes,
|
|
31
|
+
truncated: record.truncated,
|
|
32
|
+
createdAt: new Date().toISOString(),
|
|
33
|
+
expiresAt: record.expiresAt?.toISOString(),
|
|
34
|
+
retentionExpiresAt: record.retentionExpiresAt,
|
|
35
|
+
};
|
|
36
|
+
this.rows.set(created.artifactId, created);
|
|
37
|
+
return created;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async listByTraceId(traceId: string): Promise<ReadonlyArray<TelemetryArtifactRecord>> {
|
|
41
|
+
return [...this.rows.values()]
|
|
42
|
+
.filter((row) => row.traceId === traceId)
|
|
43
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async pruneExpired(args: Readonly<{ nowIso: string; limit?: number }>): Promise<number> {
|
|
47
|
+
const candidates = [...this.rows.entries()]
|
|
48
|
+
.filter(([, row]) => row.retentionExpiresAt !== undefined && row.retentionExpiresAt <= args.nowIso)
|
|
49
|
+
.sort((left, right) => (left[1].retentionExpiresAt ?? "").localeCompare(right[1].retentionExpiresAt ?? ""))
|
|
50
|
+
.slice(0, args.limit ?? Number.MAX_SAFE_INTEGER);
|
|
51
|
+
for (const [key] of candidates) {
|
|
52
|
+
this.rows.delete(key);
|
|
53
|
+
}
|
|
54
|
+
return candidates.length;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import { OtelIdentityFactory } from "../../application/telemetry/OtelIdentityFactory";
|
|
3
|
+
import type {
|
|
4
|
+
TelemetryMetricPointListQuery,
|
|
5
|
+
TelemetryMetricPointRecord,
|
|
6
|
+
TelemetryMetricPointStore,
|
|
7
|
+
TelemetryMetricPointWrite,
|
|
8
|
+
} from "../../domain/telemetry/TelemetryContracts";
|
|
9
|
+
|
|
10
|
+
@injectable()
|
|
11
|
+
export class InMemoryTelemetryMetricPointStore implements TelemetryMetricPointStore {
|
|
12
|
+
private readonly rows = new Map<string, TelemetryMetricPointRecord>();
|
|
13
|
+
|
|
14
|
+
constructor(@inject(OtelIdentityFactory) private readonly otelIdentityFactory: OtelIdentityFactory) {}
|
|
15
|
+
|
|
16
|
+
async save(record: TelemetryMetricPointWrite): Promise<TelemetryMetricPointRecord> {
|
|
17
|
+
const created: TelemetryMetricPointRecord = {
|
|
18
|
+
metricPointId: this.otelIdentityFactory.createArtifactId(),
|
|
19
|
+
traceId: record.traceId,
|
|
20
|
+
spanId: record.spanId,
|
|
21
|
+
runId: record.runId,
|
|
22
|
+
workflowId: record.workflowId,
|
|
23
|
+
nodeId: record.nodeId,
|
|
24
|
+
activationId: record.activationId,
|
|
25
|
+
metricName: record.name,
|
|
26
|
+
value: record.value,
|
|
27
|
+
unit: record.unit,
|
|
28
|
+
observedAt: record.observedAt,
|
|
29
|
+
workflowFolder: record.workflowFolder,
|
|
30
|
+
nodeType: record.nodeType,
|
|
31
|
+
nodeRole: record.nodeRole,
|
|
32
|
+
modelName: record.modelName,
|
|
33
|
+
dimensions: record.attributes,
|
|
34
|
+
retentionExpiresAt: record.retentionExpiresAt,
|
|
35
|
+
};
|
|
36
|
+
this.rows.set(created.metricPointId, created);
|
|
37
|
+
return created;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async list(args: TelemetryMetricPointListQuery = {}): Promise<ReadonlyArray<TelemetryMetricPointRecord>> {
|
|
41
|
+
return [...this.rows.values()]
|
|
42
|
+
.filter((row) => this.matches(row, args))
|
|
43
|
+
.sort((left, right) => {
|
|
44
|
+
const observedCompare = left.observedAt.localeCompare(right.observedAt);
|
|
45
|
+
if (observedCompare !== 0) {
|
|
46
|
+
return observedCompare;
|
|
47
|
+
}
|
|
48
|
+
return left.metricPointId.localeCompare(right.metricPointId);
|
|
49
|
+
})
|
|
50
|
+
.slice(0, args.limit ?? Number.MAX_SAFE_INTEGER);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async pruneExpired(args: Readonly<{ nowIso: string; limit?: number }>): Promise<number> {
|
|
54
|
+
const candidates = [...this.rows.entries()]
|
|
55
|
+
.filter(([, row]) => row.retentionExpiresAt !== undefined && row.retentionExpiresAt <= args.nowIso)
|
|
56
|
+
.sort((left, right) => (left[1].retentionExpiresAt ?? "").localeCompare(right[1].retentionExpiresAt ?? ""))
|
|
57
|
+
.slice(0, args.limit ?? Number.MAX_SAFE_INTEGER);
|
|
58
|
+
for (const [key] of candidates) {
|
|
59
|
+
this.rows.delete(key);
|
|
60
|
+
}
|
|
61
|
+
return candidates.length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private matches(row: TelemetryMetricPointRecord, args: TelemetryMetricPointListQuery): boolean {
|
|
65
|
+
if (args.traceId && row.traceId !== args.traceId) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (args.runId && row.runId !== args.runId) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (args.runIds && args.runIds.length > 0 && (!row.runId || !args.runIds.includes(row.runId))) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (args.workflowId && row.workflowId !== args.workflowId) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (args.workflowIds && args.workflowIds.length > 0 && !args.workflowIds.includes(row.workflowId)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (args.nodeId && row.nodeId !== args.nodeId) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (args.metricNames && args.metricNames.length > 0 && !args.metricNames.includes(row.metricName)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if (args.modelNames && args.modelNames.length > 0 && (!row.modelName || !args.modelNames.includes(row.modelName))) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (args.observedAtGte && row.observedAt < args.observedAtGte) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (args.observedAtLte && row.observedAt > args.observedAtLte) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { injectable } from "@codemation/core";
|
|
2
|
+
import type {
|
|
3
|
+
TelemetrySpanListQuery,
|
|
4
|
+
TelemetrySpanRecord,
|
|
5
|
+
TelemetrySpanStore,
|
|
6
|
+
TelemetrySpanUpsert,
|
|
7
|
+
} from "../../domain/telemetry/TelemetryContracts";
|
|
8
|
+
|
|
9
|
+
@injectable()
|
|
10
|
+
export class InMemoryTelemetrySpanStore implements TelemetrySpanStore {
|
|
11
|
+
private readonly rows = new Map<string, TelemetrySpanRecord>();
|
|
12
|
+
|
|
13
|
+
async upsert(record: TelemetrySpanUpsert): Promise<void> {
|
|
14
|
+
const key = this.createKey(record.traceId, record.spanId);
|
|
15
|
+
const existing = this.rows.get(key);
|
|
16
|
+
this.rows.set(key, this.merge(existing, record));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async list(args: TelemetrySpanListQuery = {}): Promise<ReadonlyArray<TelemetrySpanRecord>> {
|
|
20
|
+
return [...this.rows.values()]
|
|
21
|
+
.filter((row) => this.matches(row, args))
|
|
22
|
+
.sort((left, right) => {
|
|
23
|
+
const startCompare = (left.startTime ?? "").localeCompare(right.startTime ?? "");
|
|
24
|
+
if (startCompare !== 0) {
|
|
25
|
+
return startCompare;
|
|
26
|
+
}
|
|
27
|
+
return left.spanId.localeCompare(right.spanId);
|
|
28
|
+
})
|
|
29
|
+
.slice(0, args.limit ?? Number.MAX_SAFE_INTEGER);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async listByTraceId(traceId: string): Promise<ReadonlyArray<TelemetrySpanRecord>> {
|
|
33
|
+
return await this.list({ traceId });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private createKey(traceId: string, spanId: string): string {
|
|
37
|
+
return `${traceId}:${spanId}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private merge(existing: TelemetrySpanRecord | undefined, update: TelemetrySpanUpsert): TelemetrySpanRecord {
|
|
41
|
+
return {
|
|
42
|
+
traceId: update.traceId,
|
|
43
|
+
spanId: update.spanId,
|
|
44
|
+
parentSpanId: update.parentSpanId ?? existing?.parentSpanId,
|
|
45
|
+
runId: update.runId,
|
|
46
|
+
workflowId: update.workflowId,
|
|
47
|
+
nodeId: update.nodeId ?? existing?.nodeId,
|
|
48
|
+
activationId: update.activationId ?? existing?.activationId,
|
|
49
|
+
connectionInvocationId: update.connectionInvocationId ?? existing?.connectionInvocationId,
|
|
50
|
+
name: update.name ?? existing?.name ?? "codemation.span",
|
|
51
|
+
kind: update.kind ?? existing?.kind ?? "internal",
|
|
52
|
+
status: update.status ?? existing?.status,
|
|
53
|
+
statusMessage: update.statusMessage ?? existing?.statusMessage,
|
|
54
|
+
startTime: update.startTime ?? existing?.startTime,
|
|
55
|
+
endTime: update.endTime ?? existing?.endTime,
|
|
56
|
+
workflowFolder: update.workflowFolder ?? existing?.workflowFolder,
|
|
57
|
+
nodeType: update.nodeType ?? existing?.nodeType,
|
|
58
|
+
nodeRole: update.nodeRole ?? existing?.nodeRole,
|
|
59
|
+
modelName: update.modelName ?? existing?.modelName,
|
|
60
|
+
attributes: {
|
|
61
|
+
...(existing?.attributes ?? {}),
|
|
62
|
+
...(update.attributes ?? {}),
|
|
63
|
+
},
|
|
64
|
+
events: [...(existing?.events ?? []), ...(update.events ?? [])],
|
|
65
|
+
retentionExpiresAt: update.retentionExpiresAt ?? existing?.retentionExpiresAt,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private matches(row: TelemetrySpanRecord, args: TelemetrySpanListQuery): boolean {
|
|
70
|
+
if (args.traceId && row.traceId !== args.traceId) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (args.runId && row.runId !== args.runId) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (args.runIds && args.runIds.length > 0 && !args.runIds.includes(row.runId)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (args.workflowId && row.workflowId !== args.workflowId) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (args.workflowIds && args.workflowIds.length > 0 && !args.workflowIds.includes(row.workflowId)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (args.statuses && args.statuses.length > 0 && (!row.status || !args.statuses.includes(row.status))) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (args.names && args.names.length > 0 && !args.names.includes(row.name)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (args.modelNames && args.modelNames.length > 0 && (!row.modelName || !args.modelNames.includes(row.modelName))) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (args.startTimeGte && row.startTime && row.startTime < args.startTimeGte) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (args.endTimeLte && row.endTime && row.endTime > args.endTimeLte) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async pruneExpired(args: Readonly<{ nowIso: string; limit?: number }>): Promise<number> {
|
|
104
|
+
const candidates = [...this.rows.entries()]
|
|
105
|
+
.filter(([, row]) => row.retentionExpiresAt !== undefined && row.retentionExpiresAt <= args.nowIso)
|
|
106
|
+
.sort((left, right) => (left[1].retentionExpiresAt ?? "").localeCompare(right[1].retentionExpiresAt ?? ""))
|
|
107
|
+
.slice(0, args.limit ?? Number.MAX_SAFE_INTEGER);
|
|
108
|
+
for (const [key] of candidates) {
|
|
109
|
+
this.rows.delete(key);
|
|
110
|
+
}
|
|
111
|
+
return candidates.length;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -160,14 +160,16 @@ export class InMemoryWorkflowRunRepository implements WorkflowRunRepository, Wor
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
async listRunsOlderThan(
|
|
163
|
-
args: Readonly<{
|
|
163
|
+
args: Readonly<{ nowIso: string; defaultRetentionSeconds: number; limit?: number }>,
|
|
164
164
|
): Promise<ReadonlyArray<RunPruneCandidate>> {
|
|
165
165
|
const limit = args.limit ?? 100;
|
|
166
166
|
const out: RunPruneCandidate[] = [];
|
|
167
167
|
for (const s of this.runs.values()) {
|
|
168
168
|
if (s.status !== "completed" && s.status !== "failed") continue;
|
|
169
169
|
const finishedAt = RunFinishedAtFactory.resolveIso(s);
|
|
170
|
-
|
|
170
|
+
const retentionSeconds = s.policySnapshot?.retentionSeconds ?? args.defaultRetentionSeconds;
|
|
171
|
+
const cutoffIso = new Date(new Date(args.nowIso).getTime() - retentionSeconds * 1000).toISOString();
|
|
172
|
+
if (!finishedAt || finishedAt >= cutoffIso) continue;
|
|
171
173
|
out.push({
|
|
172
174
|
runId: s.runId,
|
|
173
175
|
workflowId: s.workflowId,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import { OtelIdentityFactory } from "../../application/telemetry/OtelIdentityFactory";
|
|
3
|
+
import type { RunTraceContextRepository, TelemetryTraceContext } from "../../domain/telemetry/TelemetryContracts";
|
|
4
|
+
import { PrismaDatabaseClientToken, type PrismaDatabaseClient } from "./PrismaDatabaseClient";
|
|
5
|
+
|
|
6
|
+
@injectable()
|
|
7
|
+
export class PrismaRunTraceContextRepository implements RunTraceContextRepository {
|
|
8
|
+
constructor(
|
|
9
|
+
@inject(PrismaDatabaseClientToken)
|
|
10
|
+
private readonly prisma: PrismaDatabaseClient,
|
|
11
|
+
@inject(OtelIdentityFactory)
|
|
12
|
+
private readonly otelIdentityFactory: OtelIdentityFactory,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
async load(runId: string): Promise<TelemetryTraceContext | undefined> {
|
|
16
|
+
const row = await this.prisma.runTraceContext.findUnique({
|
|
17
|
+
where: { runId: decodeURIComponent(runId) },
|
|
18
|
+
});
|
|
19
|
+
if (!row) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
runId: row.runId,
|
|
24
|
+
workflowId: row.workflowId,
|
|
25
|
+
traceId: row.traceId,
|
|
26
|
+
rootSpanId: row.rootSpanId,
|
|
27
|
+
serviceName: row.serviceName ?? undefined,
|
|
28
|
+
createdAt: row.createdAt,
|
|
29
|
+
expiresAt: row.expiresAt ?? undefined,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getOrCreate(
|
|
34
|
+
args: Readonly<{ runId: string; workflowId: string; serviceName?: string }>,
|
|
35
|
+
): Promise<TelemetryTraceContext> {
|
|
36
|
+
const existing = await this.load(args.runId);
|
|
37
|
+
if (existing) {
|
|
38
|
+
return existing;
|
|
39
|
+
}
|
|
40
|
+
const created: TelemetryTraceContext = {
|
|
41
|
+
runId: decodeURIComponent(args.runId),
|
|
42
|
+
workflowId: decodeURIComponent(args.workflowId),
|
|
43
|
+
traceId: this.otelIdentityFactory.createTraceId(args.runId),
|
|
44
|
+
rootSpanId: this.otelIdentityFactory.createRootSpanId(args.runId),
|
|
45
|
+
serviceName: args.serviceName,
|
|
46
|
+
createdAt: new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
await this.prisma.runTraceContext.create({
|
|
49
|
+
data: {
|
|
50
|
+
runId: created.runId,
|
|
51
|
+
workflowId: created.workflowId,
|
|
52
|
+
traceId: created.traceId,
|
|
53
|
+
rootSpanId: created.rootSpanId,
|
|
54
|
+
serviceName: created.serviceName ?? null,
|
|
55
|
+
createdAt: created.createdAt,
|
|
56
|
+
expiresAt: null,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
return created;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async upsertExpiry(args: Readonly<{ runId: string; expiresAt?: string }>): Promise<void> {
|
|
63
|
+
if (!args.expiresAt) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const runId = decodeURIComponent(args.runId);
|
|
67
|
+
const existing = await this.prisma.runTraceContext.findUnique({
|
|
68
|
+
where: { runId },
|
|
69
|
+
select: { expiresAt: true },
|
|
70
|
+
});
|
|
71
|
+
if (!existing) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const expiresAt = this.resolveLaterExpiry(existing.expiresAt ?? undefined, args.expiresAt);
|
|
75
|
+
await this.prisma.runTraceContext.update({
|
|
76
|
+
where: { runId },
|
|
77
|
+
data: {
|
|
78
|
+
expiresAt: expiresAt ?? null,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private resolveLaterExpiry(current: string | undefined, candidate: string | undefined): string | undefined {
|
|
84
|
+
if (!current) {
|
|
85
|
+
return candidate;
|
|
86
|
+
}
|
|
87
|
+
if (!candidate) {
|
|
88
|
+
return current;
|
|
89
|
+
}
|
|
90
|
+
return current >= candidate ? current : candidate;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import { OtelIdentityFactory } from "../../application/telemetry/OtelIdentityFactory";
|
|
3
|
+
import type {
|
|
4
|
+
TelemetryArtifactRecord,
|
|
5
|
+
TelemetryArtifactStore,
|
|
6
|
+
TelemetryArtifactWrite,
|
|
7
|
+
} from "../../domain/telemetry/TelemetryContracts";
|
|
8
|
+
import { PrismaDatabaseClientToken, type PrismaDatabaseClient } from "./PrismaDatabaseClient";
|
|
9
|
+
|
|
10
|
+
@injectable()
|
|
11
|
+
export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
|
|
12
|
+
constructor(
|
|
13
|
+
@inject(PrismaDatabaseClientToken)
|
|
14
|
+
private readonly prisma: PrismaDatabaseClient,
|
|
15
|
+
@inject(OtelIdentityFactory)
|
|
16
|
+
private readonly otelIdentityFactory: OtelIdentityFactory,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
async save(record: TelemetryArtifactWrite): Promise<TelemetryArtifactRecord> {
|
|
20
|
+
const artifactId = this.otelIdentityFactory.createArtifactId();
|
|
21
|
+
const createdAt = new Date().toISOString();
|
|
22
|
+
await this.prisma.telemetryArtifact.create({
|
|
23
|
+
data: {
|
|
24
|
+
artifactId,
|
|
25
|
+
traceId: record.traceId,
|
|
26
|
+
spanId: record.spanId,
|
|
27
|
+
runId: record.runId,
|
|
28
|
+
workflowId: record.workflowId,
|
|
29
|
+
nodeId: record.nodeId ?? null,
|
|
30
|
+
activationId: record.activationId ?? null,
|
|
31
|
+
kind: record.kind,
|
|
32
|
+
contentType: record.contentType,
|
|
33
|
+
previewText: record.previewText ?? null,
|
|
34
|
+
previewJson: record.previewJson !== undefined ? JSON.stringify(record.previewJson) : null,
|
|
35
|
+
payloadText: record.payloadText ?? null,
|
|
36
|
+
payloadJson: record.payloadJson !== undefined ? JSON.stringify(record.payloadJson) : null,
|
|
37
|
+
bytes: record.bytes ?? null,
|
|
38
|
+
truncated: record.truncated ?? null,
|
|
39
|
+
createdAt,
|
|
40
|
+
expiresAt: record.expiresAt?.toISOString() ?? null,
|
|
41
|
+
retentionExpiresAt: record.retentionExpiresAt ?? null,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
artifactId,
|
|
46
|
+
traceId: record.traceId,
|
|
47
|
+
spanId: record.spanId,
|
|
48
|
+
runId: record.runId,
|
|
49
|
+
workflowId: record.workflowId,
|
|
50
|
+
nodeId: record.nodeId,
|
|
51
|
+
activationId: record.activationId,
|
|
52
|
+
kind: record.kind,
|
|
53
|
+
contentType: record.contentType,
|
|
54
|
+
previewText: record.previewText,
|
|
55
|
+
previewJson: record.previewJson,
|
|
56
|
+
payloadText: record.payloadText,
|
|
57
|
+
payloadJson: record.payloadJson,
|
|
58
|
+
bytes: record.bytes,
|
|
59
|
+
truncated: record.truncated,
|
|
60
|
+
createdAt,
|
|
61
|
+
expiresAt: record.expiresAt?.toISOString(),
|
|
62
|
+
retentionExpiresAt: record.retentionExpiresAt,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async listByTraceId(traceId: string): Promise<ReadonlyArray<TelemetryArtifactRecord>> {
|
|
67
|
+
const rows = await this.prisma.telemetryArtifact.findMany({
|
|
68
|
+
where: { traceId },
|
|
69
|
+
orderBy: [{ createdAt: "asc" }, { artifactId: "asc" }],
|
|
70
|
+
});
|
|
71
|
+
return rows.map((row) => ({
|
|
72
|
+
artifactId: row.artifactId,
|
|
73
|
+
traceId: row.traceId,
|
|
74
|
+
spanId: row.spanId,
|
|
75
|
+
runId: row.runId,
|
|
76
|
+
workflowId: row.workflowId,
|
|
77
|
+
nodeId: row.nodeId ?? undefined,
|
|
78
|
+
activationId: row.activationId ?? undefined,
|
|
79
|
+
kind: row.kind,
|
|
80
|
+
contentType: row.contentType,
|
|
81
|
+
previewText: row.previewText ?? undefined,
|
|
82
|
+
previewJson: this.parseJson(row.previewJson),
|
|
83
|
+
payloadText: row.payloadText ?? undefined,
|
|
84
|
+
payloadJson: this.parseJson(row.payloadJson),
|
|
85
|
+
bytes: row.bytes ?? undefined,
|
|
86
|
+
truncated: row.truncated ?? undefined,
|
|
87
|
+
createdAt: row.createdAt,
|
|
88
|
+
expiresAt: row.expiresAt ?? undefined,
|
|
89
|
+
retentionExpiresAt: row.retentionExpiresAt ?? undefined,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async pruneExpired(args: Readonly<{ nowIso: string; limit?: number }>): Promise<number> {
|
|
94
|
+
const rows = await this.prisma.telemetryArtifact.findMany({
|
|
95
|
+
where: {
|
|
96
|
+
retentionExpiresAt: {
|
|
97
|
+
lte: args.nowIso,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
select: {
|
|
101
|
+
artifactId: true,
|
|
102
|
+
},
|
|
103
|
+
orderBy: [{ retentionExpiresAt: "asc" }, { artifactId: "asc" }],
|
|
104
|
+
...(args.limit ? { take: args.limit } : {}),
|
|
105
|
+
});
|
|
106
|
+
if (rows.length === 0) {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
const result = await this.prisma.telemetryArtifact.deleteMany({
|
|
110
|
+
where: {
|
|
111
|
+
artifactId: {
|
|
112
|
+
in: rows.map((row) => row.artifactId),
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
return result.count;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private parseJson(value: string | null): unknown {
|
|
120
|
+
if (!value) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
return JSON.parse(value);
|
|
124
|
+
}
|
|
125
|
+
}
|