@mastra/duckdb 1.5.1 → 1.5.2
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 +53 -9
- package/dist/db-Dl0fY488.js +204 -0
- package/dist/db-Dl0fY488.js.map +1 -0
- package/dist/db-TBEcMD49.cjs +215 -0
- package/dist/db-TBEcMD49.cjs.map +1 -0
- package/dist/docs/SKILL.md +1 -1
- package/dist/docs/assets/SOURCE_MAP.json +2 -8
- package/dist/docs/references/reference-storage-duckdb.md +4 -0
- package/dist/index.cjs +708 -805
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +703 -797
- package/dist/index.js.map +1 -1
- package/dist/observability-Dc-V68UX.cjs +3732 -0
- package/dist/observability-Dc-V68UX.cjs.map +1 -0
- package/dist/observability-UnP20Kyg.js +3732 -0
- package/dist/observability-UnP20Kyg.js.map +1 -0
- package/dist/storage/db/index.d.ts +17 -0
- package/dist/storage/db/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/tracing.d.ts.map +1 -1
- package/dist/storage/index.d.ts +13 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/package.json +16 -15
- package/dist/chunk-4LIZE4MC.js +0 -216
- package/dist/chunk-4LIZE4MC.js.map +0 -1
- package/dist/chunk-SMRZJTCI.cjs +0 -219
- package/dist/chunk-SMRZJTCI.cjs.map +0 -1
- package/dist/observability-M35AJUGT.js +0 -3806
- package/dist/observability-M35AJUGT.js.map +0 -1
- package/dist/observability-V2KYD7UF.cjs +0 -3808
- package/dist/observability-V2KYD7UF.cjs.map +0 -1
|
@@ -0,0 +1,3732 @@
|
|
|
1
|
+
import { t as DuckDBConnection } from "./db-Dl0fY488.js";
|
|
2
|
+
import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
|
|
3
|
+
import { BRANCH_SPAN_TYPES, METRIC_DISTINCT_COLUMNS, ObservabilityStorage, createStorageErrorId, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, toTraceSpans } from "@mastra/core/storage";
|
|
4
|
+
import { coreFeatures } from "@mastra/core/features";
|
|
5
|
+
import { EntityType } from "@mastra/core/observability";
|
|
6
|
+
import { parseFieldKey } from "@mastra/core/utils";
|
|
7
|
+
//#region src/storage/domains/observability/ddl.ts
|
|
8
|
+
/**
|
|
9
|
+
* DDL statements for DuckDB observability tables.
|
|
10
|
+
* All tables use append-only patterns with a single `timestamp` column.
|
|
11
|
+
*
|
|
12
|
+
* Column ordering convention:
|
|
13
|
+
* 1. Event metadata (eventType, timestamp)
|
|
14
|
+
* 2. IDs (trace, span, experiment, resource, run, session, etc.)
|
|
15
|
+
* 3. Entity hierarchy (entity, parent, root)
|
|
16
|
+
* 4. Context (user, org, environment, service, executionSource)
|
|
17
|
+
* 5. Domain-specific scalar fields
|
|
18
|
+
* 6. JSON fields (attributes, metadata, tags, input/output, etc.)
|
|
19
|
+
*/
|
|
20
|
+
const SPAN_EVENTS_CURSOR_SEQUENCE_DDL = `
|
|
21
|
+
CREATE SEQUENCE IF NOT EXISTS span_events_cursor_id_seq START 1
|
|
22
|
+
`;
|
|
23
|
+
const METRIC_EVENTS_CURSOR_SEQUENCE_DDL = `
|
|
24
|
+
CREATE SEQUENCE IF NOT EXISTS metric_events_cursor_id_seq START 1
|
|
25
|
+
`;
|
|
26
|
+
const LOG_EVENTS_CURSOR_SEQUENCE_DDL = `
|
|
27
|
+
CREATE SEQUENCE IF NOT EXISTS log_events_cursor_id_seq START 1
|
|
28
|
+
`;
|
|
29
|
+
const SCORE_EVENTS_CURSOR_SEQUENCE_DDL = `
|
|
30
|
+
CREATE SEQUENCE IF NOT EXISTS score_events_cursor_id_seq START 1
|
|
31
|
+
`;
|
|
32
|
+
const FEEDBACK_EVENTS_CURSOR_SEQUENCE_DDL = `
|
|
33
|
+
CREATE SEQUENCE IF NOT EXISTS feedback_events_cursor_id_seq START 1
|
|
34
|
+
`;
|
|
35
|
+
/** DDL for the span_events append-only table. */
|
|
36
|
+
const SPAN_EVENTS_DDL = `
|
|
37
|
+
CREATE TABLE IF NOT EXISTS span_events (
|
|
38
|
+
-- Event metadata
|
|
39
|
+
eventType VARCHAR NOT NULL,
|
|
40
|
+
timestamp TIMESTAMP NOT NULL,
|
|
41
|
+
cursorId BIGINT,
|
|
42
|
+
|
|
43
|
+
-- IDs
|
|
44
|
+
traceId VARCHAR NOT NULL,
|
|
45
|
+
spanId VARCHAR NOT NULL,
|
|
46
|
+
parentSpanId VARCHAR,
|
|
47
|
+
experimentId VARCHAR,
|
|
48
|
+
|
|
49
|
+
-- Entity
|
|
50
|
+
entityType VARCHAR,
|
|
51
|
+
entityId VARCHAR,
|
|
52
|
+
entityName VARCHAR,
|
|
53
|
+
entityVersionId VARCHAR,
|
|
54
|
+
|
|
55
|
+
-- Context
|
|
56
|
+
userId VARCHAR,
|
|
57
|
+
organizationId VARCHAR,
|
|
58
|
+
resourceId VARCHAR,
|
|
59
|
+
runId VARCHAR,
|
|
60
|
+
sessionId VARCHAR,
|
|
61
|
+
threadId VARCHAR,
|
|
62
|
+
requestId VARCHAR,
|
|
63
|
+
environment VARCHAR,
|
|
64
|
+
source VARCHAR,
|
|
65
|
+
serviceName VARCHAR,
|
|
66
|
+
requestContext JSON,
|
|
67
|
+
|
|
68
|
+
-- Span-specific scalars
|
|
69
|
+
name VARCHAR,
|
|
70
|
+
spanType VARCHAR,
|
|
71
|
+
isEvent BOOLEAN,
|
|
72
|
+
endedAt TIMESTAMP,
|
|
73
|
+
|
|
74
|
+
-- JSON fields
|
|
75
|
+
attributes JSON,
|
|
76
|
+
metadata JSON,
|
|
77
|
+
tags JSON,
|
|
78
|
+
scope JSON,
|
|
79
|
+
links JSON,
|
|
80
|
+
input JSON,
|
|
81
|
+
output JSON,
|
|
82
|
+
error JSON
|
|
83
|
+
)`;
|
|
84
|
+
/** DDL for the metric_events append-only table. */
|
|
85
|
+
const METRIC_EVENTS_DDL = `
|
|
86
|
+
CREATE TABLE IF NOT EXISTS metric_events (
|
|
87
|
+
-- Event metadata
|
|
88
|
+
timestamp TIMESTAMP NOT NULL,
|
|
89
|
+
cursorId BIGINT,
|
|
90
|
+
|
|
91
|
+
-- IDs
|
|
92
|
+
metricId VARCHAR NOT NULL PRIMARY KEY,
|
|
93
|
+
traceId VARCHAR,
|
|
94
|
+
spanId VARCHAR,
|
|
95
|
+
experimentId VARCHAR,
|
|
96
|
+
|
|
97
|
+
-- Entity hierarchy
|
|
98
|
+
entityType VARCHAR,
|
|
99
|
+
entityId VARCHAR,
|
|
100
|
+
entityName VARCHAR,
|
|
101
|
+
entityVersionId VARCHAR,
|
|
102
|
+
parentEntityVersionId VARCHAR,
|
|
103
|
+
parentEntityType VARCHAR,
|
|
104
|
+
parentEntityId VARCHAR,
|
|
105
|
+
parentEntityName VARCHAR,
|
|
106
|
+
rootEntityVersionId VARCHAR,
|
|
107
|
+
rootEntityType VARCHAR,
|
|
108
|
+
rootEntityId VARCHAR,
|
|
109
|
+
rootEntityName VARCHAR,
|
|
110
|
+
|
|
111
|
+
-- Context
|
|
112
|
+
userId VARCHAR,
|
|
113
|
+
organizationId VARCHAR,
|
|
114
|
+
resourceId VARCHAR,
|
|
115
|
+
runId VARCHAR,
|
|
116
|
+
sessionId VARCHAR,
|
|
117
|
+
threadId VARCHAR,
|
|
118
|
+
requestId VARCHAR,
|
|
119
|
+
environment VARCHAR,
|
|
120
|
+
executionSource VARCHAR,
|
|
121
|
+
serviceName VARCHAR,
|
|
122
|
+
|
|
123
|
+
-- Metric-specific scalars
|
|
124
|
+
name VARCHAR NOT NULL,
|
|
125
|
+
value DOUBLE NOT NULL,
|
|
126
|
+
provider VARCHAR,
|
|
127
|
+
model VARCHAR,
|
|
128
|
+
estimatedCost DOUBLE,
|
|
129
|
+
costUnit VARCHAR,
|
|
130
|
+
|
|
131
|
+
-- JSON fields
|
|
132
|
+
tags JSON,
|
|
133
|
+
labels JSON,
|
|
134
|
+
costMetadata JSON,
|
|
135
|
+
metadata JSON,
|
|
136
|
+
scope JSON
|
|
137
|
+
)`;
|
|
138
|
+
/** DDL for the log_events append-only table. */
|
|
139
|
+
const LOG_EVENTS_DDL = `
|
|
140
|
+
CREATE TABLE IF NOT EXISTS log_events (
|
|
141
|
+
-- Event metadata
|
|
142
|
+
timestamp TIMESTAMP NOT NULL,
|
|
143
|
+
cursorId BIGINT,
|
|
144
|
+
|
|
145
|
+
-- IDs
|
|
146
|
+
logId VARCHAR NOT NULL PRIMARY KEY,
|
|
147
|
+
traceId VARCHAR,
|
|
148
|
+
spanId VARCHAR,
|
|
149
|
+
experimentId VARCHAR,
|
|
150
|
+
|
|
151
|
+
-- Entity hierarchy
|
|
152
|
+
entityType VARCHAR,
|
|
153
|
+
entityId VARCHAR,
|
|
154
|
+
entityName VARCHAR,
|
|
155
|
+
entityVersionId VARCHAR,
|
|
156
|
+
parentEntityVersionId VARCHAR,
|
|
157
|
+
parentEntityType VARCHAR,
|
|
158
|
+
parentEntityId VARCHAR,
|
|
159
|
+
parentEntityName VARCHAR,
|
|
160
|
+
rootEntityVersionId VARCHAR,
|
|
161
|
+
rootEntityType VARCHAR,
|
|
162
|
+
rootEntityId VARCHAR,
|
|
163
|
+
rootEntityName VARCHAR,
|
|
164
|
+
|
|
165
|
+
-- Context
|
|
166
|
+
userId VARCHAR,
|
|
167
|
+
organizationId VARCHAR,
|
|
168
|
+
resourceId VARCHAR,
|
|
169
|
+
runId VARCHAR,
|
|
170
|
+
sessionId VARCHAR,
|
|
171
|
+
threadId VARCHAR,
|
|
172
|
+
requestId VARCHAR,
|
|
173
|
+
environment VARCHAR,
|
|
174
|
+
executionSource VARCHAR,
|
|
175
|
+
serviceName VARCHAR,
|
|
176
|
+
|
|
177
|
+
-- Log-specific scalars
|
|
178
|
+
level VARCHAR NOT NULL,
|
|
179
|
+
message VARCHAR NOT NULL,
|
|
180
|
+
|
|
181
|
+
-- JSON fields
|
|
182
|
+
data JSON,
|
|
183
|
+
tags JSON,
|
|
184
|
+
metadata JSON,
|
|
185
|
+
scope JSON
|
|
186
|
+
)`;
|
|
187
|
+
/** DDL for the score_events append-only table. */
|
|
188
|
+
const SCORE_EVENTS_DDL = `
|
|
189
|
+
CREATE TABLE IF NOT EXISTS score_events (
|
|
190
|
+
-- Event metadata
|
|
191
|
+
timestamp TIMESTAMP NOT NULL,
|
|
192
|
+
cursorId BIGINT,
|
|
193
|
+
|
|
194
|
+
-- IDs
|
|
195
|
+
scoreId VARCHAR NOT NULL PRIMARY KEY,
|
|
196
|
+
traceId VARCHAR,
|
|
197
|
+
spanId VARCHAR,
|
|
198
|
+
experimentId VARCHAR,
|
|
199
|
+
scoreTraceId VARCHAR,
|
|
200
|
+
|
|
201
|
+
-- Entity hierarchy
|
|
202
|
+
entityType VARCHAR,
|
|
203
|
+
entityId VARCHAR,
|
|
204
|
+
entityName VARCHAR,
|
|
205
|
+
entityVersionId VARCHAR,
|
|
206
|
+
parentEntityVersionId VARCHAR,
|
|
207
|
+
parentEntityType VARCHAR,
|
|
208
|
+
parentEntityId VARCHAR,
|
|
209
|
+
parentEntityName VARCHAR,
|
|
210
|
+
rootEntityVersionId VARCHAR,
|
|
211
|
+
rootEntityType VARCHAR,
|
|
212
|
+
rootEntityId VARCHAR,
|
|
213
|
+
rootEntityName VARCHAR,
|
|
214
|
+
|
|
215
|
+
-- Context
|
|
216
|
+
userId VARCHAR,
|
|
217
|
+
organizationId VARCHAR,
|
|
218
|
+
resourceId VARCHAR,
|
|
219
|
+
runId VARCHAR,
|
|
220
|
+
sessionId VARCHAR,
|
|
221
|
+
threadId VARCHAR,
|
|
222
|
+
requestId VARCHAR,
|
|
223
|
+
environment VARCHAR,
|
|
224
|
+
executionSource VARCHAR,
|
|
225
|
+
serviceName VARCHAR,
|
|
226
|
+
|
|
227
|
+
-- Score-specific scalars
|
|
228
|
+
scorerId VARCHAR NOT NULL,
|
|
229
|
+
scorerVersion VARCHAR,
|
|
230
|
+
source VARCHAR,
|
|
231
|
+
scoreSource VARCHAR,
|
|
232
|
+
score DOUBLE NOT NULL,
|
|
233
|
+
reason VARCHAR,
|
|
234
|
+
|
|
235
|
+
-- JSON fields
|
|
236
|
+
tags JSON,
|
|
237
|
+
metadata JSON,
|
|
238
|
+
scope JSON
|
|
239
|
+
)`;
|
|
240
|
+
/** DDL for the feedback_events append-only table. */
|
|
241
|
+
const FEEDBACK_EVENTS_DDL = `
|
|
242
|
+
CREATE TABLE IF NOT EXISTS feedback_events (
|
|
243
|
+
-- Event metadata
|
|
244
|
+
timestamp TIMESTAMP NOT NULL,
|
|
245
|
+
cursorId BIGINT,
|
|
246
|
+
|
|
247
|
+
-- IDs
|
|
248
|
+
feedbackId VARCHAR NOT NULL PRIMARY KEY,
|
|
249
|
+
traceId VARCHAR,
|
|
250
|
+
spanId VARCHAR,
|
|
251
|
+
experimentId VARCHAR,
|
|
252
|
+
-- Entity hierarchy
|
|
253
|
+
entityType VARCHAR,
|
|
254
|
+
entityId VARCHAR,
|
|
255
|
+
entityName VARCHAR,
|
|
256
|
+
entityVersionId VARCHAR,
|
|
257
|
+
parentEntityVersionId VARCHAR,
|
|
258
|
+
parentEntityType VARCHAR,
|
|
259
|
+
parentEntityId VARCHAR,
|
|
260
|
+
parentEntityName VARCHAR,
|
|
261
|
+
rootEntityVersionId VARCHAR,
|
|
262
|
+
rootEntityType VARCHAR,
|
|
263
|
+
rootEntityId VARCHAR,
|
|
264
|
+
rootEntityName VARCHAR,
|
|
265
|
+
|
|
266
|
+
-- Context
|
|
267
|
+
userId VARCHAR,
|
|
268
|
+
organizationId VARCHAR,
|
|
269
|
+
resourceId VARCHAR,
|
|
270
|
+
runId VARCHAR,
|
|
271
|
+
sessionId VARCHAR,
|
|
272
|
+
threadId VARCHAR,
|
|
273
|
+
requestId VARCHAR,
|
|
274
|
+
environment VARCHAR,
|
|
275
|
+
executionSource VARCHAR,
|
|
276
|
+
serviceName VARCHAR,
|
|
277
|
+
|
|
278
|
+
-- Feedback actor / linkage
|
|
279
|
+
feedbackUserId VARCHAR,
|
|
280
|
+
sourceId VARCHAR,
|
|
281
|
+
|
|
282
|
+
-- Feedback-specific scalars
|
|
283
|
+
source VARCHAR,
|
|
284
|
+
feedbackSource VARCHAR NOT NULL,
|
|
285
|
+
feedbackType VARCHAR NOT NULL,
|
|
286
|
+
value VARCHAR NOT NULL,
|
|
287
|
+
comment VARCHAR,
|
|
288
|
+
|
|
289
|
+
-- JSON fields
|
|
290
|
+
tags JSON,
|
|
291
|
+
metadata JSON,
|
|
292
|
+
scope JSON
|
|
293
|
+
)`;
|
|
294
|
+
/** All observability DDL statements, in creation order. */
|
|
295
|
+
const ALL_DDL = [
|
|
296
|
+
SPAN_EVENTS_CURSOR_SEQUENCE_DDL,
|
|
297
|
+
METRIC_EVENTS_CURSOR_SEQUENCE_DDL,
|
|
298
|
+
LOG_EVENTS_CURSOR_SEQUENCE_DDL,
|
|
299
|
+
SCORE_EVENTS_CURSOR_SEQUENCE_DDL,
|
|
300
|
+
FEEDBACK_EVENTS_CURSOR_SEQUENCE_DDL,
|
|
301
|
+
SPAN_EVENTS_DDL,
|
|
302
|
+
METRIC_EVENTS_DDL,
|
|
303
|
+
LOG_EVENTS_DDL,
|
|
304
|
+
SCORE_EVENTS_DDL,
|
|
305
|
+
FEEDBACK_EVENTS_DDL
|
|
306
|
+
];
|
|
307
|
+
/** Additive migrations for observability tables created by older versions. */
|
|
308
|
+
const ALL_MIGRATIONS = [
|
|
309
|
+
`CREATE SEQUENCE IF NOT EXISTS span_events_cursor_id_seq START 1`,
|
|
310
|
+
`CREATE SEQUENCE IF NOT EXISTS metric_events_cursor_id_seq START 1`,
|
|
311
|
+
`CREATE SEQUENCE IF NOT EXISTS log_events_cursor_id_seq START 1`,
|
|
312
|
+
`CREATE SEQUENCE IF NOT EXISTS score_events_cursor_id_seq START 1`,
|
|
313
|
+
`CREATE SEQUENCE IF NOT EXISTS feedback_events_cursor_id_seq START 1`,
|
|
314
|
+
`ALTER TABLE span_events ADD COLUMN IF NOT EXISTS cursorId BIGINT`,
|
|
315
|
+
`ALTER TABLE span_events ADD COLUMN IF NOT EXISTS entityVersionId VARCHAR`,
|
|
316
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS cursorId BIGINT`,
|
|
317
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS entityVersionId VARCHAR`,
|
|
318
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS parentEntityVersionId VARCHAR`,
|
|
319
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS rootEntityVersionId VARCHAR`,
|
|
320
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS experimentId VARCHAR`,
|
|
321
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS parentEntityType VARCHAR`,
|
|
322
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS parentEntityId VARCHAR`,
|
|
323
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS parentEntityName VARCHAR`,
|
|
324
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS rootEntityType VARCHAR`,
|
|
325
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS rootEntityId VARCHAR`,
|
|
326
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS rootEntityName VARCHAR`,
|
|
327
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS userId VARCHAR`,
|
|
328
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS organizationId VARCHAR`,
|
|
329
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS resourceId VARCHAR`,
|
|
330
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS runId VARCHAR`,
|
|
331
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS sessionId VARCHAR`,
|
|
332
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS threadId VARCHAR`,
|
|
333
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS requestId VARCHAR`,
|
|
334
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS environment VARCHAR`,
|
|
335
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS executionSource VARCHAR`,
|
|
336
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS serviceName VARCHAR`,
|
|
337
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS costMetadata JSON`,
|
|
338
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS metadata JSON`,
|
|
339
|
+
`ALTER TABLE metric_events ADD COLUMN IF NOT EXISTS scope JSON`,
|
|
340
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS cursorId BIGINT`,
|
|
341
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS entityVersionId VARCHAR`,
|
|
342
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS parentEntityVersionId VARCHAR`,
|
|
343
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS rootEntityVersionId VARCHAR`,
|
|
344
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS experimentId VARCHAR`,
|
|
345
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS parentEntityType VARCHAR`,
|
|
346
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS parentEntityId VARCHAR`,
|
|
347
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS parentEntityName VARCHAR`,
|
|
348
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS rootEntityType VARCHAR`,
|
|
349
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS rootEntityId VARCHAR`,
|
|
350
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS rootEntityName VARCHAR`,
|
|
351
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS userId VARCHAR`,
|
|
352
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS organizationId VARCHAR`,
|
|
353
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS resourceId VARCHAR`,
|
|
354
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS runId VARCHAR`,
|
|
355
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS sessionId VARCHAR`,
|
|
356
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS threadId VARCHAR`,
|
|
357
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS requestId VARCHAR`,
|
|
358
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS environment VARCHAR`,
|
|
359
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS executionSource VARCHAR`,
|
|
360
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS serviceName VARCHAR`,
|
|
361
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS tags JSON`,
|
|
362
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS metadata JSON`,
|
|
363
|
+
`ALTER TABLE log_events ADD COLUMN IF NOT EXISTS scope JSON`,
|
|
364
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS cursorId BIGINT`,
|
|
365
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS entityVersionId VARCHAR`,
|
|
366
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS parentEntityVersionId VARCHAR`,
|
|
367
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS rootEntityVersionId VARCHAR`,
|
|
368
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS entityType VARCHAR`,
|
|
369
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS entityId VARCHAR`,
|
|
370
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS entityName VARCHAR`,
|
|
371
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS parentEntityType VARCHAR`,
|
|
372
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS parentEntityId VARCHAR`,
|
|
373
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS parentEntityName VARCHAR`,
|
|
374
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS rootEntityType VARCHAR`,
|
|
375
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS rootEntityId VARCHAR`,
|
|
376
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS rootEntityName VARCHAR`,
|
|
377
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS userId VARCHAR`,
|
|
378
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS organizationId VARCHAR`,
|
|
379
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS resourceId VARCHAR`,
|
|
380
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS runId VARCHAR`,
|
|
381
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS sessionId VARCHAR`,
|
|
382
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS threadId VARCHAR`,
|
|
383
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS requestId VARCHAR`,
|
|
384
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS environment VARCHAR`,
|
|
385
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS executionSource VARCHAR`,
|
|
386
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS serviceName VARCHAR`,
|
|
387
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS tags JSON`,
|
|
388
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS scope JSON`,
|
|
389
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS source VARCHAR`,
|
|
390
|
+
`ALTER TABLE score_events ADD COLUMN IF NOT EXISTS scoreSource VARCHAR`,
|
|
391
|
+
`ALTER TABLE score_events ALTER COLUMN traceId DROP NOT NULL`,
|
|
392
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS cursorId BIGINT`,
|
|
393
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS entityVersionId VARCHAR`,
|
|
394
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS parentEntityVersionId VARCHAR`,
|
|
395
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS rootEntityVersionId VARCHAR`,
|
|
396
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS entityType VARCHAR`,
|
|
397
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS entityId VARCHAR`,
|
|
398
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS entityName VARCHAR`,
|
|
399
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS parentEntityType VARCHAR`,
|
|
400
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS parentEntityId VARCHAR`,
|
|
401
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS parentEntityName VARCHAR`,
|
|
402
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS rootEntityType VARCHAR`,
|
|
403
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS rootEntityId VARCHAR`,
|
|
404
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS rootEntityName VARCHAR`,
|
|
405
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS organizationId VARCHAR`,
|
|
406
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS resourceId VARCHAR`,
|
|
407
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS runId VARCHAR`,
|
|
408
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS sessionId VARCHAR`,
|
|
409
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS threadId VARCHAR`,
|
|
410
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS requestId VARCHAR`,
|
|
411
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS environment VARCHAR`,
|
|
412
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS executionSource VARCHAR`,
|
|
413
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS serviceName VARCHAR`,
|
|
414
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS feedbackUserId VARCHAR`,
|
|
415
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS sourceId VARCHAR`,
|
|
416
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS tags JSON`,
|
|
417
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS scope JSON`,
|
|
418
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS source VARCHAR`,
|
|
419
|
+
`ALTER TABLE feedback_events ADD COLUMN IF NOT EXISTS feedbackSource VARCHAR`,
|
|
420
|
+
`ALTER TABLE feedback_events ALTER COLUMN traceId DROP NOT NULL`
|
|
421
|
+
];
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/storage/domains/observability/discovery.ts
|
|
424
|
+
function unionDistinctQueries(selects, orderBy) {
|
|
425
|
+
return `${selects.join("\nUNION\n")}\nORDER BY ${orderBy}`;
|
|
426
|
+
}
|
|
427
|
+
/** Return distinct entity types across observability signals that carry them. */
|
|
428
|
+
async function getEntityTypes(db, _args) {
|
|
429
|
+
const rows = await db.query(unionDistinctQueries([
|
|
430
|
+
`SELECT entityType FROM span_events WHERE entityType IS NOT NULL`,
|
|
431
|
+
`SELECT entityType FROM metric_events WHERE entityType IS NOT NULL`,
|
|
432
|
+
`SELECT entityType FROM log_events WHERE entityType IS NOT NULL`
|
|
433
|
+
], "entityType"));
|
|
434
|
+
const validTypes = new Set(Object.values(EntityType));
|
|
435
|
+
const typeSet = /* @__PURE__ */ new Set();
|
|
436
|
+
for (const row of rows) if (row.entityType && validTypes.has(row.entityType)) typeSet.add(row.entityType);
|
|
437
|
+
return { entityTypes: Array.from(typeSet).sort() };
|
|
438
|
+
}
|
|
439
|
+
/** Return distinct entity names across observability signals, optionally filtered by entity type. */
|
|
440
|
+
async function getEntityNames(db, args) {
|
|
441
|
+
const buildSelect = (table) => {
|
|
442
|
+
const conditions = [`entityName IS NOT NULL`];
|
|
443
|
+
if (args.entityType) conditions.push(`entityType = ?`);
|
|
444
|
+
return `SELECT entityName FROM ${table} WHERE ${conditions.join(" AND ")}`;
|
|
445
|
+
};
|
|
446
|
+
const params = args.entityType ? [
|
|
447
|
+
args.entityType,
|
|
448
|
+
args.entityType,
|
|
449
|
+
args.entityType
|
|
450
|
+
] : [];
|
|
451
|
+
return { names: (await db.query(unionDistinctQueries([
|
|
452
|
+
buildSelect("span_events"),
|
|
453
|
+
buildSelect("metric_events"),
|
|
454
|
+
buildSelect("log_events")
|
|
455
|
+
], "entityName"), params)).map((r) => r.entityName) };
|
|
456
|
+
}
|
|
457
|
+
/** Return distinct service names across observability signals. */
|
|
458
|
+
async function getServiceNames(db, _args) {
|
|
459
|
+
return { serviceNames: (await db.query(unionDistinctQueries([
|
|
460
|
+
`SELECT serviceName FROM span_events WHERE serviceName IS NOT NULL`,
|
|
461
|
+
`SELECT serviceName FROM metric_events WHERE serviceName IS NOT NULL`,
|
|
462
|
+
`SELECT serviceName FROM log_events WHERE serviceName IS NOT NULL`
|
|
463
|
+
], "serviceName"))).map((r) => r.serviceName) };
|
|
464
|
+
}
|
|
465
|
+
/** Return distinct environment values across observability signals. */
|
|
466
|
+
async function getEnvironments(db, _args) {
|
|
467
|
+
return { environments: (await db.query(unionDistinctQueries([
|
|
468
|
+
`SELECT environment FROM span_events WHERE environment IS NOT NULL`,
|
|
469
|
+
`SELECT environment FROM metric_events WHERE environment IS NOT NULL`,
|
|
470
|
+
`SELECT environment FROM log_events WHERE environment IS NOT NULL`
|
|
471
|
+
], "environment"))).map((r) => r.environment) };
|
|
472
|
+
}
|
|
473
|
+
/** Return distinct tags across observability signals, optionally filtered by entity type. */
|
|
474
|
+
async function getTags(db, args) {
|
|
475
|
+
const buildSelect = (table) => {
|
|
476
|
+
const conditions = [`tags IS NOT NULL`];
|
|
477
|
+
if (args.entityType) conditions.push(`entityType = ?`);
|
|
478
|
+
return `SELECT unnest(CAST(tags AS VARCHAR[])) AS tag FROM ${table} WHERE ${conditions.join(" AND ")}`;
|
|
479
|
+
};
|
|
480
|
+
const params = args.entityType ? [
|
|
481
|
+
args.entityType,
|
|
482
|
+
args.entityType,
|
|
483
|
+
args.entityType
|
|
484
|
+
] : [];
|
|
485
|
+
return { tags: (await db.query(unionDistinctQueries([
|
|
486
|
+
buildSelect("span_events"),
|
|
487
|
+
buildSelect("metric_events"),
|
|
488
|
+
buildSelect("log_events")
|
|
489
|
+
], "tag"), params)).map((r) => r.tag) };
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/storage/domains/observability/filters.ts
|
|
493
|
+
function buildJsonPath(key) {
|
|
494
|
+
try {
|
|
495
|
+
return `$.${parseFieldKey(key)}`;
|
|
496
|
+
} catch {
|
|
497
|
+
return `$."${key.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function normalizeJsonFilterValue(value) {
|
|
501
|
+
if (value === void 0) return null;
|
|
502
|
+
if (typeof value === "string") return value;
|
|
503
|
+
return JSON.stringify(value) ?? null;
|
|
504
|
+
}
|
|
505
|
+
function sanitizeColumn(column) {
|
|
506
|
+
return parseFieldKey(column);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Build a WHERE clause from a filter object.
|
|
510
|
+
* Returns { clause, params } for parameterized queries.
|
|
511
|
+
*/
|
|
512
|
+
function buildWhereClause(filters, fieldMappings) {
|
|
513
|
+
if (!filters) return {
|
|
514
|
+
clause: "",
|
|
515
|
+
params: []
|
|
516
|
+
};
|
|
517
|
+
const conditions = [];
|
|
518
|
+
const params = [];
|
|
519
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
520
|
+
if (value === void 0 || value === null) continue;
|
|
521
|
+
const column = sanitizeColumn(fieldMappings?.[key] ?? key);
|
|
522
|
+
if (key === "timestamp" || key === "startedAt" || key === "endedAt") {
|
|
523
|
+
const dateRange = value;
|
|
524
|
+
if (dateRange.start) {
|
|
525
|
+
const op = dateRange.startExclusive ? ">" : ">=";
|
|
526
|
+
conditions.push(`${column} ${op} ?`);
|
|
527
|
+
params.push(dateRange.start);
|
|
528
|
+
}
|
|
529
|
+
if (dateRange.end) {
|
|
530
|
+
const op = dateRange.endExclusive ? "<" : "<=";
|
|
531
|
+
conditions.push(`${column} ${op} ?`);
|
|
532
|
+
params.push(dateRange.end);
|
|
533
|
+
}
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (key === "labels") {
|
|
537
|
+
const labelsObj = value;
|
|
538
|
+
for (const [labelKey, labelValue] of Object.entries(labelsObj)) {
|
|
539
|
+
conditions.push(`json_extract_string(${column}, ?) = ?`);
|
|
540
|
+
params.push(buildJsonPath(labelKey), labelValue);
|
|
541
|
+
}
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (key === "tags") {
|
|
545
|
+
const tags = value;
|
|
546
|
+
for (const tag of tags) {
|
|
547
|
+
conditions.push(`list_contains(CAST(${column} AS VARCHAR[]), ?)`);
|
|
548
|
+
params.push(tag);
|
|
549
|
+
}
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (key === "status") {
|
|
553
|
+
const status = value;
|
|
554
|
+
if (status === "error") conditions.push(`error IS NOT NULL`);
|
|
555
|
+
else if (status === "running") conditions.push(`endedAt IS NULL AND error IS NULL`);
|
|
556
|
+
else if (status === "success") conditions.push(`endedAt IS NOT NULL AND error IS NULL`);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (key === "hasChildError") continue;
|
|
560
|
+
if (key === "metadata" || key === "scope") {
|
|
561
|
+
const jsonObj = value;
|
|
562
|
+
for (const [jsonKey, jsonValue] of Object.entries(jsonObj)) {
|
|
563
|
+
const normalized = normalizeJsonFilterValue(jsonValue);
|
|
564
|
+
if (normalized === null) continue;
|
|
565
|
+
conditions.push(`json_extract_string(${column}, ?) = ?`);
|
|
566
|
+
params.push(buildJsonPath(jsonKey), normalized);
|
|
567
|
+
}
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (Array.isArray(value)) {
|
|
571
|
+
if (value.length === 0) continue;
|
|
572
|
+
const placeholders = value.map(() => "?").join(", ");
|
|
573
|
+
conditions.push(`${column} IN (${placeholders})`);
|
|
574
|
+
params.push(...value);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
conditions.push(`${column} = ?`);
|
|
578
|
+
params.push(value);
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
clause: conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "",
|
|
582
|
+
params
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Build an ORDER BY clause from orderBy config.
|
|
587
|
+
*/
|
|
588
|
+
function buildOrderByClause(orderBy) {
|
|
589
|
+
if (!orderBy) return "";
|
|
590
|
+
const dir = orderBy.direction.toUpperCase();
|
|
591
|
+
if (dir !== "ASC" && dir !== "DESC") throw new Error(`Invalid sort direction: ${orderBy.direction}`);
|
|
592
|
+
return `ORDER BY ${parseFieldKey(orderBy.field)} ${dir}`;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Build a LIMIT/OFFSET clause from pagination config.
|
|
596
|
+
*/
|
|
597
|
+
function buildPaginationClause(pagination) {
|
|
598
|
+
if (!pagination) return {
|
|
599
|
+
clause: "",
|
|
600
|
+
params: []
|
|
601
|
+
};
|
|
602
|
+
if (!Number.isInteger(pagination.page) || pagination.page < 0) throw new Error(`Invalid page: ${pagination.page}`);
|
|
603
|
+
if (!Number.isInteger(pagination.perPage) || pagination.perPage <= 0) throw new Error(`Invalid perPage: ${pagination.perPage}`);
|
|
604
|
+
const offset = pagination.page * pagination.perPage;
|
|
605
|
+
return {
|
|
606
|
+
clause: `LIMIT ? OFFSET ?`,
|
|
607
|
+
params: [pagination.perPage, offset]
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region src/storage/domains/observability/helpers.ts
|
|
612
|
+
/** Shorthand for {@link DuckDBConnection.sqlValue}. */
|
|
613
|
+
const v = DuckDBConnection.sqlValue;
|
|
614
|
+
/** Serialize a value to JSON then SQL-escape it, or return 'NULL'. */
|
|
615
|
+
function jsonV(val) {
|
|
616
|
+
if (val === null || val === void 0) return "NULL";
|
|
617
|
+
return DuckDBConnection.sqlValue(JSON.stringify(val));
|
|
618
|
+
}
|
|
619
|
+
/** Coerce a value to a Date. Throws if value is nullish. */
|
|
620
|
+
function toDate(val) {
|
|
621
|
+
if (val === null || val === void 0) throw new Error("Expected date value but received null/undefined");
|
|
622
|
+
const date = val instanceof Date ? val : new Date(String(val));
|
|
623
|
+
if (Number.isNaN(date.getTime())) throw new Error("Expected valid date but received invalid date");
|
|
624
|
+
return date;
|
|
625
|
+
}
|
|
626
|
+
/** Coerce a value to a Date, returning null for nullish values. */
|
|
627
|
+
function toDateOrNull(val) {
|
|
628
|
+
if (val === null || val === void 0) return null;
|
|
629
|
+
return val instanceof Date ? val : new Date(String(val));
|
|
630
|
+
}
|
|
631
|
+
/** Parse a JSON string, returning the original value if parsing fails. */
|
|
632
|
+
function parseJson(value) {
|
|
633
|
+
if (value === null || value === void 0) return null;
|
|
634
|
+
if (typeof value === "string") try {
|
|
635
|
+
return JSON.parse(value);
|
|
636
|
+
} catch {
|
|
637
|
+
return value;
|
|
638
|
+
}
|
|
639
|
+
return value;
|
|
640
|
+
}
|
|
641
|
+
/** Parse a JSON string and return the result only if it is an array. */
|
|
642
|
+
function parseJsonArray(value) {
|
|
643
|
+
if (value === null || value === void 0) return null;
|
|
644
|
+
const parsed = parseJson(value);
|
|
645
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
646
|
+
}
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/storage/domains/observability/polling.ts
|
|
649
|
+
const OBSERVABILITY_DELTA_POLLING_FEATURE = "observability-delta-polling";
|
|
650
|
+
function deltaPollingFeatureEnabled() {
|
|
651
|
+
return coreFeatures.has(OBSERVABILITY_DELTA_POLLING_FEATURE);
|
|
652
|
+
}
|
|
653
|
+
function assertDeltaPollingEnabled() {
|
|
654
|
+
if (deltaPollingFeatureEnabled()) return;
|
|
655
|
+
throw new MastraError({
|
|
656
|
+
id: "OBSERVABILITY_DELTA_POLLING_NOT_SUPPORTED",
|
|
657
|
+
domain: ErrorDomain.MASTRA_OBSERVABILITY,
|
|
658
|
+
category: ErrorCategory.SYSTEM,
|
|
659
|
+
text: "This storage provider does not support observability delta polling"
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
function encodeDeltaCursor(value) {
|
|
663
|
+
return String(value ?? 0);
|
|
664
|
+
}
|
|
665
|
+
function validateCursorId(cursor) {
|
|
666
|
+
if (!/^\d+$/.test(cursor)) throw new MastraError({
|
|
667
|
+
id: "OBSERVABILITY_INVALID_DELTA_CURSOR",
|
|
668
|
+
domain: ErrorDomain.MASTRA_OBSERVABILITY,
|
|
669
|
+
category: ErrorCategory.USER,
|
|
670
|
+
text: "Invalid observability delta cursor"
|
|
671
|
+
});
|
|
672
|
+
return cursor;
|
|
673
|
+
}
|
|
674
|
+
function extendWhereClause(baseClause, extraConditions) {
|
|
675
|
+
const conditions = extraConditions.filter(Boolean);
|
|
676
|
+
if (conditions.length === 0) return baseClause;
|
|
677
|
+
if (!baseClause) return `WHERE ${conditions.join(" AND ")}`;
|
|
678
|
+
return `${baseClause} AND ${conditions.join(" AND ")}`;
|
|
679
|
+
}
|
|
680
|
+
//#endregion
|
|
681
|
+
//#region src/storage/domains/observability/feedback.ts
|
|
682
|
+
const FEEDBACK_GROUP_BY_COLUMNS = /* @__PURE__ */ new Set([
|
|
683
|
+
"timestamp",
|
|
684
|
+
"traceId",
|
|
685
|
+
"spanId",
|
|
686
|
+
"experimentId",
|
|
687
|
+
"entityType",
|
|
688
|
+
"entityId",
|
|
689
|
+
"entityName",
|
|
690
|
+
"entityVersionId",
|
|
691
|
+
"parentEntityVersionId",
|
|
692
|
+
"parentEntityType",
|
|
693
|
+
"parentEntityId",
|
|
694
|
+
"parentEntityName",
|
|
695
|
+
"rootEntityVersionId",
|
|
696
|
+
"rootEntityType",
|
|
697
|
+
"rootEntityId",
|
|
698
|
+
"rootEntityName",
|
|
699
|
+
"userId",
|
|
700
|
+
"organizationId",
|
|
701
|
+
"resourceId",
|
|
702
|
+
"runId",
|
|
703
|
+
"sessionId",
|
|
704
|
+
"threadId",
|
|
705
|
+
"requestId",
|
|
706
|
+
"environment",
|
|
707
|
+
"executionSource",
|
|
708
|
+
"serviceName",
|
|
709
|
+
"feedbackUserId",
|
|
710
|
+
"sourceId",
|
|
711
|
+
"feedbackSource",
|
|
712
|
+
"feedbackType",
|
|
713
|
+
"value",
|
|
714
|
+
"comment"
|
|
715
|
+
]);
|
|
716
|
+
function getAggregationSql$2(aggregation, measure = "TRY_CAST(value AS DOUBLE)") {
|
|
717
|
+
switch (aggregation) {
|
|
718
|
+
case "sum": return `SUM(${measure})`;
|
|
719
|
+
case "avg": return `AVG(${measure})`;
|
|
720
|
+
case "min": return `MIN(${measure})`;
|
|
721
|
+
case "max": return `MAX(${measure})`;
|
|
722
|
+
case "count": return `CAST(COUNT(${measure}) AS DOUBLE)`;
|
|
723
|
+
case "last": return `arg_max(${measure}, timestamp)`;
|
|
724
|
+
default: return `SUM(${measure})`;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function getIntervalSql$2(interval) {
|
|
728
|
+
switch (interval) {
|
|
729
|
+
case "1m": return "1 minute";
|
|
730
|
+
case "5m": return "5 minutes";
|
|
731
|
+
case "15m": return "15 minutes";
|
|
732
|
+
case "1h": return "1 hour";
|
|
733
|
+
case "1d": return "1 day";
|
|
734
|
+
default: return "1 hour";
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function getValidatedPercentiles$1(percentiles) {
|
|
738
|
+
if (!Array.isArray(percentiles) || percentiles.length === 0) throw new Error("Percentiles must include at least one value between 0 and 1.");
|
|
739
|
+
return percentiles.map((percentile) => {
|
|
740
|
+
if (!Number.isFinite(percentile) || percentile < 0 || percentile > 1) throw new Error("Percentiles must be finite numbers between 0 and 1.");
|
|
741
|
+
return percentile;
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
function buildFeedbackWhereClause(args, includeNumericGuard = false) {
|
|
745
|
+
const conditions = ["feedbackType = ?"];
|
|
746
|
+
const params = [args.feedbackType];
|
|
747
|
+
if (args.feedbackSource !== void 0) {
|
|
748
|
+
conditions.push("feedbackSource = ?");
|
|
749
|
+
params.push(args.feedbackSource);
|
|
750
|
+
}
|
|
751
|
+
const { clause: filterClause, params: filterParams } = buildWhereClause(args.filters, { source: "feedbackSource" });
|
|
752
|
+
if (filterClause) {
|
|
753
|
+
conditions.push(filterClause.replace("WHERE ", ""));
|
|
754
|
+
params.push(...filterParams);
|
|
755
|
+
}
|
|
756
|
+
if (includeNumericGuard) conditions.push("TRY_CAST(value AS DOUBLE) IS NOT NULL");
|
|
757
|
+
return {
|
|
758
|
+
clause: `WHERE ${conditions.join(" AND ")}`,
|
|
759
|
+
params
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function resolveFeedbackGroupBy(groupBy) {
|
|
763
|
+
return groupBy.map((key, index) => {
|
|
764
|
+
const column = parseFieldKey(key);
|
|
765
|
+
if (!FEEDBACK_GROUP_BY_COLUMNS.has(column)) throw new Error(`Invalid groupBy column(s): ${key}`);
|
|
766
|
+
const alias = `group_by_${index}`;
|
|
767
|
+
return {
|
|
768
|
+
key,
|
|
769
|
+
selectSql: `${column} AS ${alias}`,
|
|
770
|
+
groupSql: alias
|
|
771
|
+
};
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
function toSeriesName$1(values) {
|
|
775
|
+
return values.map((value) => value === null || value === void 0 ? "" : String(value)).join("|");
|
|
776
|
+
}
|
|
777
|
+
function rowToFeedbackRecord(row) {
|
|
778
|
+
const rawValue = row.value;
|
|
779
|
+
let value = rawValue;
|
|
780
|
+
const numValue = Number(rawValue);
|
|
781
|
+
if (!isNaN(numValue)) value = numValue;
|
|
782
|
+
return {
|
|
783
|
+
feedbackId: row.feedbackId,
|
|
784
|
+
timestamp: toDate(row.timestamp),
|
|
785
|
+
traceId: row.traceId ?? null,
|
|
786
|
+
spanId: row.spanId ?? null,
|
|
787
|
+
experimentId: row.experimentId ?? null,
|
|
788
|
+
entityType: row.entityType ?? null,
|
|
789
|
+
entityId: row.entityId ?? null,
|
|
790
|
+
entityName: row.entityName ?? null,
|
|
791
|
+
entityVersionId: row.entityVersionId ?? null,
|
|
792
|
+
parentEntityVersionId: row.parentEntityVersionId ?? null,
|
|
793
|
+
parentEntityType: row.parentEntityType ?? null,
|
|
794
|
+
parentEntityId: row.parentEntityId ?? null,
|
|
795
|
+
parentEntityName: row.parentEntityName ?? null,
|
|
796
|
+
rootEntityVersionId: row.rootEntityVersionId ?? null,
|
|
797
|
+
rootEntityType: row.rootEntityType ?? null,
|
|
798
|
+
rootEntityId: row.rootEntityId ?? null,
|
|
799
|
+
rootEntityName: row.rootEntityName ?? null,
|
|
800
|
+
userId: row.userId ?? null,
|
|
801
|
+
organizationId: row.organizationId ?? null,
|
|
802
|
+
resourceId: row.resourceId ?? null,
|
|
803
|
+
runId: row.runId ?? null,
|
|
804
|
+
sessionId: row.sessionId ?? null,
|
|
805
|
+
threadId: row.threadId ?? null,
|
|
806
|
+
requestId: row.requestId ?? null,
|
|
807
|
+
environment: row.environment ?? null,
|
|
808
|
+
executionSource: row.executionSource ?? null,
|
|
809
|
+
serviceName: row.serviceName ?? null,
|
|
810
|
+
feedbackUserId: row.feedbackUserId ?? null,
|
|
811
|
+
sourceId: row.sourceId ?? null,
|
|
812
|
+
source: row.feedbackSource,
|
|
813
|
+
feedbackSource: row.feedbackSource,
|
|
814
|
+
feedbackType: row.feedbackType,
|
|
815
|
+
value,
|
|
816
|
+
comment: row.comment ?? null,
|
|
817
|
+
tags: parseJsonArray(row.tags),
|
|
818
|
+
metadata: parseJson(row.metadata),
|
|
819
|
+
scope: parseJson(row.scope)
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
function getComparisonDateRange$1(comparePeriod, timestamp) {
|
|
823
|
+
if (!timestamp.start || !timestamp.end) return null;
|
|
824
|
+
const duration = timestamp.end.getTime() - timestamp.start.getTime();
|
|
825
|
+
switch (comparePeriod) {
|
|
826
|
+
case "previous_period": return {
|
|
827
|
+
start: new Date(timestamp.start.getTime() - duration),
|
|
828
|
+
end: new Date(timestamp.end.getTime() - duration),
|
|
829
|
+
startExclusive: timestamp.startExclusive,
|
|
830
|
+
endExclusive: timestamp.endExclusive
|
|
831
|
+
};
|
|
832
|
+
case "previous_day": return {
|
|
833
|
+
start: /* @__PURE__ */ new Date(timestamp.start.getTime() - 864e5),
|
|
834
|
+
end: /* @__PURE__ */ new Date(timestamp.end.getTime() - 864e5),
|
|
835
|
+
startExclusive: timestamp.startExclusive,
|
|
836
|
+
endExclusive: timestamp.endExclusive
|
|
837
|
+
};
|
|
838
|
+
case "previous_week": return {
|
|
839
|
+
start: /* @__PURE__ */ new Date(timestamp.start.getTime() - 6048e5),
|
|
840
|
+
end: /* @__PURE__ */ new Date(timestamp.end.getTime() - 6048e5),
|
|
841
|
+
startExclusive: timestamp.startExclusive,
|
|
842
|
+
endExclusive: timestamp.endExclusive
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/** Insert a single feedback event. */
|
|
847
|
+
async function createFeedback(db, args) {
|
|
848
|
+
const f = args.feedback;
|
|
849
|
+
const feedbackSource = f.feedbackSource ?? f.source ?? "";
|
|
850
|
+
const feedbackUserId = f.feedbackUserId ?? f.userId ?? null;
|
|
851
|
+
await db.execute(`INSERT INTO feedback_events (
|
|
852
|
+
feedbackId, timestamp, cursorId, traceId, spanId, experimentId,
|
|
853
|
+
entityType, entityId, entityName, entityVersionId, parentEntityVersionId, parentEntityType, parentEntityId, parentEntityName, rootEntityVersionId, rootEntityType, rootEntityId, rootEntityName,
|
|
854
|
+
userId, organizationId, resourceId, runId, sessionId, threadId, requestId, environment, executionSource, serviceName,
|
|
855
|
+
feedbackUserId, sourceId, feedbackSource, feedbackType, value, comment, tags, metadata, scope
|
|
856
|
+
)
|
|
857
|
+
VALUES (${[
|
|
858
|
+
v(f.feedbackId),
|
|
859
|
+
v(f.timestamp),
|
|
860
|
+
"nextval('feedback_events_cursor_id_seq')",
|
|
861
|
+
v(f.traceId),
|
|
862
|
+
v(f.spanId ?? null),
|
|
863
|
+
v(f.experimentId ?? null),
|
|
864
|
+
v(f.entityType ?? null),
|
|
865
|
+
v(f.entityId ?? null),
|
|
866
|
+
v(f.entityName ?? null),
|
|
867
|
+
v(f.entityVersionId ?? null),
|
|
868
|
+
v(f.parentEntityVersionId ?? null),
|
|
869
|
+
v(f.parentEntityType ?? null),
|
|
870
|
+
v(f.parentEntityId ?? null),
|
|
871
|
+
v(f.parentEntityName ?? null),
|
|
872
|
+
v(f.rootEntityVersionId ?? null),
|
|
873
|
+
v(f.rootEntityType ?? null),
|
|
874
|
+
v(f.rootEntityId ?? null),
|
|
875
|
+
v(f.rootEntityName ?? null),
|
|
876
|
+
v(f.userId ?? null),
|
|
877
|
+
v(f.organizationId ?? null),
|
|
878
|
+
v(f.resourceId ?? null),
|
|
879
|
+
v(f.runId ?? null),
|
|
880
|
+
v(f.sessionId ?? null),
|
|
881
|
+
v(f.threadId ?? null),
|
|
882
|
+
v(f.requestId ?? null),
|
|
883
|
+
v(f.environment ?? null),
|
|
884
|
+
v(f.executionSource ?? null),
|
|
885
|
+
v(f.serviceName ?? null),
|
|
886
|
+
v(feedbackUserId),
|
|
887
|
+
v(f.sourceId ?? null),
|
|
888
|
+
v(feedbackSource),
|
|
889
|
+
v(f.feedbackType),
|
|
890
|
+
v(String(f.value)),
|
|
891
|
+
v(f.comment ?? null),
|
|
892
|
+
jsonV(f.tags ?? null),
|
|
893
|
+
jsonV(f.metadata),
|
|
894
|
+
jsonV(f.scope ?? null)
|
|
895
|
+
].join(", ")})
|
|
896
|
+
ON CONFLICT DO NOTHING`);
|
|
897
|
+
}
|
|
898
|
+
/** Insert multiple feedback events in a single statement. */
|
|
899
|
+
async function batchCreateFeedback(db, args) {
|
|
900
|
+
if (args.feedbacks.length === 0) return;
|
|
901
|
+
const tuples = args.feedbacks.map((f) => {
|
|
902
|
+
const legacyFeedback = f;
|
|
903
|
+
const feedbackSource = legacyFeedback.feedbackSource ?? legacyFeedback.source ?? "";
|
|
904
|
+
const feedbackUserId = legacyFeedback.feedbackUserId ?? legacyFeedback.userId ?? null;
|
|
905
|
+
return `(${[
|
|
906
|
+
v(legacyFeedback.feedbackId),
|
|
907
|
+
v(legacyFeedback.timestamp),
|
|
908
|
+
"nextval('feedback_events_cursor_id_seq')",
|
|
909
|
+
v(legacyFeedback.traceId),
|
|
910
|
+
v(legacyFeedback.spanId ?? null),
|
|
911
|
+
v(legacyFeedback.experimentId ?? null),
|
|
912
|
+
v(legacyFeedback.entityType ?? null),
|
|
913
|
+
v(legacyFeedback.entityId ?? null),
|
|
914
|
+
v(legacyFeedback.entityName ?? null),
|
|
915
|
+
v(legacyFeedback.entityVersionId ?? null),
|
|
916
|
+
v(legacyFeedback.parentEntityVersionId ?? null),
|
|
917
|
+
v(legacyFeedback.parentEntityType ?? null),
|
|
918
|
+
v(legacyFeedback.parentEntityId ?? null),
|
|
919
|
+
v(legacyFeedback.parentEntityName ?? null),
|
|
920
|
+
v(legacyFeedback.rootEntityVersionId ?? null),
|
|
921
|
+
v(legacyFeedback.rootEntityType ?? null),
|
|
922
|
+
v(legacyFeedback.rootEntityId ?? null),
|
|
923
|
+
v(legacyFeedback.rootEntityName ?? null),
|
|
924
|
+
v(legacyFeedback.userId ?? null),
|
|
925
|
+
v(legacyFeedback.organizationId ?? null),
|
|
926
|
+
v(legacyFeedback.resourceId ?? null),
|
|
927
|
+
v(legacyFeedback.runId ?? null),
|
|
928
|
+
v(legacyFeedback.sessionId ?? null),
|
|
929
|
+
v(legacyFeedback.threadId ?? null),
|
|
930
|
+
v(legacyFeedback.requestId ?? null),
|
|
931
|
+
v(legacyFeedback.environment ?? null),
|
|
932
|
+
v(legacyFeedback.executionSource ?? null),
|
|
933
|
+
v(legacyFeedback.serviceName ?? null),
|
|
934
|
+
v(feedbackUserId),
|
|
935
|
+
v(legacyFeedback.sourceId ?? null),
|
|
936
|
+
v(feedbackSource),
|
|
937
|
+
v(legacyFeedback.feedbackType),
|
|
938
|
+
v(String(legacyFeedback.value)),
|
|
939
|
+
v(legacyFeedback.comment ?? null),
|
|
940
|
+
jsonV(legacyFeedback.tags ?? null),
|
|
941
|
+
jsonV(legacyFeedback.metadata),
|
|
942
|
+
jsonV(legacyFeedback.scope ?? null)
|
|
943
|
+
].join(", ")})`;
|
|
944
|
+
});
|
|
945
|
+
await db.execute(`INSERT INTO feedback_events (
|
|
946
|
+
feedbackId, timestamp, cursorId, traceId, spanId, experimentId,
|
|
947
|
+
entityType, entityId, entityName, entityVersionId, parentEntityVersionId, parentEntityType, parentEntityId, parentEntityName, rootEntityVersionId, rootEntityType, rootEntityId, rootEntityName,
|
|
948
|
+
userId, organizationId, resourceId, runId, sessionId, threadId, requestId, environment, executionSource, serviceName,
|
|
949
|
+
feedbackUserId, sourceId, feedbackSource, feedbackType, value, comment, tags, metadata, scope
|
|
950
|
+
)
|
|
951
|
+
VALUES ${tuples.join(",\n ")}
|
|
952
|
+
ON CONFLICT DO NOTHING`);
|
|
953
|
+
}
|
|
954
|
+
/** Query feedback events with filtering, ordering, and pagination. */
|
|
955
|
+
async function listFeedback(db, args) {
|
|
956
|
+
const { mode, filters, pagination, orderBy, after, limit } = listFeedbackArgsSchema.parse(args);
|
|
957
|
+
const page = Number(pagination.page);
|
|
958
|
+
const perPage = Number(pagination.perPage);
|
|
959
|
+
const { clause: filterClause, params: filterParams } = buildWhereClause(filters, { source: "feedbackSource" });
|
|
960
|
+
if (mode === "delta") {
|
|
961
|
+
assertDeltaPollingEnabled();
|
|
962
|
+
const streamHeadCursor = await getStreamHeadCursor$3(db);
|
|
963
|
+
if (after === void 0) return {
|
|
964
|
+
feedback: [],
|
|
965
|
+
delta: {
|
|
966
|
+
limit,
|
|
967
|
+
hasMore: false
|
|
968
|
+
},
|
|
969
|
+
deltaCursor: streamHeadCursor
|
|
970
|
+
};
|
|
971
|
+
const afterCursorId = validateCursorId(after);
|
|
972
|
+
const deltaWhereClause = extendWhereClause(filterClause, ["cursorId IS NOT NULL", `cursorId > CAST(? AS BIGINT)`]);
|
|
973
|
+
const rows = await db.query(`SELECT * FROM feedback_events ${deltaWhereClause} ORDER BY cursorId ASC LIMIT ?`, [
|
|
974
|
+
...filterParams,
|
|
975
|
+
afterCursorId,
|
|
976
|
+
limit + 1
|
|
977
|
+
]);
|
|
978
|
+
const visibleRows = rows.slice(0, limit).map((row) => ({
|
|
979
|
+
cursorId: row.cursorId,
|
|
980
|
+
feedback: rowToFeedbackRecord(row)
|
|
981
|
+
}));
|
|
982
|
+
return {
|
|
983
|
+
feedback: visibleRows.map((row) => row.feedback),
|
|
984
|
+
delta: {
|
|
985
|
+
limit,
|
|
986
|
+
hasMore: rows.length > limit
|
|
987
|
+
},
|
|
988
|
+
deltaCursor: visibleRows.length > 0 ? encodeDeltaCursor(visibleRows[visibleRows.length - 1]?.cursorId) : streamHeadCursor
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
const orderByClause = buildOrderByClause(orderBy);
|
|
992
|
+
const { clause: paginationClause, params: paginationParams } = buildPaginationClause({
|
|
993
|
+
page,
|
|
994
|
+
perPage
|
|
995
|
+
});
|
|
996
|
+
const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getDeltaCursor$3(db, filterClause, filterParams) : void 0;
|
|
997
|
+
const countResult = await db.query(`SELECT COUNT(*) as total FROM feedback_events ${filterClause}`, filterParams);
|
|
998
|
+
const total = Number(countResult[0]?.total ?? 0);
|
|
999
|
+
const rows = await db.query(`SELECT * FROM feedback_events ${filterClause} ${orderByClause} ${paginationClause}`, [...filterParams, ...paginationParams]);
|
|
1000
|
+
return {
|
|
1001
|
+
pagination: {
|
|
1002
|
+
total,
|
|
1003
|
+
page,
|
|
1004
|
+
perPage,
|
|
1005
|
+
hasMore: (page + 1) * perPage < total
|
|
1006
|
+
},
|
|
1007
|
+
feedback: rows.map((row) => rowToFeedbackRecord(row)),
|
|
1008
|
+
...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
async function getDeltaCursor$3(db, filterClause, filterParams) {
|
|
1012
|
+
const cursorId = (await db.query(`SELECT max(cursorId) AS cursorId FROM feedback_events ${filterClause}`, filterParams))[0]?.cursorId;
|
|
1013
|
+
if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
|
|
1014
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM feedback_events`))[0]?.cursorId);
|
|
1015
|
+
}
|
|
1016
|
+
async function getStreamHeadCursor$3(db) {
|
|
1017
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM feedback_events`))[0]?.cursorId);
|
|
1018
|
+
}
|
|
1019
|
+
async function getFeedbackAggregate(db, args) {
|
|
1020
|
+
const aggSql = getAggregationSql$2(args.aggregation);
|
|
1021
|
+
const { clause, params } = buildFeedbackWhereClause(args, true);
|
|
1022
|
+
const rows = await db.query(`SELECT ${aggSql} AS value FROM feedback_events ${clause}`, params);
|
|
1023
|
+
const value = rows[0]?.value === null || rows[0]?.value === void 0 ? null : Number(rows[0]?.value);
|
|
1024
|
+
if (args.comparePeriod && args.filters?.timestamp) {
|
|
1025
|
+
const previousTimestamp = getComparisonDateRange$1(args.comparePeriod, args.filters.timestamp);
|
|
1026
|
+
if (previousTimestamp) {
|
|
1027
|
+
const previousWhere = buildFeedbackWhereClause({
|
|
1028
|
+
...args,
|
|
1029
|
+
filters: {
|
|
1030
|
+
...args.filters ?? {},
|
|
1031
|
+
timestamp: previousTimestamp
|
|
1032
|
+
}
|
|
1033
|
+
}, true);
|
|
1034
|
+
const prevRows = await db.query(`SELECT ${aggSql} AS value FROM feedback_events ${previousWhere.clause}`, previousWhere.params);
|
|
1035
|
+
const previousValue = prevRows[0]?.value === null || prevRows[0]?.value === void 0 ? null : Number(prevRows[0]?.value);
|
|
1036
|
+
let changePercent = null;
|
|
1037
|
+
if (previousValue !== null && previousValue !== 0 && value !== null) changePercent = (value - previousValue) / Math.abs(previousValue) * 100;
|
|
1038
|
+
return {
|
|
1039
|
+
value,
|
|
1040
|
+
previousValue,
|
|
1041
|
+
changePercent
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return { value };
|
|
1046
|
+
}
|
|
1047
|
+
async function getFeedbackBreakdown(db, args) {
|
|
1048
|
+
const aggSql = getAggregationSql$2(args.aggregation);
|
|
1049
|
+
const { clause, params } = buildFeedbackWhereClause(args, true);
|
|
1050
|
+
const resolvedGroupBy = resolveFeedbackGroupBy(args.groupBy);
|
|
1051
|
+
const sql = `SELECT ${resolvedGroupBy.map((entry) => entry.selectSql).join(", ")}, ${aggSql} AS value FROM feedback_events ${clause} GROUP BY ${resolvedGroupBy.map((entry) => entry.groupSql).join(", ")} ORDER BY value DESC`;
|
|
1052
|
+
return { groups: (await db.query(sql, params)).map((row) => ({
|
|
1053
|
+
dimensions: Object.fromEntries(resolvedGroupBy.map((entry, index) => {
|
|
1054
|
+
const value = row[`group_by_${index}`];
|
|
1055
|
+
return [entry.key, value === null || value === void 0 ? null : String(value)];
|
|
1056
|
+
})),
|
|
1057
|
+
value: Number(row.value ?? 0)
|
|
1058
|
+
})) };
|
|
1059
|
+
}
|
|
1060
|
+
async function getFeedbackTimeSeries(db, args) {
|
|
1061
|
+
const aggSql = getAggregationSql$2(args.aggregation);
|
|
1062
|
+
const intervalSql = getIntervalSql$2(args.interval);
|
|
1063
|
+
const { clause, params } = buildFeedbackWhereClause(args, true);
|
|
1064
|
+
if (args.groupBy && args.groupBy.length > 0) {
|
|
1065
|
+
const resolvedGroupBy = resolveFeedbackGroupBy(args.groupBy);
|
|
1066
|
+
const sql = `
|
|
1067
|
+
SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
|
|
1068
|
+
${resolvedGroupBy.map((entry) => entry.selectSql).join(", ")},
|
|
1069
|
+
${aggSql} AS value
|
|
1070
|
+
FROM feedback_events ${clause}
|
|
1071
|
+
GROUP BY bucket, ${resolvedGroupBy.map((entry) => entry.groupSql).join(", ")}
|
|
1072
|
+
ORDER BY bucket
|
|
1073
|
+
`;
|
|
1074
|
+
const rows = await db.query(sql, params);
|
|
1075
|
+
const seriesMap = /* @__PURE__ */ new Map();
|
|
1076
|
+
for (const row of rows) {
|
|
1077
|
+
const groupValues = resolvedGroupBy.map((_, index) => row[`group_by_${index}`]);
|
|
1078
|
+
const key = JSON.stringify(groupValues);
|
|
1079
|
+
if (!seriesMap.has(key)) seriesMap.set(key, {
|
|
1080
|
+
name: toSeriesName$1(groupValues),
|
|
1081
|
+
points: []
|
|
1082
|
+
});
|
|
1083
|
+
seriesMap.get(key).points.push({
|
|
1084
|
+
timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
|
|
1085
|
+
value: Number(row.value ?? 0)
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
return { series: Array.from(seriesMap.values()) };
|
|
1089
|
+
}
|
|
1090
|
+
const rows = await db.query(`
|
|
1091
|
+
SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
|
|
1092
|
+
${aggSql} AS value
|
|
1093
|
+
FROM feedback_events ${clause}
|
|
1094
|
+
GROUP BY bucket
|
|
1095
|
+
ORDER BY bucket
|
|
1096
|
+
`, params);
|
|
1097
|
+
return { series: [{
|
|
1098
|
+
name: args.feedbackSource ? `${args.feedbackType}|${args.feedbackSource}` : args.feedbackType,
|
|
1099
|
+
points: rows.map((row) => ({
|
|
1100
|
+
timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
|
|
1101
|
+
value: Number(row.value ?? 0)
|
|
1102
|
+
}))
|
|
1103
|
+
}] };
|
|
1104
|
+
}
|
|
1105
|
+
async function getFeedbackPercentiles(db, args) {
|
|
1106
|
+
const intervalSql = getIntervalSql$2(args.interval);
|
|
1107
|
+
const { clause, params } = buildFeedbackWhereClause(args, true);
|
|
1108
|
+
const percentiles = getValidatedPercentiles$1(args.percentiles);
|
|
1109
|
+
const series = [];
|
|
1110
|
+
for (const percentile of percentiles) {
|
|
1111
|
+
const rows = await db.query(`
|
|
1112
|
+
SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
|
|
1113
|
+
percentile_cont(${percentile}) WITHIN GROUP (ORDER BY TRY_CAST(value AS DOUBLE)) AS pvalue
|
|
1114
|
+
FROM feedback_events ${clause}
|
|
1115
|
+
GROUP BY bucket
|
|
1116
|
+
ORDER BY bucket
|
|
1117
|
+
`, params);
|
|
1118
|
+
series.push({
|
|
1119
|
+
percentile,
|
|
1120
|
+
points: rows.map((row) => ({
|
|
1121
|
+
timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
|
|
1122
|
+
value: Number(row.pvalue ?? 0)
|
|
1123
|
+
}))
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
return { series };
|
|
1127
|
+
}
|
|
1128
|
+
//#endregion
|
|
1129
|
+
//#region src/storage/domains/observability/logs.ts
|
|
1130
|
+
const COLUMNS_SQL$1 = [
|
|
1131
|
+
"logId",
|
|
1132
|
+
"timestamp",
|
|
1133
|
+
"cursorId",
|
|
1134
|
+
"level",
|
|
1135
|
+
"message",
|
|
1136
|
+
"data",
|
|
1137
|
+
"traceId",
|
|
1138
|
+
"spanId",
|
|
1139
|
+
"entityType",
|
|
1140
|
+
"entityId",
|
|
1141
|
+
"entityName",
|
|
1142
|
+
"entityVersionId",
|
|
1143
|
+
"parentEntityVersionId",
|
|
1144
|
+
"parentEntityType",
|
|
1145
|
+
"parentEntityId",
|
|
1146
|
+
"parentEntityName",
|
|
1147
|
+
"rootEntityVersionId",
|
|
1148
|
+
"rootEntityType",
|
|
1149
|
+
"rootEntityId",
|
|
1150
|
+
"rootEntityName",
|
|
1151
|
+
"userId",
|
|
1152
|
+
"organizationId",
|
|
1153
|
+
"resourceId",
|
|
1154
|
+
"runId",
|
|
1155
|
+
"sessionId",
|
|
1156
|
+
"threadId",
|
|
1157
|
+
"requestId",
|
|
1158
|
+
"environment",
|
|
1159
|
+
"executionSource",
|
|
1160
|
+
"serviceName",
|
|
1161
|
+
"experimentId",
|
|
1162
|
+
"tags",
|
|
1163
|
+
"metadata",
|
|
1164
|
+
"scope"
|
|
1165
|
+
].join(", ");
|
|
1166
|
+
function rowToLogRecord(row) {
|
|
1167
|
+
return {
|
|
1168
|
+
logId: row.logId,
|
|
1169
|
+
timestamp: toDate(row.timestamp),
|
|
1170
|
+
level: row.level,
|
|
1171
|
+
message: row.message,
|
|
1172
|
+
data: parseJson(row.data),
|
|
1173
|
+
traceId: row.traceId ?? null,
|
|
1174
|
+
spanId: row.spanId ?? null,
|
|
1175
|
+
entityType: row.entityType ?? null,
|
|
1176
|
+
entityId: row.entityId ?? null,
|
|
1177
|
+
entityName: row.entityName ?? null,
|
|
1178
|
+
entityVersionId: row.entityVersionId ?? null,
|
|
1179
|
+
parentEntityVersionId: row.parentEntityVersionId ?? null,
|
|
1180
|
+
parentEntityType: row.parentEntityType ?? null,
|
|
1181
|
+
parentEntityId: row.parentEntityId ?? null,
|
|
1182
|
+
parentEntityName: row.parentEntityName ?? null,
|
|
1183
|
+
rootEntityVersionId: row.rootEntityVersionId ?? null,
|
|
1184
|
+
rootEntityType: row.rootEntityType ?? null,
|
|
1185
|
+
rootEntityId: row.rootEntityId ?? null,
|
|
1186
|
+
rootEntityName: row.rootEntityName ?? null,
|
|
1187
|
+
userId: row.userId ?? null,
|
|
1188
|
+
organizationId: row.organizationId ?? null,
|
|
1189
|
+
resourceId: row.resourceId ?? null,
|
|
1190
|
+
runId: row.runId ?? null,
|
|
1191
|
+
sessionId: row.sessionId ?? null,
|
|
1192
|
+
threadId: row.threadId ?? null,
|
|
1193
|
+
requestId: row.requestId ?? null,
|
|
1194
|
+
environment: row.environment ?? null,
|
|
1195
|
+
executionSource: row.executionSource ?? null,
|
|
1196
|
+
serviceName: row.serviceName ?? null,
|
|
1197
|
+
experimentId: row.experimentId ?? null,
|
|
1198
|
+
tags: parseJsonArray(row.tags),
|
|
1199
|
+
metadata: parseJson(row.metadata),
|
|
1200
|
+
scope: parseJson(row.scope)
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
/** Insert multiple log events in a single statement. */
|
|
1204
|
+
async function batchCreateLogs(db, args) {
|
|
1205
|
+
if (args.logs.length === 0) return;
|
|
1206
|
+
const tuples = args.logs.map((log) => {
|
|
1207
|
+
return `(${[
|
|
1208
|
+
v(log.logId),
|
|
1209
|
+
v(log.timestamp),
|
|
1210
|
+
"nextval('log_events_cursor_id_seq')",
|
|
1211
|
+
v(log.level),
|
|
1212
|
+
v(log.message),
|
|
1213
|
+
jsonV(log.data),
|
|
1214
|
+
v(log.traceId ?? null),
|
|
1215
|
+
v(log.spanId ?? null),
|
|
1216
|
+
v(log.entityType ?? null),
|
|
1217
|
+
v(log.entityId ?? null),
|
|
1218
|
+
v(log.entityName ?? null),
|
|
1219
|
+
v(log.entityVersionId ?? null),
|
|
1220
|
+
v(log.parentEntityVersionId ?? null),
|
|
1221
|
+
v(log.parentEntityType ?? null),
|
|
1222
|
+
v(log.parentEntityId ?? null),
|
|
1223
|
+
v(log.parentEntityName ?? null),
|
|
1224
|
+
v(log.rootEntityVersionId ?? null),
|
|
1225
|
+
v(log.rootEntityType ?? null),
|
|
1226
|
+
v(log.rootEntityId ?? null),
|
|
1227
|
+
v(log.rootEntityName ?? null),
|
|
1228
|
+
v(log.userId ?? null),
|
|
1229
|
+
v(log.organizationId ?? null),
|
|
1230
|
+
v(log.resourceId ?? null),
|
|
1231
|
+
v(log.runId ?? null),
|
|
1232
|
+
v(log.sessionId ?? null),
|
|
1233
|
+
v(log.threadId ?? null),
|
|
1234
|
+
v(log.requestId ?? null),
|
|
1235
|
+
v(log.environment ?? null),
|
|
1236
|
+
v(log.executionSource ?? null),
|
|
1237
|
+
v(log.serviceName ?? null),
|
|
1238
|
+
v(log.experimentId ?? null),
|
|
1239
|
+
jsonV(log.tags),
|
|
1240
|
+
jsonV(log.metadata),
|
|
1241
|
+
jsonV(log.scope)
|
|
1242
|
+
].join(", ")})`;
|
|
1243
|
+
});
|
|
1244
|
+
await db.execute(`INSERT INTO log_events (${COLUMNS_SQL$1}) VALUES ${tuples.join(",\n")} ON CONFLICT DO NOTHING`);
|
|
1245
|
+
}
|
|
1246
|
+
/** Query log events with filtering, ordering, and pagination. */
|
|
1247
|
+
async function listLogs(db, args) {
|
|
1248
|
+
const { mode, filters, pagination, orderBy, after, limit } = listLogsArgsSchema.parse(args);
|
|
1249
|
+
const filterRecord = filters;
|
|
1250
|
+
const page = Number(pagination.page);
|
|
1251
|
+
const perPage = Number(pagination.perPage);
|
|
1252
|
+
const { clause: filterClause, params: filterParams } = buildWhereClause(filterRecord);
|
|
1253
|
+
if (mode === "delta") {
|
|
1254
|
+
assertDeltaPollingEnabled();
|
|
1255
|
+
const streamHeadCursor = await getStreamHeadCursor$2(db);
|
|
1256
|
+
if (after === void 0) return {
|
|
1257
|
+
logs: [],
|
|
1258
|
+
delta: {
|
|
1259
|
+
limit,
|
|
1260
|
+
hasMore: false
|
|
1261
|
+
},
|
|
1262
|
+
deltaCursor: streamHeadCursor
|
|
1263
|
+
};
|
|
1264
|
+
const afterCursorId = validateCursorId(after);
|
|
1265
|
+
const deltaWhereClause = extendWhereClause(filterClause, ["cursorId IS NOT NULL", `cursorId > CAST(? AS BIGINT)`]);
|
|
1266
|
+
const rows = await db.query(`SELECT * FROM log_events ${deltaWhereClause} ORDER BY cursorId ASC LIMIT ?`, [
|
|
1267
|
+
...filterParams,
|
|
1268
|
+
afterCursorId,
|
|
1269
|
+
limit + 1
|
|
1270
|
+
]);
|
|
1271
|
+
const visibleRows = rows.slice(0, limit).map((row) => ({
|
|
1272
|
+
cursorId: row.cursorId,
|
|
1273
|
+
log: rowToLogRecord(row)
|
|
1274
|
+
}));
|
|
1275
|
+
return {
|
|
1276
|
+
logs: visibleRows.map((row) => row.log),
|
|
1277
|
+
delta: {
|
|
1278
|
+
limit,
|
|
1279
|
+
hasMore: rows.length > limit
|
|
1280
|
+
},
|
|
1281
|
+
deltaCursor: visibleRows.length > 0 ? encodeDeltaCursor(visibleRows[visibleRows.length - 1]?.cursorId) : streamHeadCursor
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
const orderByClause = buildOrderByClause(orderBy);
|
|
1285
|
+
const { clause: paginationClause, params: paginationParams } = buildPaginationClause({
|
|
1286
|
+
page,
|
|
1287
|
+
perPage
|
|
1288
|
+
});
|
|
1289
|
+
const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getDeltaCursor$2(db, filterClause, filterParams) : void 0;
|
|
1290
|
+
const countResult = await db.query(`SELECT COUNT(*) as total FROM log_events ${filterClause}`, filterParams);
|
|
1291
|
+
const total = Number(countResult[0]?.total ?? 0);
|
|
1292
|
+
const logs = (await db.query(`SELECT * FROM log_events ${filterClause} ${orderByClause} ${paginationClause}`, [...filterParams, ...paginationParams])).map((row) => rowToLogRecord(row));
|
|
1293
|
+
return {
|
|
1294
|
+
pagination: {
|
|
1295
|
+
total,
|
|
1296
|
+
page,
|
|
1297
|
+
perPage,
|
|
1298
|
+
hasMore: (page + 1) * perPage < total
|
|
1299
|
+
},
|
|
1300
|
+
logs,
|
|
1301
|
+
...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
async function getDeltaCursor$2(db, filterClause, filterParams) {
|
|
1305
|
+
const cursorId = (await db.query(`SELECT max(cursorId) AS cursorId FROM log_events ${filterClause}`, filterParams))[0]?.cursorId;
|
|
1306
|
+
if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
|
|
1307
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM log_events`))[0]?.cursorId);
|
|
1308
|
+
}
|
|
1309
|
+
async function getStreamHeadCursor$2(db) {
|
|
1310
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM log_events`))[0]?.cursorId);
|
|
1311
|
+
}
|
|
1312
|
+
//#endregion
|
|
1313
|
+
//#region src/storage/domains/observability/metrics.ts
|
|
1314
|
+
function resolveDistinctColumnSql(distinctColumn) {
|
|
1315
|
+
if (!distinctColumn) throw new Error(`count_distinct aggregation requires a 'distinctColumn' argument`);
|
|
1316
|
+
if (!METRIC_DISTINCT_COLUMNS.includes(distinctColumn)) throw new Error(`Invalid distinctColumn: ${distinctColumn}`);
|
|
1317
|
+
return parseFieldKey(distinctColumn);
|
|
1318
|
+
}
|
|
1319
|
+
function getAggregationSql$1(aggregation, measure = "value", distinctColumn) {
|
|
1320
|
+
switch (aggregation) {
|
|
1321
|
+
case "sum": return `SUM(${measure})`;
|
|
1322
|
+
case "avg": return `AVG(${measure})`;
|
|
1323
|
+
case "min": return `MIN(${measure})`;
|
|
1324
|
+
case "max": return `MAX(${measure})`;
|
|
1325
|
+
case "count": return `CAST(COUNT(${measure}) AS DOUBLE)`;
|
|
1326
|
+
case "count_distinct": return `CAST(approx_count_distinct(${resolveDistinctColumnSql(distinctColumn)}) AS DOUBLE)`;
|
|
1327
|
+
case "last": return `arg_max(${measure}, timestamp)`;
|
|
1328
|
+
default: return `SUM(${measure})`;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function getIntervalSql$1(interval) {
|
|
1332
|
+
switch (interval) {
|
|
1333
|
+
case "1m": return "1 minute";
|
|
1334
|
+
case "5m": return "5 minutes";
|
|
1335
|
+
case "15m": return "15 minutes";
|
|
1336
|
+
case "1h": return "1 hour";
|
|
1337
|
+
case "1d": return "1 day";
|
|
1338
|
+
default: return "1 hour";
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
function buildMetricNameFilter(name) {
|
|
1342
|
+
if (Array.isArray(name)) return {
|
|
1343
|
+
clause: `name IN (${name.map(() => "?").join(", ")})`,
|
|
1344
|
+
params: name
|
|
1345
|
+
};
|
|
1346
|
+
return {
|
|
1347
|
+
clause: `name = ?`,
|
|
1348
|
+
params: [name]
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
const METRIC_COLUMNS = [
|
|
1352
|
+
"metricId",
|
|
1353
|
+
"timestamp",
|
|
1354
|
+
"cursorId",
|
|
1355
|
+
"name",
|
|
1356
|
+
"value",
|
|
1357
|
+
"traceId",
|
|
1358
|
+
"spanId",
|
|
1359
|
+
"entityType",
|
|
1360
|
+
"entityId",
|
|
1361
|
+
"entityName",
|
|
1362
|
+
"entityVersionId",
|
|
1363
|
+
"parentEntityVersionId",
|
|
1364
|
+
"parentEntityType",
|
|
1365
|
+
"parentEntityId",
|
|
1366
|
+
"parentEntityName",
|
|
1367
|
+
"rootEntityVersionId",
|
|
1368
|
+
"rootEntityType",
|
|
1369
|
+
"rootEntityId",
|
|
1370
|
+
"rootEntityName",
|
|
1371
|
+
"userId",
|
|
1372
|
+
"organizationId",
|
|
1373
|
+
"resourceId",
|
|
1374
|
+
"runId",
|
|
1375
|
+
"sessionId",
|
|
1376
|
+
"threadId",
|
|
1377
|
+
"requestId",
|
|
1378
|
+
"environment",
|
|
1379
|
+
"executionSource",
|
|
1380
|
+
"serviceName",
|
|
1381
|
+
"experimentId",
|
|
1382
|
+
"provider",
|
|
1383
|
+
"model",
|
|
1384
|
+
"estimatedCost",
|
|
1385
|
+
"costUnit",
|
|
1386
|
+
"tags",
|
|
1387
|
+
"labels",
|
|
1388
|
+
"costMetadata",
|
|
1389
|
+
"metadata",
|
|
1390
|
+
"scope"
|
|
1391
|
+
];
|
|
1392
|
+
const METRIC_COLUMNS_SQL = METRIC_COLUMNS.join(", ");
|
|
1393
|
+
const METRIC_COLUMN_SET = new Set(METRIC_COLUMNS);
|
|
1394
|
+
const METRIC_LABEL_ONLY_GROUP_BY_EXCLUDED = /* @__PURE__ */ new Set([
|
|
1395
|
+
"metadata",
|
|
1396
|
+
"scope",
|
|
1397
|
+
"costMetadata",
|
|
1398
|
+
"tags"
|
|
1399
|
+
]);
|
|
1400
|
+
function buildGroupByAlias(index) {
|
|
1401
|
+
return `group_by_${index}`;
|
|
1402
|
+
}
|
|
1403
|
+
function toSeriesDisplayValue(value) {
|
|
1404
|
+
return value === null || value === void 0 ? "" : String(value);
|
|
1405
|
+
}
|
|
1406
|
+
function getCostSummarySelect(prefix = "") {
|
|
1407
|
+
const ref = (column) => `${prefix}${column}`;
|
|
1408
|
+
return [
|
|
1409
|
+
`SUM(${ref("estimatedCost")}) FILTER (WHERE ${ref("estimatedCost")} IS NOT NULL) AS estimatedCost`,
|
|
1410
|
+
`,`,
|
|
1411
|
+
`CASE`,
|
|
1412
|
+
` WHEN COUNT(DISTINCT ${ref("costUnit")}) FILTER (WHERE ${ref("costUnit")} IS NOT NULL) = 1`,
|
|
1413
|
+
` THEN MIN(${ref("costUnit")}) FILTER (WHERE ${ref("costUnit")} IS NOT NULL)`,
|
|
1414
|
+
` ELSE NULL`,
|
|
1415
|
+
`END AS costUnit`
|
|
1416
|
+
].join(" ");
|
|
1417
|
+
}
|
|
1418
|
+
function normalizeCostSummaryRow(row) {
|
|
1419
|
+
return {
|
|
1420
|
+
estimatedCost: row.estimatedCost === null || row.estimatedCost === void 0 ? null : Number(row.estimatedCost),
|
|
1421
|
+
costUnit: row.costUnit === null || row.costUnit === void 0 ? null : String(row.costUnit)
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
function buildCombinedWhereClause(nameClause, nameParams, filterClause, filterParams) {
|
|
1425
|
+
const conditions = [nameClause];
|
|
1426
|
+
const params = [...nameParams];
|
|
1427
|
+
if (filterClause) {
|
|
1428
|
+
conditions.push(filterClause.replace("WHERE ", ""));
|
|
1429
|
+
params.push(...filterParams);
|
|
1430
|
+
}
|
|
1431
|
+
return {
|
|
1432
|
+
clause: `WHERE ${conditions.join(" AND ")}`,
|
|
1433
|
+
params
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
function resolveGroupBy(groupBy) {
|
|
1437
|
+
return groupBy.map((key, index) => {
|
|
1438
|
+
if (METRIC_COLUMN_SET.has(key)) {
|
|
1439
|
+
const parsed = parseFieldKey(key);
|
|
1440
|
+
if (METRIC_LABEL_ONLY_GROUP_BY_EXCLUDED.has(parsed)) throw new Error(`Invalid groupBy column(s): ${key}`);
|
|
1441
|
+
return {
|
|
1442
|
+
kind: "column",
|
|
1443
|
+
key,
|
|
1444
|
+
selectSql: `${parsed} AS "${key}"`,
|
|
1445
|
+
groupSql: parsed,
|
|
1446
|
+
resultKey: key
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
const labelExpr = `json_extract_string(labels, '${buildJsonPath(key).replace(/'/g, "''")}')`;
|
|
1450
|
+
const alias = buildGroupByAlias(index);
|
|
1451
|
+
return {
|
|
1452
|
+
kind: "label",
|
|
1453
|
+
key,
|
|
1454
|
+
selectSql: `${labelExpr} AS ${alias}`,
|
|
1455
|
+
groupSql: alias,
|
|
1456
|
+
resultKey: alias
|
|
1457
|
+
};
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
function rowToMetricRecord(row) {
|
|
1461
|
+
return {
|
|
1462
|
+
metricId: row.metricId,
|
|
1463
|
+
timestamp: toDate(row.timestamp),
|
|
1464
|
+
name: row.name,
|
|
1465
|
+
value: Number(row.value),
|
|
1466
|
+
traceId: row.traceId ?? null,
|
|
1467
|
+
spanId: row.spanId ?? null,
|
|
1468
|
+
entityType: row.entityType ?? null,
|
|
1469
|
+
entityId: row.entityId ?? null,
|
|
1470
|
+
entityName: row.entityName ?? null,
|
|
1471
|
+
entityVersionId: row.entityVersionId ?? null,
|
|
1472
|
+
parentEntityVersionId: row.parentEntityVersionId ?? null,
|
|
1473
|
+
parentEntityType: row.parentEntityType ?? null,
|
|
1474
|
+
parentEntityId: row.parentEntityId ?? null,
|
|
1475
|
+
parentEntityName: row.parentEntityName ?? null,
|
|
1476
|
+
rootEntityVersionId: row.rootEntityVersionId ?? null,
|
|
1477
|
+
rootEntityType: row.rootEntityType ?? null,
|
|
1478
|
+
rootEntityId: row.rootEntityId ?? null,
|
|
1479
|
+
rootEntityName: row.rootEntityName ?? null,
|
|
1480
|
+
userId: row.userId ?? null,
|
|
1481
|
+
organizationId: row.organizationId ?? null,
|
|
1482
|
+
resourceId: row.resourceId ?? null,
|
|
1483
|
+
runId: row.runId ?? null,
|
|
1484
|
+
sessionId: row.sessionId ?? null,
|
|
1485
|
+
threadId: row.threadId ?? null,
|
|
1486
|
+
requestId: row.requestId ?? null,
|
|
1487
|
+
environment: row.environment ?? null,
|
|
1488
|
+
executionSource: row.executionSource ?? null,
|
|
1489
|
+
serviceName: row.serviceName ?? null,
|
|
1490
|
+
experimentId: row.experimentId ?? null,
|
|
1491
|
+
provider: row.provider ?? null,
|
|
1492
|
+
model: row.model ?? null,
|
|
1493
|
+
estimatedCost: row.estimatedCost === null || row.estimatedCost === void 0 ? null : Number(row.estimatedCost),
|
|
1494
|
+
costUnit: row.costUnit ?? null,
|
|
1495
|
+
costMetadata: parseJson(row.costMetadata),
|
|
1496
|
+
tags: parseJsonArray(row.tags),
|
|
1497
|
+
labels: parseJson(row.labels) ?? {},
|
|
1498
|
+
metadata: parseJson(row.metadata),
|
|
1499
|
+
scope: parseJson(row.scope)
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
/** Insert multiple metric events in a single statement. */
|
|
1503
|
+
async function batchCreateMetrics(db, args) {
|
|
1504
|
+
if (args.metrics.length === 0) return;
|
|
1505
|
+
const tuples = args.metrics.map((m) => {
|
|
1506
|
+
return `(${[
|
|
1507
|
+
v(m.metricId),
|
|
1508
|
+
v(m.timestamp),
|
|
1509
|
+
"nextval('metric_events_cursor_id_seq')",
|
|
1510
|
+
v(m.name),
|
|
1511
|
+
v(m.value),
|
|
1512
|
+
v(m.traceId ?? null),
|
|
1513
|
+
v(m.spanId ?? null),
|
|
1514
|
+
v(m.entityType ?? null),
|
|
1515
|
+
v(m.entityId ?? null),
|
|
1516
|
+
v(m.entityName ?? null),
|
|
1517
|
+
v(m.entityVersionId ?? null),
|
|
1518
|
+
v(m.parentEntityVersionId ?? null),
|
|
1519
|
+
v(m.parentEntityType ?? null),
|
|
1520
|
+
v(m.parentEntityId ?? null),
|
|
1521
|
+
v(m.parentEntityName ?? null),
|
|
1522
|
+
v(m.rootEntityVersionId ?? null),
|
|
1523
|
+
v(m.rootEntityType ?? null),
|
|
1524
|
+
v(m.rootEntityId ?? null),
|
|
1525
|
+
v(m.rootEntityName ?? null),
|
|
1526
|
+
v(m.userId ?? null),
|
|
1527
|
+
v(m.organizationId ?? null),
|
|
1528
|
+
v(m.resourceId ?? null),
|
|
1529
|
+
v(m.runId ?? null),
|
|
1530
|
+
v(m.sessionId ?? null),
|
|
1531
|
+
v(m.threadId ?? null),
|
|
1532
|
+
v(m.requestId ?? null),
|
|
1533
|
+
v(m.environment ?? null),
|
|
1534
|
+
v(m.executionSource ?? null),
|
|
1535
|
+
v(m.serviceName ?? null),
|
|
1536
|
+
v(m.experimentId ?? null),
|
|
1537
|
+
v(m.provider ?? null),
|
|
1538
|
+
v(m.model ?? null),
|
|
1539
|
+
v(m.estimatedCost ?? null),
|
|
1540
|
+
v(m.costUnit ?? null),
|
|
1541
|
+
jsonV(m.tags ?? null),
|
|
1542
|
+
v(JSON.stringify(m.labels ?? {})),
|
|
1543
|
+
jsonV(m.costMetadata ?? null),
|
|
1544
|
+
jsonV(m.metadata ?? null),
|
|
1545
|
+
jsonV(m.scope ?? null)
|
|
1546
|
+
].join(", ")})`;
|
|
1547
|
+
});
|
|
1548
|
+
await db.execute(`INSERT INTO metric_events (${METRIC_COLUMNS_SQL}) VALUES ${tuples.join(",\n")} ON CONFLICT DO NOTHING`);
|
|
1549
|
+
}
|
|
1550
|
+
/** Query metric events with filtering, ordering, and pagination. */
|
|
1551
|
+
async function listMetrics(db, args) {
|
|
1552
|
+
const { mode, filters, pagination, orderBy, after, limit } = listMetricsArgsSchema.parse(args);
|
|
1553
|
+
const filterRecord = filters;
|
|
1554
|
+
const page = Number(pagination.page);
|
|
1555
|
+
const perPage = Number(pagination.perPage);
|
|
1556
|
+
const { clause: filterClause, params: filterParams } = buildWhereClause(filterRecord);
|
|
1557
|
+
if (mode === "delta") {
|
|
1558
|
+
assertDeltaPollingEnabled();
|
|
1559
|
+
const streamHeadCursor = await getStreamHeadCursor$1(db);
|
|
1560
|
+
if (after === void 0) return {
|
|
1561
|
+
metrics: [],
|
|
1562
|
+
delta: {
|
|
1563
|
+
limit,
|
|
1564
|
+
hasMore: false
|
|
1565
|
+
},
|
|
1566
|
+
deltaCursor: streamHeadCursor
|
|
1567
|
+
};
|
|
1568
|
+
const afterCursorId = validateCursorId(after);
|
|
1569
|
+
const deltaWhereClause = extendWhereClause(filterClause, ["cursorId IS NOT NULL", `cursorId > CAST(? AS BIGINT)`]);
|
|
1570
|
+
const rows = await db.query(`SELECT * FROM metric_events ${deltaWhereClause} ORDER BY cursorId ASC LIMIT ?`, [
|
|
1571
|
+
...filterParams,
|
|
1572
|
+
afterCursorId,
|
|
1573
|
+
limit + 1
|
|
1574
|
+
]);
|
|
1575
|
+
const visibleRows = rows.slice(0, limit).map((row) => ({
|
|
1576
|
+
cursorId: row.cursorId,
|
|
1577
|
+
metric: rowToMetricRecord(row)
|
|
1578
|
+
}));
|
|
1579
|
+
return {
|
|
1580
|
+
metrics: visibleRows.map((row) => row.metric),
|
|
1581
|
+
delta: {
|
|
1582
|
+
limit,
|
|
1583
|
+
hasMore: rows.length > limit
|
|
1584
|
+
},
|
|
1585
|
+
deltaCursor: visibleRows.length > 0 ? encodeDeltaCursor(visibleRows[visibleRows.length - 1]?.cursorId) : streamHeadCursor
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
const orderByClause = buildOrderByClause(orderBy);
|
|
1589
|
+
const { clause: paginationClause, params: paginationParams } = buildPaginationClause({
|
|
1590
|
+
page,
|
|
1591
|
+
perPage
|
|
1592
|
+
});
|
|
1593
|
+
const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getDeltaCursor$1(db, filterClause, filterParams) : void 0;
|
|
1594
|
+
const countResult = await db.query(`SELECT COUNT(*) AS total FROM metric_events ${filterClause}`, filterParams);
|
|
1595
|
+
const total = Number(countResult[0]?.total ?? 0);
|
|
1596
|
+
const rows = await db.query(`SELECT * FROM metric_events ${filterClause} ${orderByClause} ${paginationClause}`, [...filterParams, ...paginationParams]);
|
|
1597
|
+
return {
|
|
1598
|
+
pagination: {
|
|
1599
|
+
total,
|
|
1600
|
+
page,
|
|
1601
|
+
perPage,
|
|
1602
|
+
hasMore: (page + 1) * perPage < total
|
|
1603
|
+
},
|
|
1604
|
+
metrics: rows.map((row) => rowToMetricRecord(row)),
|
|
1605
|
+
...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
async function getDeltaCursor$1(db, filterClause, filterParams) {
|
|
1609
|
+
const cursorId = (await db.query(`SELECT max(cursorId) AS cursorId FROM metric_events ${filterClause}`, filterParams))[0]?.cursorId;
|
|
1610
|
+
if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
|
|
1611
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM metric_events`))[0]?.cursorId);
|
|
1612
|
+
}
|
|
1613
|
+
async function getStreamHeadCursor$1(db) {
|
|
1614
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM metric_events`))[0]?.cursorId);
|
|
1615
|
+
}
|
|
1616
|
+
/** Compute an aggregate value (sum, avg, min, max, etc.) for a metric, with optional period comparison. */
|
|
1617
|
+
async function getMetricAggregate(db, args) {
|
|
1618
|
+
const aggSql = getAggregationSql$1(args.aggregation, "value", args.distinctColumn);
|
|
1619
|
+
const { clause: nameClause, params: nameParams } = buildMetricNameFilter(args.name);
|
|
1620
|
+
const { clause: filterClause, params: filterParams } = buildWhereClause(args.filters);
|
|
1621
|
+
const { clause: whereClause, params: allParams } = buildCombinedWhereClause(nameClause, nameParams, filterClause, filterParams);
|
|
1622
|
+
const sql = `SELECT ${aggSql} AS value, ${getCostSummarySelect()} FROM metric_events ${whereClause}`;
|
|
1623
|
+
const row = (await db.query(sql, allParams))[0] ?? {};
|
|
1624
|
+
const value = row.value === null || row.value === void 0 ? null : Number(row.value);
|
|
1625
|
+
const costSummary = normalizeCostSummaryRow(row);
|
|
1626
|
+
if (args.comparePeriod && args.filters?.timestamp) {
|
|
1627
|
+
const ts = args.filters.timestamp;
|
|
1628
|
+
if (ts.start && ts.end) {
|
|
1629
|
+
const duration = ts.end.getTime() - ts.start.getTime();
|
|
1630
|
+
let prevStart;
|
|
1631
|
+
let prevEnd;
|
|
1632
|
+
switch (args.comparePeriod) {
|
|
1633
|
+
case "previous_period":
|
|
1634
|
+
prevStart = new Date(ts.start.getTime() - duration);
|
|
1635
|
+
prevEnd = new Date(ts.end.getTime() - duration);
|
|
1636
|
+
break;
|
|
1637
|
+
case "previous_day":
|
|
1638
|
+
prevStart = /* @__PURE__ */ new Date(ts.start.getTime() - 864e5);
|
|
1639
|
+
prevEnd = /* @__PURE__ */ new Date(ts.end.getTime() - 864e5);
|
|
1640
|
+
break;
|
|
1641
|
+
case "previous_week":
|
|
1642
|
+
prevStart = /* @__PURE__ */ new Date(ts.start.getTime() - 6048e5);
|
|
1643
|
+
prevEnd = /* @__PURE__ */ new Date(ts.end.getTime() - 6048e5);
|
|
1644
|
+
break;
|
|
1645
|
+
default:
|
|
1646
|
+
prevStart = new Date(ts.start.getTime() - duration);
|
|
1647
|
+
prevEnd = new Date(ts.end.getTime() - duration);
|
|
1648
|
+
}
|
|
1649
|
+
const { clause: prevFilterClause, params: prevFilterParams } = buildWhereClause({
|
|
1650
|
+
...args.filters ?? {},
|
|
1651
|
+
timestamp: {
|
|
1652
|
+
start: prevStart,
|
|
1653
|
+
end: prevEnd,
|
|
1654
|
+
startExclusive: ts.startExclusive,
|
|
1655
|
+
endExclusive: ts.endExclusive
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
const { clause: prevWhereClause, params: prevParams } = buildCombinedWhereClause(nameClause, nameParams, prevFilterClause, prevFilterParams);
|
|
1659
|
+
const prevSql = `SELECT ${aggSql} AS value, ${getCostSummarySelect()} FROM metric_events ${prevWhereClause}`;
|
|
1660
|
+
const prevRow = (await db.query(prevSql, prevParams))[0] ?? {};
|
|
1661
|
+
const previousValue = prevRow.value === null || prevRow.value === void 0 ? null : Number(prevRow.value);
|
|
1662
|
+
const previousCostSummary = normalizeCostSummaryRow(prevRow);
|
|
1663
|
+
let changePercent = null;
|
|
1664
|
+
if (previousValue !== null && previousValue !== 0 && value !== null) changePercent = (value - previousValue) / Math.abs(previousValue) * 100;
|
|
1665
|
+
let costChangePercent = null;
|
|
1666
|
+
if (previousCostSummary.estimatedCost !== null && previousCostSummary.estimatedCost !== 0 && costSummary.estimatedCost !== null) costChangePercent = (costSummary.estimatedCost - previousCostSummary.estimatedCost) / Math.abs(previousCostSummary.estimatedCost) * 100;
|
|
1667
|
+
return {
|
|
1668
|
+
value,
|
|
1669
|
+
estimatedCost: costSummary.estimatedCost,
|
|
1670
|
+
costUnit: costSummary.costUnit,
|
|
1671
|
+
previousValue,
|
|
1672
|
+
previousEstimatedCost: previousCostSummary.estimatedCost,
|
|
1673
|
+
changePercent,
|
|
1674
|
+
costChangePercent
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
return {
|
|
1679
|
+
value,
|
|
1680
|
+
estimatedCost: costSummary.estimatedCost,
|
|
1681
|
+
costUnit: costSummary.costUnit
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
/** Aggregate a metric grouped by one or more dimensions. */
|
|
1685
|
+
async function getMetricBreakdown(db, args) {
|
|
1686
|
+
const aggSql = getAggregationSql$1(args.aggregation, "value", args.distinctColumn);
|
|
1687
|
+
const { clause: nameClause, params: nameParams } = buildMetricNameFilter(args.name);
|
|
1688
|
+
const { clause: filterClause, params: filterParams } = buildWhereClause(args.filters);
|
|
1689
|
+
const { clause: whereClause, params: allParams } = buildCombinedWhereClause(nameClause, nameParams, filterClause, filterParams);
|
|
1690
|
+
const resolvedGroupBy = resolveGroupBy(args.groupBy);
|
|
1691
|
+
const selectGroupBy = resolvedGroupBy.map((entry) => entry.selectSql).join(", ");
|
|
1692
|
+
const groupByCols = resolvedGroupBy.map((entry) => entry.groupSql).join(", ");
|
|
1693
|
+
const orderDirection = args.orderDirection === "ASC" ? "ASC" : "DESC";
|
|
1694
|
+
const limitClause = typeof args.limit === "number" ? `LIMIT ?` : "";
|
|
1695
|
+
const limitParams = typeof args.limit === "number" ? [args.limit] : [];
|
|
1696
|
+
const sql = `SELECT ${selectGroupBy}, ${aggSql} AS value, ${getCostSummarySelect()} FROM metric_events ${whereClause} GROUP BY ${groupByCols} ORDER BY value ${orderDirection} ${limitClause}`;
|
|
1697
|
+
return { groups: (await db.query(sql, [...allParams, ...limitParams])).map((row) => {
|
|
1698
|
+
const dimensions = {};
|
|
1699
|
+
for (const entry of resolvedGroupBy) {
|
|
1700
|
+
const value = row[entry.resultKey];
|
|
1701
|
+
dimensions[entry.key] = value === null || value === void 0 ? null : String(value);
|
|
1702
|
+
}
|
|
1703
|
+
const costSummary = normalizeCostSummaryRow(row);
|
|
1704
|
+
return {
|
|
1705
|
+
dimensions,
|
|
1706
|
+
value: Number(row.value ?? 0),
|
|
1707
|
+
estimatedCost: costSummary.estimatedCost,
|
|
1708
|
+
costUnit: costSummary.costUnit
|
|
1709
|
+
};
|
|
1710
|
+
}) };
|
|
1711
|
+
}
|
|
1712
|
+
/** Aggregate a metric into time-bucketed series, with optional group-by dimensions. */
|
|
1713
|
+
async function getMetricTimeSeries(db, args) {
|
|
1714
|
+
const aggSql = getAggregationSql$1(args.aggregation, "value", args.distinctColumn);
|
|
1715
|
+
const intervalSql = getIntervalSql$1(args.interval);
|
|
1716
|
+
const { clause: nameClause, params: nameParams } = buildMetricNameFilter(args.name);
|
|
1717
|
+
const { clause: filterClause, params: filterParams } = buildWhereClause(args.filters);
|
|
1718
|
+
const { clause: whereClause, params: allParams } = buildCombinedWhereClause(nameClause, nameParams, filterClause, filterParams);
|
|
1719
|
+
if (args.groupBy && args.groupBy.length > 0) {
|
|
1720
|
+
const resolvedGroupBy = resolveGroupBy(args.groupBy);
|
|
1721
|
+
const selectGroupBy = resolvedGroupBy.map((entry) => entry.selectSql).join(", ");
|
|
1722
|
+
const groupByCols = resolvedGroupBy.map((entry) => entry.groupSql).join(", ");
|
|
1723
|
+
const sql = `
|
|
1724
|
+
SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
|
|
1725
|
+
${selectGroupBy},
|
|
1726
|
+
${aggSql} AS value,
|
|
1727
|
+
${getCostSummarySelect()}
|
|
1728
|
+
FROM metric_events ${whereClause}
|
|
1729
|
+
GROUP BY bucket, ${groupByCols}
|
|
1730
|
+
ORDER BY bucket
|
|
1731
|
+
`;
|
|
1732
|
+
const rows = await db.query(sql, allParams);
|
|
1733
|
+
const seriesMap = /* @__PURE__ */ new Map();
|
|
1734
|
+
for (const row of rows) {
|
|
1735
|
+
const dimensionValues = resolvedGroupBy.map((entry) => row[entry.resultKey]);
|
|
1736
|
+
const seriesKey = JSON.stringify(dimensionValues);
|
|
1737
|
+
const name = dimensionValues.map(toSeriesDisplayValue).join("|");
|
|
1738
|
+
const costSummary = normalizeCostSummaryRow(row);
|
|
1739
|
+
if (!seriesMap.has(seriesKey)) seriesMap.set(seriesKey, {
|
|
1740
|
+
name,
|
|
1741
|
+
costUnits: /* @__PURE__ */ new Set(),
|
|
1742
|
+
points: []
|
|
1743
|
+
});
|
|
1744
|
+
if (costSummary.costUnit) seriesMap.get(seriesKey).costUnits.add(costSummary.costUnit);
|
|
1745
|
+
seriesMap.get(seriesKey).points.push({
|
|
1746
|
+
timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
|
|
1747
|
+
value: Number(row.value ?? 0),
|
|
1748
|
+
estimatedCost: costSummary.estimatedCost
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
return { series: Array.from(seriesMap.values()).map((series) => ({
|
|
1752
|
+
name: series.name,
|
|
1753
|
+
costUnit: series.costUnits.size === 1 ? Array.from(series.costUnits)[0] : null,
|
|
1754
|
+
points: series.points
|
|
1755
|
+
})) };
|
|
1756
|
+
}
|
|
1757
|
+
const sql = `
|
|
1758
|
+
SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
|
|
1759
|
+
${aggSql} AS value,
|
|
1760
|
+
${getCostSummarySelect()}
|
|
1761
|
+
FROM metric_events ${whereClause}
|
|
1762
|
+
GROUP BY bucket
|
|
1763
|
+
ORDER BY bucket
|
|
1764
|
+
`;
|
|
1765
|
+
const rows = await db.query(sql, allParams);
|
|
1766
|
+
const metricName = Array.isArray(args.name) ? args.name.join(",") : args.name;
|
|
1767
|
+
const overallCostUnits = new Set(rows.map((row) => row.costUnit).filter((value) => typeof value === "string"));
|
|
1768
|
+
return { series: [{
|
|
1769
|
+
name: metricName,
|
|
1770
|
+
costUnit: overallCostUnits.size === 1 ? Array.from(overallCostUnits)[0] : null,
|
|
1771
|
+
points: rows.map((row) => {
|
|
1772
|
+
const costSummary = normalizeCostSummaryRow(row);
|
|
1773
|
+
return {
|
|
1774
|
+
timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
|
|
1775
|
+
value: Number(row.value ?? 0),
|
|
1776
|
+
estimatedCost: costSummary.estimatedCost
|
|
1777
|
+
};
|
|
1778
|
+
})
|
|
1779
|
+
}] };
|
|
1780
|
+
}
|
|
1781
|
+
/** Compute percentile time series for a metric using `percentile_cont`. */
|
|
1782
|
+
async function getMetricPercentiles(db, args) {
|
|
1783
|
+
const intervalSql = getIntervalSql$1(args.interval);
|
|
1784
|
+
const { clause: filterClause, params: filterParams } = buildWhereClause(args.filters);
|
|
1785
|
+
const allConditions = [`name = ?`];
|
|
1786
|
+
const allParams = [args.name];
|
|
1787
|
+
if (filterClause) {
|
|
1788
|
+
allConditions.push(filterClause.replace("WHERE ", ""));
|
|
1789
|
+
allParams.push(...filterParams);
|
|
1790
|
+
}
|
|
1791
|
+
const whereClause = `WHERE ${allConditions.join(" AND ")}`;
|
|
1792
|
+
const series = [];
|
|
1793
|
+
for (const p of args.percentiles) {
|
|
1794
|
+
const sql = `
|
|
1795
|
+
SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
|
|
1796
|
+
percentile_cont(${p}) WITHIN GROUP (ORDER BY value) AS pvalue
|
|
1797
|
+
FROM metric_events ${whereClause}
|
|
1798
|
+
GROUP BY bucket
|
|
1799
|
+
ORDER BY bucket
|
|
1800
|
+
`;
|
|
1801
|
+
const rows = await db.query(sql, allParams);
|
|
1802
|
+
series.push({
|
|
1803
|
+
percentile: p,
|
|
1804
|
+
points: rows.map((row) => ({
|
|
1805
|
+
timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
|
|
1806
|
+
value: Number(row.pvalue ?? 0)
|
|
1807
|
+
}))
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
return { series };
|
|
1811
|
+
}
|
|
1812
|
+
/** Return distinct metric names, optionally filtered by prefix. */
|
|
1813
|
+
async function getMetricNames(db, args) {
|
|
1814
|
+
const conditions = [];
|
|
1815
|
+
const params = [];
|
|
1816
|
+
if (args.prefix) {
|
|
1817
|
+
conditions.push(`name LIKE ?`);
|
|
1818
|
+
params.push(`${args.prefix}%`);
|
|
1819
|
+
}
|
|
1820
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1821
|
+
const limitClause = args.limit ? `LIMIT ?` : "";
|
|
1822
|
+
if (args.limit) params.push(args.limit);
|
|
1823
|
+
return { names: (await db.query(`SELECT DISTINCT name FROM metric_events ${whereClause} ORDER BY name ${limitClause}`, params)).map((r) => r.name) };
|
|
1824
|
+
}
|
|
1825
|
+
/** Return distinct label keys for a given metric name. */
|
|
1826
|
+
async function getMetricLabelKeys(db, args) {
|
|
1827
|
+
return { keys: (await db.query(`SELECT DISTINCT unnest(json_keys(labels)) AS key FROM metric_events WHERE name = ? AND labels IS NOT NULL`, [args.metricName])).map((r) => r.key) };
|
|
1828
|
+
}
|
|
1829
|
+
/** Return distinct values for a specific label key on a metric. */
|
|
1830
|
+
async function getMetricLabelValues(db, args) {
|
|
1831
|
+
const labelPath = buildJsonPath(args.labelKey);
|
|
1832
|
+
const conditions = [`name = ?`, `json_extract_string(labels, ?) IS NOT NULL`];
|
|
1833
|
+
const params = [args.metricName, labelPath];
|
|
1834
|
+
if (args.prefix) {
|
|
1835
|
+
conditions.push(`json_extract_string(labels, ?) LIKE ?`);
|
|
1836
|
+
params.push(labelPath, `${args.prefix}%`);
|
|
1837
|
+
}
|
|
1838
|
+
const limitClause = args.limit ? `LIMIT ?` : "";
|
|
1839
|
+
if (args.limit) params.push(args.limit);
|
|
1840
|
+
return { values: (await db.query(`SELECT DISTINCT json_extract_string(labels, ?) AS val FROM metric_events WHERE ${conditions.join(" AND ")} ORDER BY val ${limitClause}`, [labelPath, ...params])).map((r) => r.val) };
|
|
1841
|
+
}
|
|
1842
|
+
//#endregion
|
|
1843
|
+
//#region src/storage/domains/observability/migration.ts
|
|
1844
|
+
const CURSOR_ID_TABLES = [
|
|
1845
|
+
"span_events",
|
|
1846
|
+
"metric_events",
|
|
1847
|
+
"log_events",
|
|
1848
|
+
"score_events",
|
|
1849
|
+
"feedback_events"
|
|
1850
|
+
];
|
|
1851
|
+
/**
|
|
1852
|
+
* Drop any leftover `DEFAULT nextval(...)` on observability `cursorId` columns.
|
|
1853
|
+
*
|
|
1854
|
+
* A previous version of the migration set this default via
|
|
1855
|
+
* `ALTER COLUMN cursorId SET DEFAULT nextval(...)`. DuckDB WAL replay cannot
|
|
1856
|
+
* bind that function expression before the default database is attached, so
|
|
1857
|
+
* affected databases fail to reopen. Insert paths now write cursor IDs
|
|
1858
|
+
* explicitly, so the catalog default is unnecessary and should be removed.
|
|
1859
|
+
*
|
|
1860
|
+
* We query `information_schema` first and only emit the `ALTER` for tables that
|
|
1861
|
+
* actually carry the bad default; this avoids writing redundant SetDefault
|
|
1862
|
+
* entries to the WAL on every startup for healthy databases.
|
|
1863
|
+
*/
|
|
1864
|
+
async function dropLegacyCursorIdDefaults(db) {
|
|
1865
|
+
const rows = await db.query(`SELECT table_name FROM information_schema.columns
|
|
1866
|
+
WHERE column_name = 'cursorId'
|
|
1867
|
+
AND column_default IS NOT NULL
|
|
1868
|
+
AND table_name IN (${CURSOR_ID_TABLES.map((t) => `'${t}'`).join(", ")})`);
|
|
1869
|
+
if (rows.length === 0) return;
|
|
1870
|
+
await db.executeBatch(rows.map((row) => `ALTER TABLE ${row.table_name} ALTER COLUMN cursorId DROP DEFAULT`));
|
|
1871
|
+
}
|
|
1872
|
+
const SIGNAL_MIGRATIONS = [
|
|
1873
|
+
{
|
|
1874
|
+
table: "metric_events",
|
|
1875
|
+
createDDL: METRIC_EVENTS_DDL,
|
|
1876
|
+
idColumn: "metricId",
|
|
1877
|
+
cursorSequenceDDL: METRIC_EVENTS_CURSOR_SEQUENCE_DDL
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
table: "log_events",
|
|
1881
|
+
createDDL: LOG_EVENTS_DDL,
|
|
1882
|
+
idColumn: "logId",
|
|
1883
|
+
cursorSequenceDDL: LOG_EVENTS_CURSOR_SEQUENCE_DDL
|
|
1884
|
+
},
|
|
1885
|
+
{
|
|
1886
|
+
table: "score_events",
|
|
1887
|
+
createDDL: SCORE_EVENTS_DDL,
|
|
1888
|
+
idColumn: "scoreId",
|
|
1889
|
+
cursorSequenceDDL: SCORE_EVENTS_CURSOR_SEQUENCE_DDL
|
|
1890
|
+
},
|
|
1891
|
+
{
|
|
1892
|
+
table: "feedback_events",
|
|
1893
|
+
createDDL: FEEDBACK_EVENTS_DDL,
|
|
1894
|
+
idColumn: "feedbackId",
|
|
1895
|
+
cursorSequenceDDL: FEEDBACK_EVENTS_CURSOR_SEQUENCE_DDL
|
|
1896
|
+
}
|
|
1897
|
+
];
|
|
1898
|
+
async function tableExists(db, table) {
|
|
1899
|
+
return (await db.query(`SELECT table_name FROM information_schema.tables WHERE table_name = ?`, [table])).length > 0;
|
|
1900
|
+
}
|
|
1901
|
+
async function hasPrimaryKey(db, table) {
|
|
1902
|
+
return (await db.query(`SELECT constraint_type FROM information_schema.table_constraints
|
|
1903
|
+
WHERE table_name = ? AND constraint_type = 'PRIMARY KEY'`, [table])).length > 0;
|
|
1904
|
+
}
|
|
1905
|
+
async function getColumns(db, table) {
|
|
1906
|
+
return (await db.query(`SELECT column_name FROM information_schema.columns WHERE table_name = ?`, [table])).map((r) => r.column_name);
|
|
1907
|
+
}
|
|
1908
|
+
function buildTemporaryTableDDL(createDDL, table, tempTable) {
|
|
1909
|
+
return createDDL.replace(`CREATE TABLE IF NOT EXISTS ${table}`, `CREATE TABLE ${tempTable}`);
|
|
1910
|
+
}
|
|
1911
|
+
async function dropTableIfExists(db, table) {
|
|
1912
|
+
if (await tableExists(db, table)) await db.execute(`DROP TABLE ${table}`);
|
|
1913
|
+
}
|
|
1914
|
+
function createMigrationError(args, error) {
|
|
1915
|
+
return new MastraError({
|
|
1916
|
+
id: createStorageErrorId("DUCKDB", "MIGRATE_SIGNAL_TABLES", "FAILED"),
|
|
1917
|
+
domain: ErrorDomain.STORAGE,
|
|
1918
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
1919
|
+
details: args
|
|
1920
|
+
}, error);
|
|
1921
|
+
}
|
|
1922
|
+
async function checkSignalTablesMigrationStatus(db) {
|
|
1923
|
+
const tables = [];
|
|
1924
|
+
for (const { table, idColumn } of SIGNAL_MIGRATIONS) {
|
|
1925
|
+
if (!await tableExists(db, table)) continue;
|
|
1926
|
+
if (await hasPrimaryKey(db, table)) continue;
|
|
1927
|
+
tables.push({
|
|
1928
|
+
table,
|
|
1929
|
+
idColumn
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
return {
|
|
1933
|
+
needsMigration: tables.length > 0,
|
|
1934
|
+
tables
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Migrate signal tables to a schema with PRIMARY KEY + NOT NULL on the signal ID
|
|
1939
|
+
* without dropping data. Copy-and-swap: create temp → INSERT…SELECT
|
|
1940
|
+
* (generating IDs) → rename old to backup → rename temp to live → drop backup.
|
|
1941
|
+
* The live table is only touched during the final swap step.
|
|
1942
|
+
*/
|
|
1943
|
+
async function migrateSignalTables(db, logger) {
|
|
1944
|
+
for (const { table, createDDL, idColumn, cursorSequenceDDL } of SIGNAL_MIGRATIONS) {
|
|
1945
|
+
if (!await tableExists(db, table)) continue;
|
|
1946
|
+
if (await hasPrimaryKey(db, table)) continue;
|
|
1947
|
+
logger?.info?.(`Migrating ${table} to schema with ${idColumn} PRIMARY KEY`);
|
|
1948
|
+
const temp = `${table}_migrating_${Date.now()}`;
|
|
1949
|
+
const backup = `${table}_backup_${Date.now()}`;
|
|
1950
|
+
let originalRenamed = false;
|
|
1951
|
+
let swapCompleted = false;
|
|
1952
|
+
try {
|
|
1953
|
+
await db.execute(cursorSequenceDDL);
|
|
1954
|
+
await db.execute(buildTemporaryTableDDL(createDDL, table, temp));
|
|
1955
|
+
const newColumns = await getColumns(db, temp);
|
|
1956
|
+
const currentColumns = new Set(await getColumns(db, table));
|
|
1957
|
+
const columnList = newColumns.map((c) => `"${c}"`).join(", ");
|
|
1958
|
+
const selectExprs = newColumns.map((c) => {
|
|
1959
|
+
if (c === idColumn) return currentColumns.has(c) ? `COALESCE(NULLIF("${c}", ''), CAST(uuid() AS VARCHAR)) AS "${c}"` : `CAST(uuid() AS VARCHAR) AS "${c}"`;
|
|
1960
|
+
return currentColumns.has(c) ? `"${c}"` : `NULL AS "${c}"`;
|
|
1961
|
+
}).join(", ");
|
|
1962
|
+
await db.execute(`INSERT INTO ${temp} (${columnList}) SELECT ${selectExprs} FROM ${table}`);
|
|
1963
|
+
await db.execute(`ALTER TABLE ${table} RENAME TO ${backup}`);
|
|
1964
|
+
originalRenamed = true;
|
|
1965
|
+
await db.execute(`ALTER TABLE ${temp} RENAME TO ${table}`);
|
|
1966
|
+
swapCompleted = true;
|
|
1967
|
+
try {
|
|
1968
|
+
await db.execute(`DROP TABLE ${backup}`);
|
|
1969
|
+
} catch (cleanupError) {
|
|
1970
|
+
logger?.warn?.(`Migration of ${table} completed, but failed to drop backup ${backup}: ${cleanupError.message}`);
|
|
1971
|
+
}
|
|
1972
|
+
logger?.info?.(`Successfully migrated ${table}`);
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
logger?.error?.(`Migration of ${table} failed: ${error.message}`);
|
|
1975
|
+
try {
|
|
1976
|
+
await dropTableIfExists(db, temp);
|
|
1977
|
+
} catch (restoreError) {
|
|
1978
|
+
logger?.error?.(`Failed to clean up temporary table ${temp}: ${restoreError.message}`);
|
|
1979
|
+
}
|
|
1980
|
+
if (originalRenamed && !swapCompleted) try {
|
|
1981
|
+
await db.execute(`ALTER TABLE ${backup} RENAME TO ${table}`);
|
|
1982
|
+
} catch (restoreError) {
|
|
1983
|
+
logger?.error?.(`Failed to restore original table ${table} from backup ${backup}: ${restoreError.message}`);
|
|
1984
|
+
}
|
|
1985
|
+
throw createMigrationError({
|
|
1986
|
+
table,
|
|
1987
|
+
idColumn
|
|
1988
|
+
}, error);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
//#endregion
|
|
1993
|
+
//#region src/storage/domains/observability/scores.ts
|
|
1994
|
+
const SCORE_GROUP_BY_COLUMNS = /* @__PURE__ */ new Set([
|
|
1995
|
+
"timestamp",
|
|
1996
|
+
"traceId",
|
|
1997
|
+
"spanId",
|
|
1998
|
+
"experimentId",
|
|
1999
|
+
"scoreTraceId",
|
|
2000
|
+
"entityType",
|
|
2001
|
+
"entityId",
|
|
2002
|
+
"entityName",
|
|
2003
|
+
"entityVersionId",
|
|
2004
|
+
"parentEntityVersionId",
|
|
2005
|
+
"parentEntityType",
|
|
2006
|
+
"parentEntityId",
|
|
2007
|
+
"parentEntityName",
|
|
2008
|
+
"rootEntityVersionId",
|
|
2009
|
+
"rootEntityType",
|
|
2010
|
+
"rootEntityId",
|
|
2011
|
+
"rootEntityName",
|
|
2012
|
+
"userId",
|
|
2013
|
+
"organizationId",
|
|
2014
|
+
"resourceId",
|
|
2015
|
+
"runId",
|
|
2016
|
+
"sessionId",
|
|
2017
|
+
"threadId",
|
|
2018
|
+
"requestId",
|
|
2019
|
+
"environment",
|
|
2020
|
+
"executionSource",
|
|
2021
|
+
"serviceName",
|
|
2022
|
+
"scorerId",
|
|
2023
|
+
"scorerVersion",
|
|
2024
|
+
"scoreSource",
|
|
2025
|
+
"score",
|
|
2026
|
+
"reason"
|
|
2027
|
+
]);
|
|
2028
|
+
function getAggregationSql(aggregation, measure = "score") {
|
|
2029
|
+
switch (aggregation) {
|
|
2030
|
+
case "sum": return `SUM(${measure})`;
|
|
2031
|
+
case "avg": return `AVG(${measure})`;
|
|
2032
|
+
case "min": return `MIN(${measure})`;
|
|
2033
|
+
case "max": return `MAX(${measure})`;
|
|
2034
|
+
case "count": return `CAST(COUNT(${measure}) AS DOUBLE)`;
|
|
2035
|
+
case "last": return `arg_max(${measure}, timestamp)`;
|
|
2036
|
+
default: return `SUM(${measure})`;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
function getIntervalSql(interval) {
|
|
2040
|
+
switch (interval) {
|
|
2041
|
+
case "1m": return "1 minute";
|
|
2042
|
+
case "5m": return "5 minutes";
|
|
2043
|
+
case "15m": return "15 minutes";
|
|
2044
|
+
case "1h": return "1 hour";
|
|
2045
|
+
case "1d": return "1 day";
|
|
2046
|
+
default: return "1 hour";
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
function getValidatedPercentiles(percentiles) {
|
|
2050
|
+
if (!Array.isArray(percentiles) || percentiles.length === 0) throw new Error("Percentiles must include at least one value between 0 and 1.");
|
|
2051
|
+
return percentiles.map((percentile) => {
|
|
2052
|
+
if (!Number.isFinite(percentile) || percentile < 0 || percentile > 1) throw new Error("Percentiles must be finite numbers between 0 and 1.");
|
|
2053
|
+
return percentile;
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
function buildScoreWhereClause(args) {
|
|
2057
|
+
const conditions = ["scorerId = ?"];
|
|
2058
|
+
const params = [args.scorerId];
|
|
2059
|
+
if (args.scoreSource !== void 0) {
|
|
2060
|
+
conditions.push("scoreSource = ?");
|
|
2061
|
+
params.push(args.scoreSource);
|
|
2062
|
+
}
|
|
2063
|
+
const { clause: filterClause, params: filterParams } = buildWhereClause(args.filters, { source: "scoreSource" });
|
|
2064
|
+
if (filterClause) {
|
|
2065
|
+
conditions.push(filterClause.replace("WHERE ", ""));
|
|
2066
|
+
params.push(...filterParams);
|
|
2067
|
+
}
|
|
2068
|
+
return {
|
|
2069
|
+
clause: `WHERE ${conditions.join(" AND ")}`,
|
|
2070
|
+
params
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
function resolveScoreGroupBy(groupBy) {
|
|
2074
|
+
return groupBy.map((key, index) => {
|
|
2075
|
+
const column = parseFieldKey(key);
|
|
2076
|
+
if (!SCORE_GROUP_BY_COLUMNS.has(column)) throw new Error(`Invalid groupBy column(s): ${key}`);
|
|
2077
|
+
const alias = `group_by_${index}`;
|
|
2078
|
+
return {
|
|
2079
|
+
key,
|
|
2080
|
+
selectSql: `${column} AS ${alias}`,
|
|
2081
|
+
groupSql: alias
|
|
2082
|
+
};
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
function toSeriesName(values) {
|
|
2086
|
+
return values.map((value) => value === null || value === void 0 ? "" : String(value)).join("|");
|
|
2087
|
+
}
|
|
2088
|
+
function rowToScoreRecord(row) {
|
|
2089
|
+
return {
|
|
2090
|
+
scoreId: row.scoreId,
|
|
2091
|
+
timestamp: toDate(row.timestamp),
|
|
2092
|
+
traceId: row.traceId ?? null,
|
|
2093
|
+
spanId: row.spanId ?? null,
|
|
2094
|
+
experimentId: row.experimentId ?? null,
|
|
2095
|
+
scoreTraceId: row.scoreTraceId ?? null,
|
|
2096
|
+
entityType: row.entityType ?? null,
|
|
2097
|
+
entityId: row.entityId ?? null,
|
|
2098
|
+
entityName: row.entityName ?? null,
|
|
2099
|
+
entityVersionId: row.entityVersionId ?? null,
|
|
2100
|
+
parentEntityVersionId: row.parentEntityVersionId ?? null,
|
|
2101
|
+
parentEntityType: row.parentEntityType ?? null,
|
|
2102
|
+
parentEntityId: row.parentEntityId ?? null,
|
|
2103
|
+
parentEntityName: row.parentEntityName ?? null,
|
|
2104
|
+
rootEntityVersionId: row.rootEntityVersionId ?? null,
|
|
2105
|
+
rootEntityType: row.rootEntityType ?? null,
|
|
2106
|
+
rootEntityId: row.rootEntityId ?? null,
|
|
2107
|
+
rootEntityName: row.rootEntityName ?? null,
|
|
2108
|
+
userId: row.userId ?? null,
|
|
2109
|
+
organizationId: row.organizationId ?? null,
|
|
2110
|
+
resourceId: row.resourceId ?? null,
|
|
2111
|
+
runId: row.runId ?? null,
|
|
2112
|
+
sessionId: row.sessionId ?? null,
|
|
2113
|
+
threadId: row.threadId ?? null,
|
|
2114
|
+
requestId: row.requestId ?? null,
|
|
2115
|
+
environment: row.environment ?? null,
|
|
2116
|
+
executionSource: row.executionSource ?? null,
|
|
2117
|
+
serviceName: row.serviceName ?? null,
|
|
2118
|
+
scorerId: row.scorerId,
|
|
2119
|
+
scorerVersion: row.scorerVersion ?? null,
|
|
2120
|
+
source: row.scoreSource ?? null,
|
|
2121
|
+
scoreSource: row.scoreSource ?? null,
|
|
2122
|
+
score: Number(row.score),
|
|
2123
|
+
reason: row.reason ?? null,
|
|
2124
|
+
tags: parseJsonArray(row.tags),
|
|
2125
|
+
metadata: parseJson(row.metadata),
|
|
2126
|
+
scope: parseJson(row.scope)
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
function getComparisonDateRange(comparePeriod, timestamp) {
|
|
2130
|
+
if (!timestamp.start || !timestamp.end) return null;
|
|
2131
|
+
const duration = timestamp.end.getTime() - timestamp.start.getTime();
|
|
2132
|
+
switch (comparePeriod) {
|
|
2133
|
+
case "previous_period": return {
|
|
2134
|
+
start: new Date(timestamp.start.getTime() - duration),
|
|
2135
|
+
end: new Date(timestamp.end.getTime() - duration),
|
|
2136
|
+
startExclusive: timestamp.startExclusive,
|
|
2137
|
+
endExclusive: timestamp.endExclusive
|
|
2138
|
+
};
|
|
2139
|
+
case "previous_day": return {
|
|
2140
|
+
start: /* @__PURE__ */ new Date(timestamp.start.getTime() - 864e5),
|
|
2141
|
+
end: /* @__PURE__ */ new Date(timestamp.end.getTime() - 864e5),
|
|
2142
|
+
startExclusive: timestamp.startExclusive,
|
|
2143
|
+
endExclusive: timestamp.endExclusive
|
|
2144
|
+
};
|
|
2145
|
+
case "previous_week": return {
|
|
2146
|
+
start: /* @__PURE__ */ new Date(timestamp.start.getTime() - 6048e5),
|
|
2147
|
+
end: /* @__PURE__ */ new Date(timestamp.end.getTime() - 6048e5),
|
|
2148
|
+
startExclusive: timestamp.startExclusive,
|
|
2149
|
+
endExclusive: timestamp.endExclusive
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
/** Insert a single score event. */
|
|
2154
|
+
async function createScore(db, args) {
|
|
2155
|
+
const s = args.score;
|
|
2156
|
+
const scoreSource = s.scoreSource ?? s.source ?? null;
|
|
2157
|
+
await db.execute(`INSERT INTO score_events (
|
|
2158
|
+
scoreId, timestamp, cursorId, traceId, spanId, experimentId, scoreTraceId,
|
|
2159
|
+
entityType, entityId, entityName, entityVersionId, parentEntityVersionId, parentEntityType, parentEntityId, parentEntityName, rootEntityVersionId, rootEntityType, rootEntityId, rootEntityName,
|
|
2160
|
+
userId, organizationId, resourceId, runId, sessionId, threadId, requestId, environment, executionSource, serviceName,
|
|
2161
|
+
scorerId, scorerVersion, scoreSource, score, reason, tags, metadata, scope
|
|
2162
|
+
)
|
|
2163
|
+
VALUES (${[
|
|
2164
|
+
v(s.scoreId),
|
|
2165
|
+
v(s.timestamp),
|
|
2166
|
+
"nextval('score_events_cursor_id_seq')",
|
|
2167
|
+
v(s.traceId),
|
|
2168
|
+
v(s.spanId ?? null),
|
|
2169
|
+
v(s.experimentId ?? null),
|
|
2170
|
+
v(s.scoreTraceId ?? null),
|
|
2171
|
+
v(s.entityType ?? null),
|
|
2172
|
+
v(s.entityId ?? null),
|
|
2173
|
+
v(s.entityName ?? null),
|
|
2174
|
+
v(s.entityVersionId ?? null),
|
|
2175
|
+
v(s.parentEntityVersionId ?? null),
|
|
2176
|
+
v(s.parentEntityType ?? null),
|
|
2177
|
+
v(s.parentEntityId ?? null),
|
|
2178
|
+
v(s.parentEntityName ?? null),
|
|
2179
|
+
v(s.rootEntityVersionId ?? null),
|
|
2180
|
+
v(s.rootEntityType ?? null),
|
|
2181
|
+
v(s.rootEntityId ?? null),
|
|
2182
|
+
v(s.rootEntityName ?? null),
|
|
2183
|
+
v(s.userId ?? null),
|
|
2184
|
+
v(s.organizationId ?? null),
|
|
2185
|
+
v(s.resourceId ?? null),
|
|
2186
|
+
v(s.runId ?? null),
|
|
2187
|
+
v(s.sessionId ?? null),
|
|
2188
|
+
v(s.threadId ?? null),
|
|
2189
|
+
v(s.requestId ?? null),
|
|
2190
|
+
v(s.environment ?? null),
|
|
2191
|
+
v(s.executionSource ?? null),
|
|
2192
|
+
v(s.serviceName ?? null),
|
|
2193
|
+
v(s.scorerId),
|
|
2194
|
+
v(s.scorerVersion ?? null),
|
|
2195
|
+
v(scoreSource),
|
|
2196
|
+
v(s.score),
|
|
2197
|
+
v(s.reason ?? null),
|
|
2198
|
+
jsonV(s.tags ?? null),
|
|
2199
|
+
jsonV(s.metadata),
|
|
2200
|
+
jsonV(s.scope ?? null)
|
|
2201
|
+
].join(", ")})
|
|
2202
|
+
ON CONFLICT DO NOTHING`);
|
|
2203
|
+
}
|
|
2204
|
+
/** Insert multiple score events in a single statement. */
|
|
2205
|
+
async function batchCreateScores(db, args) {
|
|
2206
|
+
if (args.scores.length === 0) return;
|
|
2207
|
+
const tuples = args.scores.map((s) => {
|
|
2208
|
+
const legacyScore = s;
|
|
2209
|
+
const scoreSource = legacyScore.scoreSource ?? legacyScore.source ?? null;
|
|
2210
|
+
return `(${[
|
|
2211
|
+
v(legacyScore.scoreId),
|
|
2212
|
+
v(legacyScore.timestamp),
|
|
2213
|
+
"nextval('score_events_cursor_id_seq')",
|
|
2214
|
+
v(legacyScore.traceId),
|
|
2215
|
+
v(legacyScore.spanId ?? null),
|
|
2216
|
+
v(legacyScore.experimentId ?? null),
|
|
2217
|
+
v(legacyScore.scoreTraceId ?? null),
|
|
2218
|
+
v(legacyScore.entityType ?? null),
|
|
2219
|
+
v(legacyScore.entityId ?? null),
|
|
2220
|
+
v(legacyScore.entityName ?? null),
|
|
2221
|
+
v(legacyScore.entityVersionId ?? null),
|
|
2222
|
+
v(legacyScore.parentEntityVersionId ?? null),
|
|
2223
|
+
v(legacyScore.parentEntityType ?? null),
|
|
2224
|
+
v(legacyScore.parentEntityId ?? null),
|
|
2225
|
+
v(legacyScore.parentEntityName ?? null),
|
|
2226
|
+
v(legacyScore.rootEntityVersionId ?? null),
|
|
2227
|
+
v(legacyScore.rootEntityType ?? null),
|
|
2228
|
+
v(legacyScore.rootEntityId ?? null),
|
|
2229
|
+
v(legacyScore.rootEntityName ?? null),
|
|
2230
|
+
v(legacyScore.userId ?? null),
|
|
2231
|
+
v(legacyScore.organizationId ?? null),
|
|
2232
|
+
v(legacyScore.resourceId ?? null),
|
|
2233
|
+
v(legacyScore.runId ?? null),
|
|
2234
|
+
v(legacyScore.sessionId ?? null),
|
|
2235
|
+
v(legacyScore.threadId ?? null),
|
|
2236
|
+
v(legacyScore.requestId ?? null),
|
|
2237
|
+
v(legacyScore.environment ?? null),
|
|
2238
|
+
v(legacyScore.executionSource ?? null),
|
|
2239
|
+
v(legacyScore.serviceName ?? null),
|
|
2240
|
+
v(legacyScore.scorerId),
|
|
2241
|
+
v(legacyScore.scorerVersion ?? null),
|
|
2242
|
+
v(scoreSource),
|
|
2243
|
+
v(legacyScore.score),
|
|
2244
|
+
v(legacyScore.reason ?? null),
|
|
2245
|
+
jsonV(legacyScore.tags ?? null),
|
|
2246
|
+
jsonV(legacyScore.metadata),
|
|
2247
|
+
jsonV(legacyScore.scope ?? null)
|
|
2248
|
+
].join(", ")})`;
|
|
2249
|
+
});
|
|
2250
|
+
await db.execute(`INSERT INTO score_events (
|
|
2251
|
+
scoreId, timestamp, cursorId, traceId, spanId, experimentId, scoreTraceId,
|
|
2252
|
+
entityType, entityId, entityName, entityVersionId, parentEntityVersionId, parentEntityType, parentEntityId, parentEntityName, rootEntityVersionId, rootEntityType, rootEntityId, rootEntityName,
|
|
2253
|
+
userId, organizationId, resourceId, runId, sessionId, threadId, requestId, environment, executionSource, serviceName,
|
|
2254
|
+
scorerId, scorerVersion, scoreSource, score, reason, tags, metadata, scope
|
|
2255
|
+
)
|
|
2256
|
+
VALUES ${tuples.join(",\n ")}
|
|
2257
|
+
ON CONFLICT DO NOTHING`);
|
|
2258
|
+
}
|
|
2259
|
+
/** Query score events with filtering, ordering, and pagination. */
|
|
2260
|
+
async function listScores(db, args) {
|
|
2261
|
+
const { mode, filters, pagination, orderBy, after, limit } = listScoresArgsSchema.parse(args);
|
|
2262
|
+
const page = Number(pagination.page);
|
|
2263
|
+
const perPage = Number(pagination.perPage);
|
|
2264
|
+
const { clause: filterClause, params: filterParams } = buildWhereClause(filters, { source: "scoreSource" });
|
|
2265
|
+
if (mode === "delta") {
|
|
2266
|
+
assertDeltaPollingEnabled();
|
|
2267
|
+
const streamHeadCursor = await getStreamHeadCursor(db);
|
|
2268
|
+
if (after === void 0) return {
|
|
2269
|
+
scores: [],
|
|
2270
|
+
delta: {
|
|
2271
|
+
limit,
|
|
2272
|
+
hasMore: false
|
|
2273
|
+
},
|
|
2274
|
+
deltaCursor: streamHeadCursor
|
|
2275
|
+
};
|
|
2276
|
+
const afterCursorId = validateCursorId(after);
|
|
2277
|
+
const deltaWhereClause = extendWhereClause(filterClause, ["cursorId IS NOT NULL", `cursorId > CAST(? AS BIGINT)`]);
|
|
2278
|
+
const rows = await db.query(`SELECT * FROM score_events ${deltaWhereClause} ORDER BY cursorId ASC LIMIT ?`, [
|
|
2279
|
+
...filterParams,
|
|
2280
|
+
afterCursorId,
|
|
2281
|
+
limit + 1
|
|
2282
|
+
]);
|
|
2283
|
+
const visibleRows = rows.slice(0, limit).map((row) => ({
|
|
2284
|
+
cursorId: row.cursorId,
|
|
2285
|
+
score: rowToScoreRecord(row)
|
|
2286
|
+
}));
|
|
2287
|
+
return {
|
|
2288
|
+
scores: visibleRows.map((row) => row.score),
|
|
2289
|
+
delta: {
|
|
2290
|
+
limit,
|
|
2291
|
+
hasMore: rows.length > limit
|
|
2292
|
+
},
|
|
2293
|
+
deltaCursor: visibleRows.length > 0 ? encodeDeltaCursor(visibleRows[visibleRows.length - 1]?.cursorId) : streamHeadCursor
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
const orderByClause = buildOrderByClause(orderBy);
|
|
2297
|
+
const { clause: paginationClause, params: paginationParams } = buildPaginationClause({
|
|
2298
|
+
page,
|
|
2299
|
+
perPage
|
|
2300
|
+
});
|
|
2301
|
+
const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getDeltaCursor(db, filterClause, filterParams) : void 0;
|
|
2302
|
+
const countResult = await db.query(`SELECT COUNT(*) as total FROM score_events ${filterClause}`, filterParams);
|
|
2303
|
+
const total = Number(countResult[0]?.total ?? 0);
|
|
2304
|
+
const rows = await db.query(`SELECT * FROM score_events ${filterClause} ${orderByClause} ${paginationClause}`, [...filterParams, ...paginationParams]);
|
|
2305
|
+
return {
|
|
2306
|
+
pagination: {
|
|
2307
|
+
total,
|
|
2308
|
+
page,
|
|
2309
|
+
perPage,
|
|
2310
|
+
hasMore: (page + 1) * perPage < total
|
|
2311
|
+
},
|
|
2312
|
+
scores: rows.map((row) => rowToScoreRecord(row)),
|
|
2313
|
+
...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
async function getDeltaCursor(db, filterClause, filterParams) {
|
|
2317
|
+
const cursorId = (await db.query(`SELECT max(cursorId) AS cursorId FROM score_events ${filterClause}`, filterParams))[0]?.cursorId;
|
|
2318
|
+
if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
|
|
2319
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM score_events`))[0]?.cursorId);
|
|
2320
|
+
}
|
|
2321
|
+
async function getStreamHeadCursor(db) {
|
|
2322
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM score_events`))[0]?.cursorId);
|
|
2323
|
+
}
|
|
2324
|
+
async function getScoreById(db, scoreId) {
|
|
2325
|
+
const rows = await db.query(`SELECT * FROM score_events WHERE scoreId = ? LIMIT 1`, [scoreId]);
|
|
2326
|
+
return rows[0] ? rowToScoreRecord(rows[0]) : null;
|
|
2327
|
+
}
|
|
2328
|
+
async function getScoreAggregate(db, args) {
|
|
2329
|
+
const aggSql = getAggregationSql(args.aggregation);
|
|
2330
|
+
const { clause, params } = buildScoreWhereClause(args);
|
|
2331
|
+
const rows = await db.query(`SELECT ${aggSql} AS value FROM score_events ${clause}`, params);
|
|
2332
|
+
const value = rows[0]?.value === null || rows[0]?.value === void 0 ? null : Number(rows[0]?.value);
|
|
2333
|
+
if (args.comparePeriod && args.filters?.timestamp) {
|
|
2334
|
+
const previousTimestamp = getComparisonDateRange(args.comparePeriod, args.filters.timestamp);
|
|
2335
|
+
if (previousTimestamp) {
|
|
2336
|
+
const prevRows = await db.query(`SELECT ${aggSql} AS value FROM score_events ${buildScoreWhereClause({
|
|
2337
|
+
...args,
|
|
2338
|
+
filters: {
|
|
2339
|
+
...args.filters ?? {},
|
|
2340
|
+
timestamp: previousTimestamp
|
|
2341
|
+
}
|
|
2342
|
+
}).clause}`, buildScoreWhereClause({
|
|
2343
|
+
...args,
|
|
2344
|
+
filters: {
|
|
2345
|
+
...args.filters ?? {},
|
|
2346
|
+
timestamp: previousTimestamp
|
|
2347
|
+
}
|
|
2348
|
+
}).params);
|
|
2349
|
+
const previousValue = prevRows[0]?.value === null || prevRows[0]?.value === void 0 ? null : Number(prevRows[0]?.value);
|
|
2350
|
+
let changePercent = null;
|
|
2351
|
+
if (previousValue !== null && previousValue !== 0 && value !== null) changePercent = (value - previousValue) / Math.abs(previousValue) * 100;
|
|
2352
|
+
return {
|
|
2353
|
+
value,
|
|
2354
|
+
previousValue,
|
|
2355
|
+
changePercent
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
return { value };
|
|
2360
|
+
}
|
|
2361
|
+
async function getScoreBreakdown(db, args) {
|
|
2362
|
+
const aggSql = getAggregationSql(args.aggregation);
|
|
2363
|
+
const { clause, params } = buildScoreWhereClause(args);
|
|
2364
|
+
const resolvedGroupBy = resolveScoreGroupBy(args.groupBy);
|
|
2365
|
+
const sql = `SELECT ${resolvedGroupBy.map((entry) => entry.selectSql).join(", ")}, ${aggSql} AS value FROM score_events ${clause} GROUP BY ${resolvedGroupBy.map((entry) => entry.groupSql).join(", ")} ORDER BY value DESC`;
|
|
2366
|
+
return { groups: (await db.query(sql, params)).map((row) => ({
|
|
2367
|
+
dimensions: Object.fromEntries(resolvedGroupBy.map((entry, index) => {
|
|
2368
|
+
const value = row[`group_by_${index}`];
|
|
2369
|
+
return [entry.key, value === null || value === void 0 ? null : String(value)];
|
|
2370
|
+
})),
|
|
2371
|
+
value: Number(row.value ?? 0)
|
|
2372
|
+
})) };
|
|
2373
|
+
}
|
|
2374
|
+
async function getScoreTimeSeries(db, args) {
|
|
2375
|
+
const aggSql = getAggregationSql(args.aggregation);
|
|
2376
|
+
const intervalSql = getIntervalSql(args.interval);
|
|
2377
|
+
const { clause, params } = buildScoreWhereClause(args);
|
|
2378
|
+
if (args.groupBy && args.groupBy.length > 0) {
|
|
2379
|
+
const resolvedGroupBy = resolveScoreGroupBy(args.groupBy);
|
|
2380
|
+
const sql = `
|
|
2381
|
+
SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
|
|
2382
|
+
${resolvedGroupBy.map((entry) => entry.selectSql).join(", ")},
|
|
2383
|
+
${aggSql} AS value
|
|
2384
|
+
FROM score_events ${clause}
|
|
2385
|
+
GROUP BY bucket, ${resolvedGroupBy.map((entry) => entry.groupSql).join(", ")}
|
|
2386
|
+
ORDER BY bucket
|
|
2387
|
+
`;
|
|
2388
|
+
const rows = await db.query(sql, params);
|
|
2389
|
+
const seriesMap = /* @__PURE__ */ new Map();
|
|
2390
|
+
for (const row of rows) {
|
|
2391
|
+
const groupValues = resolvedGroupBy.map((_, index) => row[`group_by_${index}`]);
|
|
2392
|
+
const key = JSON.stringify(groupValues);
|
|
2393
|
+
if (!seriesMap.has(key)) seriesMap.set(key, {
|
|
2394
|
+
name: toSeriesName(groupValues),
|
|
2395
|
+
points: []
|
|
2396
|
+
});
|
|
2397
|
+
seriesMap.get(key).points.push({
|
|
2398
|
+
timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
|
|
2399
|
+
value: Number(row.value ?? 0)
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
return { series: Array.from(seriesMap.values()) };
|
|
2403
|
+
}
|
|
2404
|
+
const rows = await db.query(`
|
|
2405
|
+
SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
|
|
2406
|
+
${aggSql} AS value
|
|
2407
|
+
FROM score_events ${clause}
|
|
2408
|
+
GROUP BY bucket
|
|
2409
|
+
ORDER BY bucket
|
|
2410
|
+
`, params);
|
|
2411
|
+
return { series: [{
|
|
2412
|
+
name: args.scoreSource ? `${args.scorerId}|${args.scoreSource}` : args.scorerId,
|
|
2413
|
+
points: rows.map((row) => ({
|
|
2414
|
+
timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
|
|
2415
|
+
value: Number(row.value ?? 0)
|
|
2416
|
+
}))
|
|
2417
|
+
}] };
|
|
2418
|
+
}
|
|
2419
|
+
async function getScorePercentiles(db, args) {
|
|
2420
|
+
const intervalSql = getIntervalSql(args.interval);
|
|
2421
|
+
const { clause, params } = buildScoreWhereClause(args);
|
|
2422
|
+
const percentiles = getValidatedPercentiles(args.percentiles);
|
|
2423
|
+
const series = [];
|
|
2424
|
+
for (const percentile of percentiles) {
|
|
2425
|
+
const rows = await db.query(`
|
|
2426
|
+
SELECT time_bucket(INTERVAL '${intervalSql}', timestamp) AS bucket,
|
|
2427
|
+
percentile_cont(${percentile}) WITHIN GROUP (ORDER BY score) AS pvalue
|
|
2428
|
+
FROM score_events ${clause}
|
|
2429
|
+
GROUP BY bucket
|
|
2430
|
+
ORDER BY bucket
|
|
2431
|
+
`, params);
|
|
2432
|
+
series.push({
|
|
2433
|
+
percentile,
|
|
2434
|
+
points: rows.map((row) => ({
|
|
2435
|
+
timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
|
|
2436
|
+
value: Number(row.pvalue ?? 0)
|
|
2437
|
+
}))
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
return { series };
|
|
2441
|
+
}
|
|
2442
|
+
//#endregion
|
|
2443
|
+
//#region src/storage/domains/observability/tracing.ts
|
|
2444
|
+
const COLUMNS_SQL = [
|
|
2445
|
+
"eventType",
|
|
2446
|
+
"timestamp",
|
|
2447
|
+
"cursorId",
|
|
2448
|
+
"traceId",
|
|
2449
|
+
"spanId",
|
|
2450
|
+
"parentSpanId",
|
|
2451
|
+
"name",
|
|
2452
|
+
"spanType",
|
|
2453
|
+
"isEvent",
|
|
2454
|
+
"endedAt",
|
|
2455
|
+
"experimentId",
|
|
2456
|
+
"entityType",
|
|
2457
|
+
"entityId",
|
|
2458
|
+
"entityName",
|
|
2459
|
+
"entityVersionId",
|
|
2460
|
+
"userId",
|
|
2461
|
+
"organizationId",
|
|
2462
|
+
"resourceId",
|
|
2463
|
+
"runId",
|
|
2464
|
+
"sessionId",
|
|
2465
|
+
"threadId",
|
|
2466
|
+
"requestId",
|
|
2467
|
+
"environment",
|
|
2468
|
+
"source",
|
|
2469
|
+
"serviceName",
|
|
2470
|
+
"attributes",
|
|
2471
|
+
"metadata",
|
|
2472
|
+
"tags",
|
|
2473
|
+
"scope",
|
|
2474
|
+
"links",
|
|
2475
|
+
"input",
|
|
2476
|
+
"output",
|
|
2477
|
+
"error",
|
|
2478
|
+
"requestContext"
|
|
2479
|
+
].join(", ");
|
|
2480
|
+
/**
|
|
2481
|
+
* Reconstruction query uses `arg_max(field, timestamp) FILTER (WHERE field IS NOT NULL)`
|
|
2482
|
+
* so that the final end event supplies the terminal span fields without wiping
|
|
2483
|
+
* stable values emitted on the start event.
|
|
2484
|
+
*/
|
|
2485
|
+
function argMaxNonNull(col) {
|
|
2486
|
+
return `arg_max(${col}, timestamp) FILTER (WHERE ${col} IS NOT NULL) as ${col}`;
|
|
2487
|
+
}
|
|
2488
|
+
const SPAN_RECONSTRUCT_SELECT = `
|
|
2489
|
+
SELECT
|
|
2490
|
+
traceId, spanId,
|
|
2491
|
+
${argMaxNonNull("name")},
|
|
2492
|
+
${argMaxNonNull("spanType")},
|
|
2493
|
+
${argMaxNonNull("parentSpanId")},
|
|
2494
|
+
${argMaxNonNull("isEvent")},
|
|
2495
|
+
coalesce(min(timestamp) FILTER (WHERE eventType = 'start'), min(timestamp)) as startedAt,
|
|
2496
|
+
${argMaxNonNull("endedAt")},
|
|
2497
|
+
${argMaxNonNull("experimentId")},
|
|
2498
|
+
${argMaxNonNull("entityType")},
|
|
2499
|
+
${argMaxNonNull("entityId")},
|
|
2500
|
+
${argMaxNonNull("entityName")},
|
|
2501
|
+
${argMaxNonNull("entityVersionId")},
|
|
2502
|
+
${argMaxNonNull("userId")},
|
|
2503
|
+
${argMaxNonNull("organizationId")},
|
|
2504
|
+
${argMaxNonNull("resourceId")},
|
|
2505
|
+
${argMaxNonNull("runId")},
|
|
2506
|
+
${argMaxNonNull("sessionId")},
|
|
2507
|
+
${argMaxNonNull("threadId")},
|
|
2508
|
+
${argMaxNonNull("requestId")},
|
|
2509
|
+
${argMaxNonNull("environment")},
|
|
2510
|
+
${argMaxNonNull("source")},
|
|
2511
|
+
${argMaxNonNull("serviceName")},
|
|
2512
|
+
${argMaxNonNull("attributes")},
|
|
2513
|
+
${argMaxNonNull("metadata")},
|
|
2514
|
+
${argMaxNonNull("tags")},
|
|
2515
|
+
${argMaxNonNull("scope")},
|
|
2516
|
+
${argMaxNonNull("links")},
|
|
2517
|
+
${argMaxNonNull("input")},
|
|
2518
|
+
${argMaxNonNull("output")},
|
|
2519
|
+
${argMaxNonNull("error")},
|
|
2520
|
+
${argMaxNonNull("requestContext")}
|
|
2521
|
+
FROM span_events
|
|
2522
|
+
`;
|
|
2523
|
+
/** Lightweight variant — only timeline-relevant columns. */
|
|
2524
|
+
const SPAN_RECONSTRUCT_SELECT_LIGHT = `
|
|
2525
|
+
SELECT
|
|
2526
|
+
traceId, spanId,
|
|
2527
|
+
${argMaxNonNull("name")},
|
|
2528
|
+
${argMaxNonNull("spanType")},
|
|
2529
|
+
${argMaxNonNull("parentSpanId")},
|
|
2530
|
+
${argMaxNonNull("isEvent")},
|
|
2531
|
+
coalesce(min(timestamp) FILTER (WHERE eventType = 'start'), min(timestamp)) as startedAt,
|
|
2532
|
+
${argMaxNonNull("endedAt")},
|
|
2533
|
+
${argMaxNonNull("entityType")},
|
|
2534
|
+
${argMaxNonNull("entityId")},
|
|
2535
|
+
${argMaxNonNull("entityName")},
|
|
2536
|
+
${argMaxNonNull("error")}
|
|
2537
|
+
FROM span_events
|
|
2538
|
+
`;
|
|
2539
|
+
/**
|
|
2540
|
+
* Which reconstructed columns each post-aggregation filter key can reference.
|
|
2541
|
+
* `status` is derived from endedAt + error (see buildWhereClause).
|
|
2542
|
+
*/
|
|
2543
|
+
const POSTAGG_FILTER_COLUMNS = {
|
|
2544
|
+
status: ["endedAt", "error"],
|
|
2545
|
+
endedAt: ["endedAt"],
|
|
2546
|
+
tags: ["tags"],
|
|
2547
|
+
metadata: ["metadata"],
|
|
2548
|
+
scope: ["scope"]
|
|
2549
|
+
};
|
|
2550
|
+
/**
|
|
2551
|
+
* Narrow reconstruction used by the slow list paths to evaluate
|
|
2552
|
+
* post-aggregation filters and ordering before pagination. Includes only the
|
|
2553
|
+
* columns the active filters and order field actually reference, so the
|
|
2554
|
+
* full-set aggregate never decompresses the heavy JSON payload columns
|
|
2555
|
+
* (attributes, links, input, output, requestContext) and skips even the
|
|
2556
|
+
* cheaper JSON columns (tags/metadata/scope) unless a filter needs them.
|
|
2557
|
+
*/
|
|
2558
|
+
function buildPostAggReconstructSelect(postAgg, orderByField) {
|
|
2559
|
+
const columns = /* @__PURE__ */ new Set();
|
|
2560
|
+
if (orderByField === "endedAt") columns.add("endedAt");
|
|
2561
|
+
for (const key of Object.keys(postAgg)) for (const column of POSTAGG_FILTER_COLUMNS[key] ?? []) columns.add(column);
|
|
2562
|
+
return `
|
|
2563
|
+
SELECT
|
|
2564
|
+
traceId, spanId,
|
|
2565
|
+
${[...columns].map((col) => `${argMaxNonNull(col)},`).join("\n ")}
|
|
2566
|
+
coalesce(min(timestamp) FILTER (WHERE eventType = 'start'), min(timestamp)) as startedAt
|
|
2567
|
+
FROM span_events
|
|
2568
|
+
`;
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Reconstruct spans for the `(traceId, spanId)` pairs selected by `anchorCte`,
|
|
2572
|
+
* scanning only events at/after the CTE's earliest `anchorStartedAt`.
|
|
2573
|
+
*
|
|
2574
|
+
* The `(traceId, spanId) IN (subquery)` semi-join alone cannot be pushed into
|
|
2575
|
+
* the table scan, so without a bound DuckDB decompresses every column of the
|
|
2576
|
+
* entire table just to emit one page of spans. The time bound is a plain range
|
|
2577
|
+
* predicate that zone maps can prune on (insertion order tracks event time).
|
|
2578
|
+
*
|
|
2579
|
+
* Correctness: span events are only 'start' (timestamp = startedAt) and 'end'
|
|
2580
|
+
* (timestamp = endedAt >= startedAt), so every event of an anchored span has
|
|
2581
|
+
* timestamp >= its start-row timestamp >= min(anchorStartedAt). An empty
|
|
2582
|
+
* anchor set makes the bound NULL, which matches the empty IN-list result.
|
|
2583
|
+
*/
|
|
2584
|
+
function reconstructForAnchors(reconstructSelect, anchorCte) {
|
|
2585
|
+
return `
|
|
2586
|
+
${reconstructSelect}
|
|
2587
|
+
WHERE timestamp >= (SELECT min(anchorStartedAt) FROM ${anchorCte})
|
|
2588
|
+
AND (traceId, spanId) IN (SELECT traceId, spanId FROM ${anchorCte})
|
|
2589
|
+
GROUP BY traceId, spanId`;
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* Same time-bound trick as {@link reconstructForAnchors}, but with the bound
|
|
2593
|
+
* precomputed in JS and passed as a `?` parameter. The delta-poll query shape
|
|
2594
|
+
* references its candidate CTE multiple times, which makes DuckDB materialize
|
|
2595
|
+
* it and lose the dynamic-filter pushdown a scalar subquery bound relies on —
|
|
2596
|
+
* a literal parameter always reaches the scan as a plain range filter.
|
|
2597
|
+
*/
|
|
2598
|
+
function reconstructForAnchorsWithBoundParam(reconstructSelect, anchorCte) {
|
|
2599
|
+
return `
|
|
2600
|
+
${reconstructSelect}
|
|
2601
|
+
WHERE timestamp >= ?
|
|
2602
|
+
AND (traceId, spanId) IN (SELECT traceId, spanId FROM ${anchorCte})
|
|
2603
|
+
GROUP BY traceId, spanId`;
|
|
2604
|
+
}
|
|
2605
|
+
function rowToLightSpanRecord(row) {
|
|
2606
|
+
return {
|
|
2607
|
+
traceId: row.traceId,
|
|
2608
|
+
spanId: row.spanId,
|
|
2609
|
+
name: row.name,
|
|
2610
|
+
spanType: row.spanType,
|
|
2611
|
+
parentSpanId: row.parentSpanId ?? null,
|
|
2612
|
+
isEvent: row.isEvent,
|
|
2613
|
+
startedAt: toDate(row.startedAt),
|
|
2614
|
+
endedAt: toDateOrNull(row.endedAt),
|
|
2615
|
+
entityType: row.entityType ?? null,
|
|
2616
|
+
entityId: row.entityId ?? null,
|
|
2617
|
+
entityName: row.entityName ?? null,
|
|
2618
|
+
error: parseJson(row.error),
|
|
2619
|
+
createdAt: toDate(row.startedAt),
|
|
2620
|
+
updatedAt: toDateOrNull(row.endedAt)
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
function rowToSpanRecord(row) {
|
|
2624
|
+
return {
|
|
2625
|
+
traceId: row.traceId,
|
|
2626
|
+
spanId: row.spanId,
|
|
2627
|
+
name: row.name,
|
|
2628
|
+
spanType: row.spanType,
|
|
2629
|
+
parentSpanId: row.parentSpanId ?? null,
|
|
2630
|
+
isEvent: row.isEvent,
|
|
2631
|
+
startedAt: toDate(row.startedAt),
|
|
2632
|
+
endedAt: toDateOrNull(row.endedAt),
|
|
2633
|
+
experimentId: row.experimentId ?? null,
|
|
2634
|
+
entityType: row.entityType ?? null,
|
|
2635
|
+
entityId: row.entityId ?? null,
|
|
2636
|
+
entityName: row.entityName ?? null,
|
|
2637
|
+
entityVersionId: row.entityVersionId ?? null,
|
|
2638
|
+
userId: row.userId ?? null,
|
|
2639
|
+
organizationId: row.organizationId ?? null,
|
|
2640
|
+
resourceId: row.resourceId ?? null,
|
|
2641
|
+
runId: row.runId ?? null,
|
|
2642
|
+
sessionId: row.sessionId ?? null,
|
|
2643
|
+
threadId: row.threadId ?? null,
|
|
2644
|
+
requestId: row.requestId ?? null,
|
|
2645
|
+
environment: row.environment ?? null,
|
|
2646
|
+
source: row.source ?? null,
|
|
2647
|
+
serviceName: row.serviceName ?? null,
|
|
2648
|
+
attributes: parseJson(row.attributes),
|
|
2649
|
+
metadata: parseJson(row.metadata),
|
|
2650
|
+
tags: parseJsonArray(row.tags),
|
|
2651
|
+
scope: parseJson(row.scope),
|
|
2652
|
+
links: parseJsonArray(row.links),
|
|
2653
|
+
input: parseJson(row.input),
|
|
2654
|
+
output: parseJson(row.output),
|
|
2655
|
+
error: parseJson(row.error),
|
|
2656
|
+
requestContext: parseJson(row.requestContext),
|
|
2657
|
+
createdAt: toDate(row.startedAt),
|
|
2658
|
+
updatedAt: null
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
function buildHasChildErrorClause(hasChildError, rootAlias) {
|
|
2662
|
+
if (hasChildError === void 0) return "";
|
|
2663
|
+
const base = `SELECT 1 FROM span_events c WHERE c.traceId = ${rootAlias}.traceId AND c.spanId != ${rootAlias}.spanId AND c.error IS NOT NULL`;
|
|
2664
|
+
return hasChildError ? `EXISTS (${base})` : `NOT EXISTS (${base})`;
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2667
|
+
* Filter keys that can be evaluated directly against raw `span_events` start
|
|
2668
|
+
* rows. These are stable scalar columns whose value on the start row matches
|
|
2669
|
+
* the reconstructed span value, so pushing them down before reconstruction is
|
|
2670
|
+
* observation-equivalent to reconstructing first and filtering after.
|
|
2671
|
+
*/
|
|
2672
|
+
const PREFILTER_KEYS = /* @__PURE__ */ new Set([
|
|
2673
|
+
"traceId",
|
|
2674
|
+
"spanId",
|
|
2675
|
+
"parentSpanId",
|
|
2676
|
+
"name",
|
|
2677
|
+
"spanType",
|
|
2678
|
+
"source",
|
|
2679
|
+
"entityType",
|
|
2680
|
+
"entityId",
|
|
2681
|
+
"entityName",
|
|
2682
|
+
"entityVersionId",
|
|
2683
|
+
"experimentId",
|
|
2684
|
+
"userId",
|
|
2685
|
+
"organizationId",
|
|
2686
|
+
"resourceId",
|
|
2687
|
+
"runId",
|
|
2688
|
+
"sessionId",
|
|
2689
|
+
"threadId",
|
|
2690
|
+
"requestId",
|
|
2691
|
+
"environment",
|
|
2692
|
+
"serviceName"
|
|
2693
|
+
]);
|
|
2694
|
+
/**
|
|
2695
|
+
* Order-by fields whose start-row value matches the reconstructed root-span
|
|
2696
|
+
* value, so ordering inside the prefilter (before GROUP BY) yields the same
|
|
2697
|
+
* sequence as ordering on reconstructed rows. Anything outside this set must
|
|
2698
|
+
* fall back to the slow path so pagination stays correct.
|
|
2699
|
+
*
|
|
2700
|
+
* `endedAt` is intentionally excluded — start rows always have NULL `endedAt`,
|
|
2701
|
+
* so ordering by it on raw rows would compare NULLs and produce wrong pages.
|
|
2702
|
+
*/
|
|
2703
|
+
const SAFE_PREFILTER_ORDER_FIELDS = /* @__PURE__ */ new Set(["startedAt"]);
|
|
2704
|
+
/**
|
|
2705
|
+
* Intersect the existing prefilter timestamp range with an incoming bound.
|
|
2706
|
+
* Each bound is an exact constraint on the start-row `timestamp`, so the
|
|
2707
|
+
* intersection is the **tighter** of the two on each side: the later `start`
|
|
2708
|
+
* wins, the earlier `end` wins. When two bounds tie on a side, the result is
|
|
2709
|
+
* exclusive if either input was exclusive (the union of exclusivity).
|
|
2710
|
+
*
|
|
2711
|
+
* Required for cases like `{ startedAt: { end: B }, endedAt: { end: C } }`:
|
|
2712
|
+
* both bound the start-row timestamp from above and we want `min(B, C)`,
|
|
2713
|
+
* regardless of insertion order.
|
|
2714
|
+
*/
|
|
2715
|
+
function intersectTimestampRange(existing, incoming) {
|
|
2716
|
+
if (!existing) return { ...incoming };
|
|
2717
|
+
const merged = { ...existing };
|
|
2718
|
+
if (incoming.start !== void 0) {
|
|
2719
|
+
if (merged.start === void 0 || incoming.start.getTime() > merged.start.getTime()) {
|
|
2720
|
+
merged.start = incoming.start;
|
|
2721
|
+
merged.startExclusive = incoming.startExclusive;
|
|
2722
|
+
} else if (incoming.start.getTime() === merged.start.getTime()) merged.startExclusive = (merged.startExclusive ?? false) || (incoming.startExclusive ?? false);
|
|
2723
|
+
}
|
|
2724
|
+
if (incoming.end !== void 0) {
|
|
2725
|
+
if (merged.end === void 0 || incoming.end.getTime() < merged.end.getTime()) {
|
|
2726
|
+
merged.end = incoming.end;
|
|
2727
|
+
merged.endExclusive = incoming.endExclusive;
|
|
2728
|
+
} else if (incoming.end.getTime() === merged.end.getTime()) merged.endExclusive = (merged.endExclusive ?? false) || (incoming.endExclusive ?? false);
|
|
2729
|
+
}
|
|
2730
|
+
return merged;
|
|
2731
|
+
}
|
|
2732
|
+
/**
|
|
2733
|
+
* Split a span-anchor filter set into a `prefilter` half (pushed to raw
|
|
2734
|
+
* `span_events` start rows) and a `postAgg` half (applied after the
|
|
2735
|
+
* reconstruction GROUP BY). Used by both `listTraces` and `listBranches`.
|
|
2736
|
+
*
|
|
2737
|
+
* `hasChildError` is split out separately since it doesn't run via
|
|
2738
|
+
* `buildWhereClause` — `listTraces` wires it up via EXISTS, `listBranches`
|
|
2739
|
+
* never sees it (not in `branchesFilterSchema`).
|
|
2740
|
+
*/
|
|
2741
|
+
function partitionAnchorFilters(filters) {
|
|
2742
|
+
const prefilter = {};
|
|
2743
|
+
const postAgg = {};
|
|
2744
|
+
let hasChildError;
|
|
2745
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
2746
|
+
if (value === void 0 || value === null) continue;
|
|
2747
|
+
if (key === "hasChildError") {
|
|
2748
|
+
if (typeof value === "boolean") hasChildError = value;
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
2751
|
+
if (key === "startedAt") {
|
|
2752
|
+
prefilter.timestamp = intersectTimestampRange(prefilter.timestamp, value);
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2755
|
+
if (key === "endedAt") {
|
|
2756
|
+
postAgg.endedAt = value;
|
|
2757
|
+
const dateRange = value;
|
|
2758
|
+
if (dateRange?.end) prefilter.timestamp = intersectTimestampRange(prefilter.timestamp, {
|
|
2759
|
+
end: dateRange.end,
|
|
2760
|
+
endExclusive: dateRange.endExclusive
|
|
2761
|
+
});
|
|
2762
|
+
continue;
|
|
2763
|
+
}
|
|
2764
|
+
if (PREFILTER_KEYS.has(key)) {
|
|
2765
|
+
prefilter[key] = value;
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
2768
|
+
postAgg[key] = value;
|
|
2769
|
+
}
|
|
2770
|
+
return {
|
|
2771
|
+
prefilter,
|
|
2772
|
+
postAgg,
|
|
2773
|
+
hasChildError
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
function toValuesTuple(row) {
|
|
2777
|
+
return [
|
|
2778
|
+
v(row.eventType),
|
|
2779
|
+
v(row.timestamp),
|
|
2780
|
+
"nextval('span_events_cursor_id_seq')",
|
|
2781
|
+
v(row.traceId),
|
|
2782
|
+
v(row.spanId),
|
|
2783
|
+
v(row.parentSpanId),
|
|
2784
|
+
v(row.name),
|
|
2785
|
+
v(row.spanType),
|
|
2786
|
+
v(row.isEvent),
|
|
2787
|
+
v(row.endedAt),
|
|
2788
|
+
v(row.experimentId),
|
|
2789
|
+
v(row.entityType),
|
|
2790
|
+
v(row.entityId),
|
|
2791
|
+
v(row.entityName),
|
|
2792
|
+
v(row.entityVersionId),
|
|
2793
|
+
v(row.userId),
|
|
2794
|
+
v(row.organizationId),
|
|
2795
|
+
v(row.resourceId),
|
|
2796
|
+
v(row.runId),
|
|
2797
|
+
v(row.sessionId),
|
|
2798
|
+
v(row.threadId),
|
|
2799
|
+
v(row.requestId),
|
|
2800
|
+
v(row.environment),
|
|
2801
|
+
v(row.source),
|
|
2802
|
+
v(row.serviceName),
|
|
2803
|
+
jsonV(row.attributes),
|
|
2804
|
+
jsonV(row.metadata),
|
|
2805
|
+
jsonV(row.tags),
|
|
2806
|
+
jsonV(row.scope),
|
|
2807
|
+
jsonV(row.links),
|
|
2808
|
+
jsonV(row.input),
|
|
2809
|
+
jsonV(row.output),
|
|
2810
|
+
jsonV(row.error),
|
|
2811
|
+
jsonV(row.requestContext)
|
|
2812
|
+
].join(", ");
|
|
2813
|
+
}
|
|
2814
|
+
async function insertSpanEvents(db, rows) {
|
|
2815
|
+
if (rows.length === 0) return;
|
|
2816
|
+
const tuples = rows.map((row) => `(${toValuesTuple(row)})`).join(",\n");
|
|
2817
|
+
await db.execute(`INSERT INTO span_events (${COLUMNS_SQL}) VALUES ${tuples}`);
|
|
2818
|
+
}
|
|
2819
|
+
function createStartSpanRow(s) {
|
|
2820
|
+
return {
|
|
2821
|
+
eventType: "start",
|
|
2822
|
+
timestamp: s.startedAt,
|
|
2823
|
+
traceId: s.traceId,
|
|
2824
|
+
spanId: s.spanId,
|
|
2825
|
+
parentSpanId: s.parentSpanId ?? null,
|
|
2826
|
+
name: s.name,
|
|
2827
|
+
spanType: s.spanType,
|
|
2828
|
+
isEvent: s.isEvent,
|
|
2829
|
+
endedAt: null,
|
|
2830
|
+
experimentId: s.experimentId ?? null,
|
|
2831
|
+
entityType: s.entityType ?? null,
|
|
2832
|
+
entityId: s.entityId ?? null,
|
|
2833
|
+
entityName: s.entityName ?? null,
|
|
2834
|
+
entityVersionId: s.entityVersionId ?? null,
|
|
2835
|
+
userId: s.userId ?? null,
|
|
2836
|
+
organizationId: s.organizationId ?? null,
|
|
2837
|
+
resourceId: s.resourceId ?? null,
|
|
2838
|
+
runId: s.runId ?? null,
|
|
2839
|
+
sessionId: s.sessionId ?? null,
|
|
2840
|
+
threadId: s.threadId ?? null,
|
|
2841
|
+
requestId: s.requestId ?? null,
|
|
2842
|
+
environment: s.environment ?? null,
|
|
2843
|
+
source: s.source ?? null,
|
|
2844
|
+
serviceName: s.serviceName ?? null,
|
|
2845
|
+
attributes: s.attributes ?? null,
|
|
2846
|
+
metadata: s.metadata ?? null,
|
|
2847
|
+
tags: s.tags ?? null,
|
|
2848
|
+
scope: s.scope ?? null,
|
|
2849
|
+
links: null,
|
|
2850
|
+
input: s.input ?? null,
|
|
2851
|
+
output: null,
|
|
2852
|
+
error: null,
|
|
2853
|
+
requestContext: s.requestContext ?? null
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
function createEndSpanRow(s) {
|
|
2857
|
+
return {
|
|
2858
|
+
eventType: "end",
|
|
2859
|
+
timestamp: s.endedAt,
|
|
2860
|
+
traceId: s.traceId,
|
|
2861
|
+
spanId: s.spanId,
|
|
2862
|
+
parentSpanId: s.parentSpanId ?? null,
|
|
2863
|
+
name: s.name,
|
|
2864
|
+
spanType: s.spanType,
|
|
2865
|
+
isEvent: s.isEvent,
|
|
2866
|
+
endedAt: s.endedAt ?? null,
|
|
2867
|
+
experimentId: s.experimentId ?? null,
|
|
2868
|
+
entityType: s.entityType ?? null,
|
|
2869
|
+
entityId: s.entityId ?? null,
|
|
2870
|
+
entityName: s.entityName ?? null,
|
|
2871
|
+
entityVersionId: s.entityVersionId ?? null,
|
|
2872
|
+
userId: s.userId ?? null,
|
|
2873
|
+
organizationId: s.organizationId ?? null,
|
|
2874
|
+
resourceId: s.resourceId ?? null,
|
|
2875
|
+
runId: s.runId ?? null,
|
|
2876
|
+
sessionId: s.sessionId ?? null,
|
|
2877
|
+
threadId: s.threadId ?? null,
|
|
2878
|
+
requestId: s.requestId ?? null,
|
|
2879
|
+
environment: s.environment ?? null,
|
|
2880
|
+
source: s.source ?? null,
|
|
2881
|
+
serviceName: s.serviceName ?? null,
|
|
2882
|
+
attributes: s.attributes ?? null,
|
|
2883
|
+
metadata: s.metadata ?? null,
|
|
2884
|
+
tags: s.tags ?? null,
|
|
2885
|
+
scope: s.scope ?? null,
|
|
2886
|
+
links: s.links ?? null,
|
|
2887
|
+
input: s.input ?? null,
|
|
2888
|
+
output: s.output ?? null,
|
|
2889
|
+
error: s.error ?? null,
|
|
2890
|
+
requestContext: s.requestContext ?? null
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
/** Insert a 'start' event for a new span. */
|
|
2894
|
+
async function createSpan(db, args) {
|
|
2895
|
+
const rows = [createStartSpanRow(args.span)];
|
|
2896
|
+
if (args.span.endedAt) rows.push(createEndSpanRow(args.span));
|
|
2897
|
+
await insertSpanEvents(db, rows);
|
|
2898
|
+
}
|
|
2899
|
+
/** Insert 'start' events for multiple spans in a single statement. */
|
|
2900
|
+
async function batchCreateSpans(db, args) {
|
|
2901
|
+
if (args.records.length === 0) return;
|
|
2902
|
+
await insertSpanEvents(db, args.records.flatMap((record) => {
|
|
2903
|
+
const events = [createStartSpanRow(record)];
|
|
2904
|
+
if (record.endedAt) events.push(createEndSpanRow(record));
|
|
2905
|
+
return events;
|
|
2906
|
+
}));
|
|
2907
|
+
}
|
|
2908
|
+
/** Delete all span events for the given trace IDs. */
|
|
2909
|
+
async function batchDeleteTraces(db, args) {
|
|
2910
|
+
if (args.traceIds.length === 0) return;
|
|
2911
|
+
const placeholders = args.traceIds.map(() => "?").join(", ");
|
|
2912
|
+
await db.execute(`DELETE FROM span_events WHERE traceId IN (${placeholders})`, args.traceIds);
|
|
2913
|
+
}
|
|
2914
|
+
/** Reconstruct a single span from its event history. */
|
|
2915
|
+
async function getSpan(db, args) {
|
|
2916
|
+
const rows = await db.query(`${SPAN_RECONSTRUCT_SELECT} WHERE traceId = ? AND spanId = ? GROUP BY traceId, spanId`, [args.traceId, args.spanId]);
|
|
2917
|
+
if (rows.length === 0) return null;
|
|
2918
|
+
return { span: rowToSpanRecord(rows[0]) };
|
|
2919
|
+
}
|
|
2920
|
+
/** Reconstruct the root span (no parent) for a trace. */
|
|
2921
|
+
async function getRootSpan(db, args) {
|
|
2922
|
+
const rows = await db.query(`${SPAN_RECONSTRUCT_SELECT} WHERE traceId = ? GROUP BY traceId, spanId HAVING arg_max(parentSpanId, timestamp) IS NULL LIMIT 1`, [args.traceId]);
|
|
2923
|
+
if (rows.length === 0) return null;
|
|
2924
|
+
return { span: rowToSpanRecord(rows[0]) };
|
|
2925
|
+
}
|
|
2926
|
+
/** Reconstruct all spans belonging to a trace. */
|
|
2927
|
+
async function getTrace(db, args) {
|
|
2928
|
+
const rows = await db.query(`${SPAN_RECONSTRUCT_SELECT} WHERE traceId = ? GROUP BY traceId, spanId`, [args.traceId]);
|
|
2929
|
+
if (rows.length === 0) return null;
|
|
2930
|
+
return {
|
|
2931
|
+
traceId: args.traceId,
|
|
2932
|
+
spans: rows.map((row) => rowToSpanRecord(row))
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
/** Reconstruct lightweight spans belonging to a trace (timeline fields only). */
|
|
2936
|
+
async function getTraceLight(db, args) {
|
|
2937
|
+
const rows = await db.query(`${SPAN_RECONSTRUCT_SELECT_LIGHT} WHERE traceId = ? GROUP BY traceId, spanId`, [args.traceId]);
|
|
2938
|
+
if (rows.length === 0) return null;
|
|
2939
|
+
return {
|
|
2940
|
+
traceId: args.traceId,
|
|
2941
|
+
spans: rows.map((row) => rowToLightSpanRecord(row))
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* List root spans (traces) with filtering, ordering, and pagination.
|
|
2946
|
+
*
|
|
2947
|
+
* Instead of reconstructing every span in the table and then filtering, we:
|
|
2948
|
+
* 1. Pick candidate root `(traceId, spanId)` tuples from raw `span_events`
|
|
2949
|
+
* by looking only at rows where `eventType = 'start'` and
|
|
2950
|
+
* `parentSpanId IS NULL`. All scalar filters (entity*, *Id, service,
|
|
2951
|
+
* environment, startedAt range, ...) run here, against raw columns.
|
|
2952
|
+
* 2. Fully reconstruct spans only for that narrowed set, then apply
|
|
2953
|
+
* post-aggregation filters (status/tags/metadata/scope/endedAt/
|
|
2954
|
+
* hasChildError).
|
|
2955
|
+
*
|
|
2956
|
+
* When there are no post-aggregation filters, ordering + pagination happen
|
|
2957
|
+
* inside the prefilter CTE so reconstruction runs on at most `perPage` rows.
|
|
2958
|
+
*/
|
|
2959
|
+
async function listTraceRows(db, args, reconstructSelect, mapRow, toSpans) {
|
|
2960
|
+
const filters = args.filters ?? {};
|
|
2961
|
+
const page = Number(args.pagination?.page ?? 0);
|
|
2962
|
+
const perPage = Number(args.pagination?.perPage ?? 10);
|
|
2963
|
+
const orderBy = {
|
|
2964
|
+
field: args.orderBy?.field ?? "startedAt",
|
|
2965
|
+
direction: args.orderBy?.direction ?? "DESC"
|
|
2966
|
+
};
|
|
2967
|
+
const { prefilter, postAgg, hasChildError } = partitionAnchorFilters(filters);
|
|
2968
|
+
const { clause: prefilterClause, params: prefilterParams } = buildWhereClause(prefilter);
|
|
2969
|
+
const prefilterParts = [`eventType = 'start'`, `parentSpanId IS NULL`];
|
|
2970
|
+
if (prefilterClause) prefilterParts.push(prefilterClause.replace(/^WHERE\s+/i, ""));
|
|
2971
|
+
const prefilterWhere = `WHERE ${prefilterParts.join(" AND ")}`;
|
|
2972
|
+
const outerAlias = "outer_root";
|
|
2973
|
+
const orderDir = orderBy.direction.toUpperCase();
|
|
2974
|
+
if (orderDir !== "ASC" && orderDir !== "DESC") throw new Error(`Invalid sort direction: ${orderBy.direction}`);
|
|
2975
|
+
const canOrderInPrefilter = SAFE_PREFILTER_ORDER_FIELDS.has(orderBy.field);
|
|
2976
|
+
if (!(Object.keys(postAgg).length > 0 || hasChildError !== void 0 || !canOrderInPrefilter)) {
|
|
2977
|
+
const prefilterOrderBy = `ORDER BY timestamp ${orderDir}`;
|
|
2978
|
+
const offset = page * perPage;
|
|
2979
|
+
const countSql = `
|
|
2980
|
+
SELECT COUNT(*) as total
|
|
2981
|
+
FROM span_events AS ${outerAlias}
|
|
2982
|
+
${prefilterWhere}
|
|
2983
|
+
`;
|
|
2984
|
+
const countResult = await db.query(countSql, prefilterParams);
|
|
2985
|
+
const total = Number(countResult[0]?.total ?? 0);
|
|
2986
|
+
const pageSql = `
|
|
2987
|
+
WITH page_roots AS (
|
|
2988
|
+
SELECT traceId, spanId, timestamp AS anchorStartedAt
|
|
2989
|
+
FROM span_events AS ${outerAlias}
|
|
2990
|
+
${prefilterWhere}
|
|
2991
|
+
${prefilterOrderBy}
|
|
2992
|
+
LIMIT ? OFFSET ?
|
|
2993
|
+
)
|
|
2994
|
+
${reconstructForAnchors(reconstructSelect, "page_roots")}
|
|
2995
|
+
${buildOrderByClause(orderBy)}
|
|
2996
|
+
`;
|
|
2997
|
+
const spans = (await db.query(pageSql, [
|
|
2998
|
+
...prefilterParams,
|
|
2999
|
+
perPage,
|
|
3000
|
+
offset
|
|
3001
|
+
])).map((row) => mapRow(row));
|
|
3002
|
+
return {
|
|
3003
|
+
pagination: {
|
|
3004
|
+
total,
|
|
3005
|
+
page,
|
|
3006
|
+
perPage,
|
|
3007
|
+
hasMore: (page + 1) * perPage < total
|
|
3008
|
+
},
|
|
3009
|
+
spans: toSpans(spans)
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
const { clause: postAggClause, params: postAggParams } = buildWhereClause(postAgg);
|
|
3013
|
+
const postAggParts = [];
|
|
3014
|
+
if (postAggClause) postAggParts.push(postAggClause.replace(/^WHERE\s+/i, ""));
|
|
3015
|
+
const childErrorClause = buildHasChildErrorClause(hasChildError, "root_spans");
|
|
3016
|
+
if (childErrorClause) postAggParts.push(childErrorClause);
|
|
3017
|
+
const postAggWhere = postAggParts.length > 0 ? `WHERE ${postAggParts.join(" AND ")}` : "";
|
|
3018
|
+
const cteSql = `
|
|
3019
|
+
WITH candidate_roots AS (
|
|
3020
|
+
SELECT traceId, spanId
|
|
3021
|
+
FROM span_events AS ${outerAlias}
|
|
3022
|
+
${prefilterWhere}
|
|
3023
|
+
),
|
|
3024
|
+
root_spans AS (
|
|
3025
|
+
${buildPostAggReconstructSelect(postAgg, orderBy.field)}
|
|
3026
|
+
WHERE (traceId, spanId) IN (SELECT traceId, spanId FROM candidate_roots)
|
|
3027
|
+
GROUP BY traceId, spanId
|
|
3028
|
+
)
|
|
3029
|
+
`;
|
|
3030
|
+
const orderByClause = buildOrderByClause(orderBy);
|
|
3031
|
+
const { clause: paginationClause, params: paginationParams } = buildPaginationClause({
|
|
3032
|
+
page,
|
|
3033
|
+
perPage
|
|
3034
|
+
});
|
|
3035
|
+
const countSql = `
|
|
3036
|
+
${cteSql}
|
|
3037
|
+
SELECT COUNT(*) as total FROM root_spans ${postAggWhere}
|
|
3038
|
+
`;
|
|
3039
|
+
const countResult = await db.query(countSql, [...prefilterParams, ...postAggParams]);
|
|
3040
|
+
const total = Number(countResult[0]?.total ?? 0);
|
|
3041
|
+
const dataSql = `
|
|
3042
|
+
${cteSql},
|
|
3043
|
+
page_roots AS (
|
|
3044
|
+
SELECT traceId, spanId, startedAt AS anchorStartedAt
|
|
3045
|
+
FROM root_spans ${postAggWhere} ${orderByClause} ${paginationClause}
|
|
3046
|
+
)
|
|
3047
|
+
${reconstructForAnchors(reconstructSelect, "page_roots")}
|
|
3048
|
+
${orderByClause}
|
|
3049
|
+
`;
|
|
3050
|
+
const spans = (await db.query(dataSql, [
|
|
3051
|
+
...prefilterParams,
|
|
3052
|
+
...postAggParams,
|
|
3053
|
+
...paginationParams
|
|
3054
|
+
])).map((row) => mapRow(row));
|
|
3055
|
+
return {
|
|
3056
|
+
pagination: {
|
|
3057
|
+
total,
|
|
3058
|
+
page,
|
|
3059
|
+
perPage,
|
|
3060
|
+
hasMore: (page + 1) * perPage < total
|
|
3061
|
+
},
|
|
3062
|
+
spans: toSpans(spans)
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
async function listTraces(db, args) {
|
|
3066
|
+
const { mode, filters, pagination, orderBy, after, limit } = listTracesArgsSchema.parse(args);
|
|
3067
|
+
const filterRecord = filters ?? {};
|
|
3068
|
+
if (mode === "delta") {
|
|
3069
|
+
assertDeltaPollingEnabled();
|
|
3070
|
+
const streamHeadCursor = await getTraceStreamHeadCursor(db);
|
|
3071
|
+
if (after === void 0) return {
|
|
3072
|
+
spans: [],
|
|
3073
|
+
delta: {
|
|
3074
|
+
limit,
|
|
3075
|
+
hasMore: false
|
|
3076
|
+
},
|
|
3077
|
+
deltaCursor: streamHeadCursor
|
|
3078
|
+
};
|
|
3079
|
+
const afterCursorId = validateCursorId(after);
|
|
3080
|
+
const { prefilter, postAgg, hasChildError } = partitionAnchorFilters(filterRecord);
|
|
3081
|
+
const { clause: prefilterClause, params: prefilterParams } = buildWhereClause(prefilter);
|
|
3082
|
+
const prefilterParts = [
|
|
3083
|
+
`eventType = 'start'`,
|
|
3084
|
+
`parentSpanId IS NULL`,
|
|
3085
|
+
`cursorId IS NOT NULL`,
|
|
3086
|
+
`cursorId > CAST(? AS BIGINT)`
|
|
3087
|
+
];
|
|
3088
|
+
if (prefilterClause) prefilterParts.push(prefilterClause.replace(/^WHERE\s+/i, ""));
|
|
3089
|
+
const prefilterWhere = `WHERE ${prefilterParts.join(" AND ")}`;
|
|
3090
|
+
const { clause: postAggClause, params: postAggParams } = buildWhereClause(postAgg);
|
|
3091
|
+
const postAggParts = [];
|
|
3092
|
+
if (postAggClause) postAggParts.push(postAggClause.replace(/^WHERE\s+/i, ""));
|
|
3093
|
+
const childErrorClause = buildHasChildErrorClause(hasChildError, "root_spans");
|
|
3094
|
+
if (childErrorClause) postAggParts.push(childErrorClause);
|
|
3095
|
+
const postAggWhere = postAggParts.length > 0 ? `WHERE ${postAggParts.join(" AND ")}` : "";
|
|
3096
|
+
const outerAlias = "outer_root";
|
|
3097
|
+
const minTs = (await db.query(`SELECT min(timestamp) as minTs FROM span_events AS ${outerAlias} ${prefilterWhere}`, [afterCursorId, ...prefilterParams]))[0]?.minTs ?? null;
|
|
3098
|
+
if (minTs === null) return {
|
|
3099
|
+
spans: [],
|
|
3100
|
+
delta: {
|
|
3101
|
+
limit,
|
|
3102
|
+
hasMore: false
|
|
3103
|
+
},
|
|
3104
|
+
deltaCursor: streamHeadCursor
|
|
3105
|
+
};
|
|
3106
|
+
const dataSql = `
|
|
3107
|
+
WITH candidate_roots AS (
|
|
3108
|
+
SELECT traceId, spanId, cursorId
|
|
3109
|
+
FROM span_events AS ${outerAlias}
|
|
3110
|
+
${prefilterWhere}
|
|
3111
|
+
),
|
|
3112
|
+
root_spans AS (
|
|
3113
|
+
SELECT reconstructed.*, candidate_roots.cursorId AS anchorCursorId
|
|
3114
|
+
FROM (
|
|
3115
|
+
${reconstructForAnchorsWithBoundParam(SPAN_RECONSTRUCT_SELECT, "candidate_roots")}
|
|
3116
|
+
) AS reconstructed
|
|
3117
|
+
INNER JOIN candidate_roots USING (traceId, spanId)
|
|
3118
|
+
)
|
|
3119
|
+
SELECT * FROM root_spans ${postAggWhere} ORDER BY anchorCursorId ASC LIMIT ?
|
|
3120
|
+
`;
|
|
3121
|
+
const rows = await db.query(dataSql, [
|
|
3122
|
+
afterCursorId,
|
|
3123
|
+
...prefilterParams,
|
|
3124
|
+
minTs,
|
|
3125
|
+
...postAggParams,
|
|
3126
|
+
limit + 1
|
|
3127
|
+
]);
|
|
3128
|
+
const visibleRows = rows.slice(0, limit).map((row) => ({
|
|
3129
|
+
cursorId: row.anchorCursorId,
|
|
3130
|
+
span: rowToSpanRecord(row)
|
|
3131
|
+
}));
|
|
3132
|
+
return {
|
|
3133
|
+
spans: toTraceSpans(visibleRows.map((row) => row.span)),
|
|
3134
|
+
delta: {
|
|
3135
|
+
limit,
|
|
3136
|
+
hasMore: rows.length > limit
|
|
3137
|
+
},
|
|
3138
|
+
deltaCursor: visibleRows.length > 0 ? encodeDeltaCursor(visibleRows[visibleRows.length - 1]?.cursorId) : streamHeadCursor
|
|
3139
|
+
};
|
|
3140
|
+
}
|
|
3141
|
+
const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getTraceDeltaCursor(db, filters) : void 0;
|
|
3142
|
+
const { pagination: resultPagination, spans } = await listTraceRows(db, {
|
|
3143
|
+
filters,
|
|
3144
|
+
pagination,
|
|
3145
|
+
orderBy
|
|
3146
|
+
}, SPAN_RECONSTRUCT_SELECT, rowToSpanRecord, toTraceSpans);
|
|
3147
|
+
return {
|
|
3148
|
+
pagination: resultPagination,
|
|
3149
|
+
spans,
|
|
3150
|
+
...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
async function listTracesLight(db, args) {
|
|
3154
|
+
return listTraceRows(db, args, SPAN_RECONSTRUCT_SELECT_LIGHT, rowToLightSpanRecord, (spans) => spans);
|
|
3155
|
+
}
|
|
3156
|
+
const BRANCH_SPAN_TYPE_PLACEHOLDERS = BRANCH_SPAN_TYPES.map(() => "?").join(", ");
|
|
3157
|
+
/**
|
|
3158
|
+
* Reconstruct multiple spans by spanId within a single trace. Single round-trip
|
|
3159
|
+
* fetch used by the optimized {@link import('@mastra/core/storage').getBranch}
|
|
3160
|
+
* path: getStructure walks the skeleton to identify branch spanIds, then this
|
|
3161
|
+
* pulls full data for only those spans instead of the whole trace.
|
|
3162
|
+
*/
|
|
3163
|
+
async function getSpans(db, args) {
|
|
3164
|
+
if (args.spanIds.length === 0) return {
|
|
3165
|
+
traceId: args.traceId,
|
|
3166
|
+
spans: []
|
|
3167
|
+
};
|
|
3168
|
+
const placeholders = args.spanIds.map(() => "?").join(", ");
|
|
3169
|
+
const rows = await db.query(`${SPAN_RECONSTRUCT_SELECT}
|
|
3170
|
+
WHERE traceId = ? AND spanId IN (${placeholders})
|
|
3171
|
+
GROUP BY traceId, spanId`, [args.traceId, ...args.spanIds]);
|
|
3172
|
+
return {
|
|
3173
|
+
traceId: args.traceId,
|
|
3174
|
+
spans: rows.map((row) => rowToSpanRecord(row))
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
/**
|
|
3178
|
+
* List branch anchor spans (named-entity invocations) across all traces with
|
|
3179
|
+
* filtering, ordering, and pagination.
|
|
3180
|
+
*
|
|
3181
|
+
* Same two-stage strategy as `listTraces`:
|
|
3182
|
+
* 1. Pick candidate anchor `(traceId, spanId)` tuples from raw `span_events`
|
|
3183
|
+
* by looking only at `eventType = 'start'` rows whose `spanType` is in
|
|
3184
|
+
* {@link BRANCH_SPAN_TYPES}. Scalar filters (entity*, *Id, environment,
|
|
3185
|
+
* serviceName, startedAt range, ...) run here, against raw columns. This
|
|
3186
|
+
* avoids paying reconstruction cost for the high-volume sub-operation
|
|
3187
|
+
* events (MODEL_STEP, MODEL_CHUNK, ...) that are never anchors.
|
|
3188
|
+
* 2. Reconstruct full span data only for that narrowed set, then apply
|
|
3189
|
+
* post-aggregation filters (status / metadata / tags / endedAt range).
|
|
3190
|
+
*
|
|
3191
|
+
* When there are no post-aggregation filters, ordering + pagination happen
|
|
3192
|
+
* inside the prefilter so reconstruction runs on at most `perPage` rows.
|
|
3193
|
+
*/
|
|
3194
|
+
async function listBranches(db, args) {
|
|
3195
|
+
const { mode, filters, pagination, orderBy, after, limit } = listBranchesArgsSchema.parse(args);
|
|
3196
|
+
const filterRecord = filters ?? {};
|
|
3197
|
+
const page = Number(pagination.page);
|
|
3198
|
+
const perPage = Number(pagination.perPage);
|
|
3199
|
+
const userSpanType = filterRecord.spanType;
|
|
3200
|
+
if (typeof userSpanType === "string" && !BRANCH_SPAN_TYPES.includes(userSpanType)) {
|
|
3201
|
+
const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getBranchDeltaCursor(db, filters) : void 0;
|
|
3202
|
+
if (mode === "delta") {
|
|
3203
|
+
assertDeltaPollingEnabled();
|
|
3204
|
+
return {
|
|
3205
|
+
branches: [],
|
|
3206
|
+
delta: {
|
|
3207
|
+
limit,
|
|
3208
|
+
hasMore: false
|
|
3209
|
+
},
|
|
3210
|
+
deltaCursor: currentDeltaCursor
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
return {
|
|
3214
|
+
pagination: {
|
|
3215
|
+
total: 0,
|
|
3216
|
+
page,
|
|
3217
|
+
perPage,
|
|
3218
|
+
hasMore: false
|
|
3219
|
+
},
|
|
3220
|
+
branches: [],
|
|
3221
|
+
...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
if (mode === "delta") {
|
|
3225
|
+
assertDeltaPollingEnabled();
|
|
3226
|
+
const streamHeadCursor = await getBranchStreamHeadCursor(db, typeof userSpanType === "string" ? userSpanType : null);
|
|
3227
|
+
if (after === void 0) return {
|
|
3228
|
+
branches: [],
|
|
3229
|
+
delta: {
|
|
3230
|
+
limit,
|
|
3231
|
+
hasMore: false
|
|
3232
|
+
},
|
|
3233
|
+
deltaCursor: streamHeadCursor
|
|
3234
|
+
};
|
|
3235
|
+
const afterCursorId = validateCursorId(after);
|
|
3236
|
+
const { spanType: _spanType, ...rest } = filterRecord;
|
|
3237
|
+
const { prefilter, postAgg, hasChildError: _hasChildError } = partitionAnchorFilters(rest);
|
|
3238
|
+
const { clause: prefilterClause, params: prefilterFilterParams } = buildWhereClause(prefilter);
|
|
3239
|
+
const prefilterParts = [
|
|
3240
|
+
`eventType = 'start'`,
|
|
3241
|
+
`cursorId IS NOT NULL`,
|
|
3242
|
+
`cursorId > CAST(? AS BIGINT)`
|
|
3243
|
+
];
|
|
3244
|
+
let spanTypeParams;
|
|
3245
|
+
if (typeof userSpanType === "string") {
|
|
3246
|
+
prefilterParts.push(`spanType = ?`);
|
|
3247
|
+
spanTypeParams = [userSpanType];
|
|
3248
|
+
} else {
|
|
3249
|
+
prefilterParts.push(`spanType IN (${BRANCH_SPAN_TYPE_PLACEHOLDERS})`);
|
|
3250
|
+
spanTypeParams = [...BRANCH_SPAN_TYPES];
|
|
3251
|
+
}
|
|
3252
|
+
if (prefilterClause) prefilterParts.push(prefilterClause.replace(/^WHERE\s+/i, ""));
|
|
3253
|
+
const prefilterWhere = `WHERE ${prefilterParts.join(" AND ")}`;
|
|
3254
|
+
const prefilterParams = [
|
|
3255
|
+
afterCursorId,
|
|
3256
|
+
...spanTypeParams,
|
|
3257
|
+
...prefilterFilterParams
|
|
3258
|
+
];
|
|
3259
|
+
const { clause: postAggClause, params: postAggParams } = buildWhereClause(postAgg);
|
|
3260
|
+
const postAggWhere = postAggClause ? postAggClause : "";
|
|
3261
|
+
const outerAlias = "outer_anchor";
|
|
3262
|
+
const minTs = (await db.query(`SELECT min(timestamp) as minTs FROM span_events AS ${outerAlias} ${prefilterWhere}`, prefilterParams))[0]?.minTs ?? null;
|
|
3263
|
+
if (minTs === null) return {
|
|
3264
|
+
branches: [],
|
|
3265
|
+
delta: {
|
|
3266
|
+
limit,
|
|
3267
|
+
hasMore: false
|
|
3268
|
+
},
|
|
3269
|
+
deltaCursor: streamHeadCursor
|
|
3270
|
+
};
|
|
3271
|
+
const dataSql = `
|
|
3272
|
+
WITH candidate_anchors AS (
|
|
3273
|
+
SELECT traceId, spanId, cursorId
|
|
3274
|
+
FROM span_events AS ${outerAlias}
|
|
3275
|
+
${prefilterWhere}
|
|
3276
|
+
),
|
|
3277
|
+
branch_anchors AS (
|
|
3278
|
+
SELECT reconstructed.*, candidate_anchors.cursorId AS anchorCursorId
|
|
3279
|
+
FROM (
|
|
3280
|
+
${reconstructForAnchorsWithBoundParam(SPAN_RECONSTRUCT_SELECT, "candidate_anchors")}
|
|
3281
|
+
) AS reconstructed
|
|
3282
|
+
INNER JOIN candidate_anchors USING (traceId, spanId)
|
|
3283
|
+
)
|
|
3284
|
+
SELECT * FROM branch_anchors ${postAggWhere} ORDER BY anchorCursorId ASC LIMIT ?
|
|
3285
|
+
`;
|
|
3286
|
+
const rows = await db.query(dataSql, [
|
|
3287
|
+
...prefilterParams,
|
|
3288
|
+
minTs,
|
|
3289
|
+
...postAggParams,
|
|
3290
|
+
limit + 1
|
|
3291
|
+
]);
|
|
3292
|
+
const visibleRows = rows.slice(0, limit).map((row) => ({
|
|
3293
|
+
cursorId: row.anchorCursorId,
|
|
3294
|
+
branch: rowToSpanRecord(row)
|
|
3295
|
+
}));
|
|
3296
|
+
return {
|
|
3297
|
+
branches: toTraceSpans(visibleRows.map((row) => row.branch)),
|
|
3298
|
+
delta: {
|
|
3299
|
+
limit,
|
|
3300
|
+
hasMore: rows.length > limit
|
|
3301
|
+
},
|
|
3302
|
+
deltaCursor: visibleRows.length > 0 ? encodeDeltaCursor(visibleRows[visibleRows.length - 1]?.cursorId) : streamHeadCursor
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
const { spanType: _spanType, ...rest } = filterRecord;
|
|
3306
|
+
const { prefilter, postAgg, hasChildError: _hasChildError } = partitionAnchorFilters(rest);
|
|
3307
|
+
const { clause: prefilterClause, params: prefilterFilterParams } = buildWhereClause(prefilter);
|
|
3308
|
+
const prefilterParts = [`eventType = 'start'`];
|
|
3309
|
+
let spanTypeParams;
|
|
3310
|
+
if (typeof userSpanType === "string") {
|
|
3311
|
+
prefilterParts.push(`spanType = ?`);
|
|
3312
|
+
spanTypeParams = [userSpanType];
|
|
3313
|
+
} else {
|
|
3314
|
+
prefilterParts.push(`spanType IN (${BRANCH_SPAN_TYPE_PLACEHOLDERS})`);
|
|
3315
|
+
spanTypeParams = [...BRANCH_SPAN_TYPES];
|
|
3316
|
+
}
|
|
3317
|
+
if (prefilterClause) prefilterParts.push(prefilterClause.replace(/^WHERE\s+/i, ""));
|
|
3318
|
+
const prefilterWhere = `WHERE ${prefilterParts.join(" AND ")}`;
|
|
3319
|
+
const prefilterParams = [...spanTypeParams, ...prefilterFilterParams];
|
|
3320
|
+
const outerAlias = "outer_anchor";
|
|
3321
|
+
const orderDir = orderBy.direction.toUpperCase();
|
|
3322
|
+
if (orderDir !== "ASC" && orderDir !== "DESC") throw new Error(`Invalid sort direction: ${orderBy.direction}`);
|
|
3323
|
+
const currentDeltaCursor = deltaPollingFeatureEnabled() ? await getBranchDeltaCursor(db, filters) : void 0;
|
|
3324
|
+
const canOrderInPrefilter = SAFE_PREFILTER_ORDER_FIELDS.has(orderBy.field);
|
|
3325
|
+
if (!(Object.keys(postAgg).length > 0 || !canOrderInPrefilter)) {
|
|
3326
|
+
const prefilterOrderBy = `ORDER BY timestamp ${orderDir}`;
|
|
3327
|
+
const offset = page * perPage;
|
|
3328
|
+
const countSql = `
|
|
3329
|
+
SELECT COUNT(*) as total
|
|
3330
|
+
FROM span_events AS ${outerAlias}
|
|
3331
|
+
${prefilterWhere}
|
|
3332
|
+
`;
|
|
3333
|
+
const countResult = await db.query(countSql, prefilterParams);
|
|
3334
|
+
const total = Number(countResult[0]?.total ?? 0);
|
|
3335
|
+
if (total === 0) return {
|
|
3336
|
+
pagination: {
|
|
3337
|
+
total: 0,
|
|
3338
|
+
page,
|
|
3339
|
+
perPage,
|
|
3340
|
+
hasMore: false
|
|
3341
|
+
},
|
|
3342
|
+
branches: [],
|
|
3343
|
+
...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
|
|
3344
|
+
};
|
|
3345
|
+
const pageSql = `
|
|
3346
|
+
WITH page_anchors AS (
|
|
3347
|
+
SELECT traceId, spanId, timestamp AS anchorStartedAt
|
|
3348
|
+
FROM span_events AS ${outerAlias}
|
|
3349
|
+
${prefilterWhere}
|
|
3350
|
+
${prefilterOrderBy}
|
|
3351
|
+
LIMIT ? OFFSET ?
|
|
3352
|
+
)
|
|
3353
|
+
${reconstructForAnchors(SPAN_RECONSTRUCT_SELECT, "page_anchors")}
|
|
3354
|
+
${buildOrderByClause(orderBy)}
|
|
3355
|
+
`;
|
|
3356
|
+
const spans = (await db.query(pageSql, [
|
|
3357
|
+
...prefilterParams,
|
|
3358
|
+
perPage,
|
|
3359
|
+
offset
|
|
3360
|
+
])).map((row) => rowToSpanRecord(row));
|
|
3361
|
+
return {
|
|
3362
|
+
pagination: {
|
|
3363
|
+
total,
|
|
3364
|
+
page,
|
|
3365
|
+
perPage,
|
|
3366
|
+
hasMore: (page + 1) * perPage < total
|
|
3367
|
+
},
|
|
3368
|
+
branches: toTraceSpans(spans),
|
|
3369
|
+
...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
const { clause: postAggClause, params: postAggParams } = buildWhereClause(postAgg);
|
|
3373
|
+
const postAggWhere = postAggClause ? postAggClause : "";
|
|
3374
|
+
const orderByClause = buildOrderByClause(orderBy);
|
|
3375
|
+
const { clause: paginationClause, params: paginationParams } = buildPaginationClause({
|
|
3376
|
+
page,
|
|
3377
|
+
perPage
|
|
3378
|
+
});
|
|
3379
|
+
const cteSql = `
|
|
3380
|
+
WITH candidate_anchors AS (
|
|
3381
|
+
SELECT traceId, spanId
|
|
3382
|
+
FROM span_events AS ${outerAlias}
|
|
3383
|
+
${prefilterWhere}
|
|
3384
|
+
),
|
|
3385
|
+
branch_anchors AS (
|
|
3386
|
+
${buildPostAggReconstructSelect(postAgg, orderBy.field)}
|
|
3387
|
+
WHERE (traceId, spanId) IN (SELECT traceId, spanId FROM candidate_anchors)
|
|
3388
|
+
GROUP BY traceId, spanId
|
|
3389
|
+
)
|
|
3390
|
+
`;
|
|
3391
|
+
const countSql = `
|
|
3392
|
+
${cteSql}
|
|
3393
|
+
SELECT COUNT(*) as total FROM branch_anchors ${postAggWhere}
|
|
3394
|
+
`;
|
|
3395
|
+
const countResult = await db.query(countSql, [...prefilterParams, ...postAggParams]);
|
|
3396
|
+
const total = Number(countResult[0]?.total ?? 0);
|
|
3397
|
+
if (total === 0) return {
|
|
3398
|
+
pagination: {
|
|
3399
|
+
total: 0,
|
|
3400
|
+
page,
|
|
3401
|
+
perPage,
|
|
3402
|
+
hasMore: false
|
|
3403
|
+
},
|
|
3404
|
+
branches: [],
|
|
3405
|
+
...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
|
|
3406
|
+
};
|
|
3407
|
+
const dataSql = `
|
|
3408
|
+
${cteSql},
|
|
3409
|
+
page_anchors AS (
|
|
3410
|
+
SELECT traceId, spanId, startedAt AS anchorStartedAt
|
|
3411
|
+
FROM branch_anchors ${postAggWhere} ${orderByClause} ${paginationClause}
|
|
3412
|
+
)
|
|
3413
|
+
${reconstructForAnchors(SPAN_RECONSTRUCT_SELECT, "page_anchors")}
|
|
3414
|
+
${orderByClause}
|
|
3415
|
+
`;
|
|
3416
|
+
const spans = (await db.query(dataSql, [
|
|
3417
|
+
...prefilterParams,
|
|
3418
|
+
...postAggParams,
|
|
3419
|
+
...paginationParams
|
|
3420
|
+
])).map((row) => rowToSpanRecord(row));
|
|
3421
|
+
return {
|
|
3422
|
+
pagination: {
|
|
3423
|
+
total,
|
|
3424
|
+
page,
|
|
3425
|
+
perPage,
|
|
3426
|
+
hasMore: (page + 1) * perPage < total
|
|
3427
|
+
},
|
|
3428
|
+
branches: toTraceSpans(spans),
|
|
3429
|
+
...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
async function getTraceDeltaCursor(db, filters) {
|
|
3433
|
+
const { prefilter, postAgg, hasChildError } = partitionAnchorFilters(filters ?? {});
|
|
3434
|
+
const { clause: prefilterClause, params: prefilterParams } = buildWhereClause(prefilter);
|
|
3435
|
+
const prefilterParts = [
|
|
3436
|
+
`eventType = 'start'`,
|
|
3437
|
+
`parentSpanId IS NULL`,
|
|
3438
|
+
`cursorId IS NOT NULL`
|
|
3439
|
+
];
|
|
3440
|
+
if (prefilterClause) prefilterParts.push(prefilterClause.replace(/^WHERE\s+/i, ""));
|
|
3441
|
+
const prefilterWhere = `WHERE ${prefilterParts.join(" AND ")}`;
|
|
3442
|
+
const outerAlias = "outer_root";
|
|
3443
|
+
const { clause: postAggClause, params: postAggParams } = buildWhereClause(postAgg);
|
|
3444
|
+
const postAggParts = [];
|
|
3445
|
+
if (postAggClause) postAggParts.push(postAggClause.replace(/^WHERE\s+/i, ""));
|
|
3446
|
+
const childErrorClause = buildHasChildErrorClause(hasChildError, "root_spans");
|
|
3447
|
+
if (childErrorClause) postAggParts.push(childErrorClause);
|
|
3448
|
+
const postAggWhere = postAggParts.length > 0 ? `WHERE ${postAggParts.join(" AND ")}` : "";
|
|
3449
|
+
if (postAggWhere === "") {
|
|
3450
|
+
const cursorId = (await db.query(`SELECT max(cursorId) AS cursorId FROM span_events AS ${outerAlias} ${prefilterWhere}`, prefilterParams))[0]?.cursorId;
|
|
3451
|
+
if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
|
|
3452
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND parentSpanId IS NULL AND cursorId IS NOT NULL`))[0]?.cursorId);
|
|
3453
|
+
}
|
|
3454
|
+
const cteSql = `
|
|
3455
|
+
WITH candidate_roots AS (
|
|
3456
|
+
SELECT traceId, spanId, cursorId
|
|
3457
|
+
FROM span_events AS ${outerAlias}
|
|
3458
|
+
${prefilterWhere}
|
|
3459
|
+
),
|
|
3460
|
+
root_spans AS (
|
|
3461
|
+
SELECT reconstructed.*, candidate_roots.cursorId AS anchorCursorId
|
|
3462
|
+
FROM (
|
|
3463
|
+
${SPAN_RECONSTRUCT_SELECT}
|
|
3464
|
+
WHERE (traceId, spanId) IN (SELECT traceId, spanId FROM candidate_roots)
|
|
3465
|
+
GROUP BY traceId, spanId
|
|
3466
|
+
) AS reconstructed
|
|
3467
|
+
INNER JOIN candidate_roots USING (traceId, spanId)
|
|
3468
|
+
)
|
|
3469
|
+
`;
|
|
3470
|
+
const cursorId = (await db.query(`${cteSql} SELECT max(anchorCursorId) AS cursorId FROM root_spans ${postAggWhere}`, [...prefilterParams, ...postAggParams]))[0]?.cursorId;
|
|
3471
|
+
if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
|
|
3472
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND parentSpanId IS NULL AND cursorId IS NOT NULL`))[0]?.cursorId);
|
|
3473
|
+
}
|
|
3474
|
+
async function getTraceStreamHeadCursor(db) {
|
|
3475
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND parentSpanId IS NULL AND cursorId IS NOT NULL`))[0]?.cursorId);
|
|
3476
|
+
}
|
|
3477
|
+
async function getBranchDeltaCursor(db, filters) {
|
|
3478
|
+
const filterRecord = filters ?? {};
|
|
3479
|
+
const userSpanType = filterRecord.spanType;
|
|
3480
|
+
const { spanType: _spanType, ...rest } = filterRecord;
|
|
3481
|
+
const { prefilter, postAgg } = partitionAnchorFilters(rest);
|
|
3482
|
+
const { clause: prefilterClause, params: prefilterFilterParams } = buildWhereClause(prefilter);
|
|
3483
|
+
const prefilterParts = [`eventType = 'start'`, `cursorId IS NOT NULL`];
|
|
3484
|
+
let spanTypeParams;
|
|
3485
|
+
if (typeof userSpanType === "string") {
|
|
3486
|
+
prefilterParts.push(`spanType = ?`);
|
|
3487
|
+
spanTypeParams = [userSpanType];
|
|
3488
|
+
} else {
|
|
3489
|
+
prefilterParts.push(`spanType IN (${BRANCH_SPAN_TYPE_PLACEHOLDERS})`);
|
|
3490
|
+
spanTypeParams = [...BRANCH_SPAN_TYPES];
|
|
3491
|
+
}
|
|
3492
|
+
if (prefilterClause) prefilterParts.push(prefilterClause.replace(/^WHERE\s+/i, ""));
|
|
3493
|
+
const prefilterWhere = `WHERE ${prefilterParts.join(" AND ")}`;
|
|
3494
|
+
const prefilterParams = [...spanTypeParams, ...prefilterFilterParams];
|
|
3495
|
+
const outerAlias = "outer_anchor";
|
|
3496
|
+
const { clause: postAggClause, params: postAggParams } = buildWhereClause(postAgg);
|
|
3497
|
+
if (!postAggClause) {
|
|
3498
|
+
const cursorId = (await db.query(`SELECT max(cursorId) AS cursorId FROM span_events AS ${outerAlias} ${prefilterWhere}`, prefilterParams))[0]?.cursorId;
|
|
3499
|
+
if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
|
|
3500
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND spanType IN (${BRANCH_SPAN_TYPE_PLACEHOLDERS}) AND cursorId IS NOT NULL`, [...BRANCH_SPAN_TYPES]))[0]?.cursorId);
|
|
3501
|
+
}
|
|
3502
|
+
const cteSql = `
|
|
3503
|
+
WITH candidate_anchors AS (
|
|
3504
|
+
SELECT traceId, spanId, cursorId
|
|
3505
|
+
FROM span_events AS ${outerAlias}
|
|
3506
|
+
${prefilterWhere}
|
|
3507
|
+
),
|
|
3508
|
+
branch_anchors AS (
|
|
3509
|
+
SELECT reconstructed.*, candidate_anchors.cursorId AS anchorCursorId
|
|
3510
|
+
FROM (
|
|
3511
|
+
${SPAN_RECONSTRUCT_SELECT}
|
|
3512
|
+
WHERE (traceId, spanId) IN (SELECT traceId, spanId FROM candidate_anchors)
|
|
3513
|
+
GROUP BY traceId, spanId
|
|
3514
|
+
) AS reconstructed
|
|
3515
|
+
INNER JOIN candidate_anchors USING (traceId, spanId)
|
|
3516
|
+
)
|
|
3517
|
+
`;
|
|
3518
|
+
const cursorId = (await db.query(`${cteSql} SELECT max(anchorCursorId) AS cursorId FROM branch_anchors ${postAggClause}`, [...prefilterParams, ...postAggParams]))[0]?.cursorId;
|
|
3519
|
+
if (cursorId !== null && cursorId !== void 0) return encodeDeltaCursor(cursorId);
|
|
3520
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND spanType IN (${BRANCH_SPAN_TYPE_PLACEHOLDERS}) AND cursorId IS NOT NULL`, [...BRANCH_SPAN_TYPES]))[0]?.cursorId);
|
|
3521
|
+
}
|
|
3522
|
+
async function getBranchStreamHeadCursor(db, userSpanType) {
|
|
3523
|
+
if (userSpanType) return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND spanType = ? AND cursorId IS NOT NULL`, [userSpanType]))[0]?.cursorId);
|
|
3524
|
+
return encodeDeltaCursor((await db.query(`SELECT max(cursorId) AS cursorId FROM span_events WHERE eventType = 'start' AND spanType IN (${BRANCH_SPAN_TYPE_PLACEHOLDERS}) AND cursorId IS NOT NULL`, [...BRANCH_SPAN_TYPES]))[0]?.cursorId);
|
|
3525
|
+
}
|
|
3526
|
+
//#endregion
|
|
3527
|
+
//#region src/storage/domains/observability/index.ts
|
|
3528
|
+
function buildSignalMigrationRequiredMessage(args) {
|
|
3529
|
+
return `
|
|
3530
|
+
===========================================================================
|
|
3531
|
+
MIGRATION REQUIRED: DuckDB observability signal tables need signal IDs
|
|
3532
|
+
===========================================================================
|
|
3533
|
+
|
|
3534
|
+
The following signal tables still use the legacy schema and must be migrated
|
|
3535
|
+
before observability storage can initialize:
|
|
3536
|
+
|
|
3537
|
+
${args.tables.map((table) => ` - ${table.table}`).join("\n")}\n\nTo fix this, run the manual migration command:\n\n npx mastra migrate\n\nThis command will:\n 1. Create replacement signal tables with signal-ID primary keys\n 2. Backfill missing signal IDs for legacy rows\n 3. Swap the migrated tables into place\n\nWARNING: This migration recreates the signal tables and may take significant\ntime for large databases. Please ensure you have a backup before proceeding.\n===========================================================================\n`;
|
|
3538
|
+
}
|
|
3539
|
+
/**
|
|
3540
|
+
* DuckDB-backed observability storage for traces, metrics, logs, scores, and feedback.
|
|
3541
|
+
* Uses an append-only event-sourced model with SQL-based reconstruction for spans.
|
|
3542
|
+
*/
|
|
3543
|
+
var ObservabilityStorageDuckDB = class extends ObservabilityStorage {
|
|
3544
|
+
db;
|
|
3545
|
+
constructor(config) {
|
|
3546
|
+
super();
|
|
3547
|
+
this.db = config.db;
|
|
3548
|
+
}
|
|
3549
|
+
/** Create all observability tables if they don't exist. */
|
|
3550
|
+
async init() {
|
|
3551
|
+
const migrationStatus = await checkSignalTablesMigrationStatus(this.db);
|
|
3552
|
+
if (migrationStatus.needsMigration) throw new MastraError({
|
|
3553
|
+
id: createStorageErrorId("DUCKDB", "MIGRATION_REQUIRED", "SIGNAL_TABLES"),
|
|
3554
|
+
domain: ErrorDomain.STORAGE,
|
|
3555
|
+
category: ErrorCategory.USER,
|
|
3556
|
+
text: buildSignalMigrationRequiredMessage({ tables: migrationStatus.tables.map(({ table }) => ({ table })) })
|
|
3557
|
+
});
|
|
3558
|
+
await this.db.executeBatch([...ALL_DDL, ...ALL_MIGRATIONS]);
|
|
3559
|
+
await dropLegacyCursorIdDefaults(this.db);
|
|
3560
|
+
}
|
|
3561
|
+
/**
|
|
3562
|
+
* Manually migrate legacy signal tables to the signal-ID primary-key schema.
|
|
3563
|
+
* The public method name is historical; the CLI still calls `migrateSpans()`
|
|
3564
|
+
* for observability migrations even though this now also migrates signal tables.
|
|
3565
|
+
*/
|
|
3566
|
+
async migrateSpans() {
|
|
3567
|
+
const migrationStatus = await checkSignalTablesMigrationStatus(this.db);
|
|
3568
|
+
if (!migrationStatus.needsMigration) return {
|
|
3569
|
+
success: true,
|
|
3570
|
+
alreadyMigrated: true,
|
|
3571
|
+
duplicatesRemoved: 0,
|
|
3572
|
+
message: "Migration already complete. Signal tables already use signal-ID primary keys."
|
|
3573
|
+
};
|
|
3574
|
+
await migrateSignalTables(this.db, this.logger);
|
|
3575
|
+
return {
|
|
3576
|
+
success: true,
|
|
3577
|
+
alreadyMigrated: false,
|
|
3578
|
+
duplicatesRemoved: 0,
|
|
3579
|
+
message: `Migration complete. Migrated signal tables: ${migrationStatus.tables.map((t) => t.table).join(", ")}.`
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
/** Delete all rows from every observability table. Use with caution. */
|
|
3583
|
+
async dangerouslyClearAll() {
|
|
3584
|
+
for (const table of [
|
|
3585
|
+
"span_events",
|
|
3586
|
+
"metric_events",
|
|
3587
|
+
"log_events",
|
|
3588
|
+
"score_events",
|
|
3589
|
+
"feedback_events"
|
|
3590
|
+
]) await this.db.execute(`TRUNCATE TABLE ${table}`);
|
|
3591
|
+
}
|
|
3592
|
+
get observabilityStrategy() {
|
|
3593
|
+
return {
|
|
3594
|
+
preferred: "event-sourced",
|
|
3595
|
+
supported: ["event-sourced"]
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
getFeatures() {
|
|
3599
|
+
if (!deltaPollingFeatureEnabled()) return;
|
|
3600
|
+
return ["delta-polling"];
|
|
3601
|
+
}
|
|
3602
|
+
async createSpan(args) {
|
|
3603
|
+
return createSpan(this.db, args);
|
|
3604
|
+
}
|
|
3605
|
+
async batchCreateSpans(args) {
|
|
3606
|
+
return batchCreateSpans(this.db, args);
|
|
3607
|
+
}
|
|
3608
|
+
async batchDeleteTraces(args) {
|
|
3609
|
+
return batchDeleteTraces(this.db, args);
|
|
3610
|
+
}
|
|
3611
|
+
async getSpan(args) {
|
|
3612
|
+
return getSpan(this.db, args);
|
|
3613
|
+
}
|
|
3614
|
+
async getSpans(args) {
|
|
3615
|
+
return getSpans(this.db, args);
|
|
3616
|
+
}
|
|
3617
|
+
async getRootSpan(args) {
|
|
3618
|
+
return getRootSpan(this.db, args);
|
|
3619
|
+
}
|
|
3620
|
+
async getTrace(args) {
|
|
3621
|
+
return getTrace(this.db, args);
|
|
3622
|
+
}
|
|
3623
|
+
async getTraceLight(args) {
|
|
3624
|
+
return getTraceLight(this.db, args);
|
|
3625
|
+
}
|
|
3626
|
+
async listTraces(args) {
|
|
3627
|
+
return listTraces(this.db, args);
|
|
3628
|
+
}
|
|
3629
|
+
async listTracesLight(args) {
|
|
3630
|
+
return listTracesLight(this.db, args);
|
|
3631
|
+
}
|
|
3632
|
+
async listBranches(args) {
|
|
3633
|
+
return listBranches(this.db, args);
|
|
3634
|
+
}
|
|
3635
|
+
async batchCreateLogs(args) {
|
|
3636
|
+
return batchCreateLogs(this.db, args);
|
|
3637
|
+
}
|
|
3638
|
+
async listLogs(args) {
|
|
3639
|
+
return listLogs(this.db, args);
|
|
3640
|
+
}
|
|
3641
|
+
async batchCreateMetrics(args) {
|
|
3642
|
+
return batchCreateMetrics(this.db, args);
|
|
3643
|
+
}
|
|
3644
|
+
async listMetrics(args) {
|
|
3645
|
+
return listMetrics(this.db, args);
|
|
3646
|
+
}
|
|
3647
|
+
async getMetricAggregate(args) {
|
|
3648
|
+
return getMetricAggregate(this.db, args);
|
|
3649
|
+
}
|
|
3650
|
+
async getMetricBreakdown(args) {
|
|
3651
|
+
return getMetricBreakdown(this.db, args);
|
|
3652
|
+
}
|
|
3653
|
+
async getMetricTimeSeries(args) {
|
|
3654
|
+
return getMetricTimeSeries(this.db, args);
|
|
3655
|
+
}
|
|
3656
|
+
async getMetricPercentiles(args) {
|
|
3657
|
+
return getMetricPercentiles(this.db, args);
|
|
3658
|
+
}
|
|
3659
|
+
async getMetricNames(args) {
|
|
3660
|
+
return getMetricNames(this.db, args);
|
|
3661
|
+
}
|
|
3662
|
+
async getMetricLabelKeys(args) {
|
|
3663
|
+
return getMetricLabelKeys(this.db, args);
|
|
3664
|
+
}
|
|
3665
|
+
async getMetricLabelValues(args) {
|
|
3666
|
+
return getMetricLabelValues(this.db, args);
|
|
3667
|
+
}
|
|
3668
|
+
async getEntityTypes(args) {
|
|
3669
|
+
return getEntityTypes(this.db, args);
|
|
3670
|
+
}
|
|
3671
|
+
async getEntityNames(args) {
|
|
3672
|
+
return getEntityNames(this.db, args);
|
|
3673
|
+
}
|
|
3674
|
+
async getServiceNames(args) {
|
|
3675
|
+
return getServiceNames(this.db, args);
|
|
3676
|
+
}
|
|
3677
|
+
async getEnvironments(args) {
|
|
3678
|
+
return getEnvironments(this.db, args);
|
|
3679
|
+
}
|
|
3680
|
+
async getTags(args) {
|
|
3681
|
+
return getTags(this.db, args);
|
|
3682
|
+
}
|
|
3683
|
+
async createScore(args) {
|
|
3684
|
+
return createScore(this.db, args);
|
|
3685
|
+
}
|
|
3686
|
+
async batchCreateScores(args) {
|
|
3687
|
+
return batchCreateScores(this.db, args);
|
|
3688
|
+
}
|
|
3689
|
+
async listScores(args) {
|
|
3690
|
+
return listScores(this.db, args);
|
|
3691
|
+
}
|
|
3692
|
+
async getScoreById(scoreId) {
|
|
3693
|
+
return getScoreById(this.db, scoreId);
|
|
3694
|
+
}
|
|
3695
|
+
async getScoreAggregate(args) {
|
|
3696
|
+
return getScoreAggregate(this.db, args);
|
|
3697
|
+
}
|
|
3698
|
+
async getScoreBreakdown(args) {
|
|
3699
|
+
return getScoreBreakdown(this.db, args);
|
|
3700
|
+
}
|
|
3701
|
+
async getScoreTimeSeries(args) {
|
|
3702
|
+
return getScoreTimeSeries(this.db, args);
|
|
3703
|
+
}
|
|
3704
|
+
async getScorePercentiles(args) {
|
|
3705
|
+
return getScorePercentiles(this.db, args);
|
|
3706
|
+
}
|
|
3707
|
+
async createFeedback(args) {
|
|
3708
|
+
return createFeedback(this.db, args);
|
|
3709
|
+
}
|
|
3710
|
+
async batchCreateFeedback(args) {
|
|
3711
|
+
return batchCreateFeedback(this.db, args);
|
|
3712
|
+
}
|
|
3713
|
+
async listFeedback(args) {
|
|
3714
|
+
return listFeedback(this.db, args);
|
|
3715
|
+
}
|
|
3716
|
+
async getFeedbackAggregate(args) {
|
|
3717
|
+
return getFeedbackAggregate(this.db, args);
|
|
3718
|
+
}
|
|
3719
|
+
async getFeedbackBreakdown(args) {
|
|
3720
|
+
return getFeedbackBreakdown(this.db, args);
|
|
3721
|
+
}
|
|
3722
|
+
async getFeedbackTimeSeries(args) {
|
|
3723
|
+
return getFeedbackTimeSeries(this.db, args);
|
|
3724
|
+
}
|
|
3725
|
+
async getFeedbackPercentiles(args) {
|
|
3726
|
+
return getFeedbackPercentiles(this.db, args);
|
|
3727
|
+
}
|
|
3728
|
+
};
|
|
3729
|
+
//#endregion
|
|
3730
|
+
export { ObservabilityStorageDuckDB };
|
|
3731
|
+
|
|
3732
|
+
//# sourceMappingURL=observability-UnP20Kyg.js.map
|