@mastra/clickhouse 1.23.0-alpha.0 → 1.23.0-alpha.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/dist/docs/SKILL.md +1 -1
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/index.cjs +815 -386
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +816 -387
- package/dist/index.js.map +1 -1
- package/dist/storage/db/index.d.ts +8 -0
- package/dist/storage/db/index.d.ts.map +1 -1
- package/dist/storage/db/utils.d.ts +11 -0
- package/dist/storage/db/utils.d.ts.map +1 -1
- package/dist/storage/domains/memory/index.d.ts +2 -0
- package/dist/storage/domains/memory/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/ddl.d.ts +5 -5
- package/dist/storage/domains/observability/v-next/ddl.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/feedback.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/index.d.ts +3 -2
- package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/metrics.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/polling.d.ts +2 -0
- package/dist/storage/domains/observability/v-next/polling.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/scores.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/span-query.d.ts +45 -0
- package/dist/storage/domains/observability/v-next/span-query.d.ts.map +1 -0
- package/dist/storage/domains/observability/v-next/trace-query.d.ts +21 -2
- package/dist/storage/domains/observability/v-next/trace-query.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/trace-roots.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/tracing.d.ts.map +1 -1
- package/dist/storage/domains/scores/index.d.ts.map +1 -1
- package/dist/storage/domains/workflows/index.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createClient } from "@clickhouse/client";
|
|
1
|
+
import { TupleParam, createClient } from "@clickhouse/client";
|
|
2
2
|
import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
|
|
3
3
|
import * as coreStorage from "@mastra/core/storage";
|
|
4
4
|
import { BRANCH_SPAN_TYPES, EntityType, METRIC_DISTINCT_COLUMNS, MastraCompositeStore, MemoryStorage, ObservabilityStorage, SCORERS_SCHEMA, SPAN_SCHEMA, ScoresStorage, TABLE_AGENT_VERSIONS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_HARNESS_SESSIONS, TABLE_KNOWLEDGE_ACTIVITY, TABLE_KNOWLEDGE_CURSORS, TABLE_KNOWLEDGE_MENTIONS, TABLE_KNOWLEDGE_NODES, TABLE_KNOWLEDGE_RECORDS, TABLE_KNOWLEDGE_SEMANTIC_OUTBOX, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_THREAD_STATE, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_TRACES, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, TraceStatus, WorkflowsStorage, buildInputPreview, calculatePagination, computeTraceStatus, createStorageErrorId, getDefaultValue, getSqlType, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, normalizePerPage, safelyParseJSON, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
|
|
@@ -256,6 +256,35 @@ const TABLE_ENGINES = {
|
|
|
256
256
|
[TABLE_KNOWLEDGE_ACTIVITY]: `ReplacingMergeTree()`,
|
|
257
257
|
[TABLE_KNOWLEDGE_SEMANTIC_OUTBOX]: `ReplacingMergeTree()`
|
|
258
258
|
};
|
|
259
|
+
/**
|
|
260
|
+
* Bloom-filter skip indexes for lookups on columns outside the sort key.
|
|
261
|
+
*
|
|
262
|
+
* These tables are keyed `(createdAt, id)` (or `(createdAt, run_id, ...)` for
|
|
263
|
+
* workflow snapshots), so every lookup by id, thread, resource or run would
|
|
264
|
+
* otherwise scan every granule. Added idempotently at init; existing parts are
|
|
265
|
+
* covered as merges and retention rewrite them (or via `MATERIALIZE INDEX`).
|
|
266
|
+
*/
|
|
267
|
+
const TABLE_SKIP_INDEXES = {
|
|
268
|
+
[TABLE_MESSAGES]: [
|
|
269
|
+
"id",
|
|
270
|
+
"thread_id",
|
|
271
|
+
"resourceId"
|
|
272
|
+
],
|
|
273
|
+
[TABLE_THREADS]: ["id", "resourceId"],
|
|
274
|
+
[TABLE_RESOURCES]: ["id"],
|
|
275
|
+
[TABLE_WORKFLOW_SNAPSHOT]: ["run_id"],
|
|
276
|
+
[TABLE_SCORERS]: [
|
|
277
|
+
"id",
|
|
278
|
+
"runId",
|
|
279
|
+
"scorerId",
|
|
280
|
+
"entityId",
|
|
281
|
+
"traceId"
|
|
282
|
+
]
|
|
283
|
+
};
|
|
284
|
+
/** Name of the skip index {@link TABLE_SKIP_INDEXES} declares for `column`. */
|
|
285
|
+
function skipIndexName(column) {
|
|
286
|
+
return `idx_${column}`;
|
|
287
|
+
}
|
|
259
288
|
const COLUMN_TYPES = {
|
|
260
289
|
text: "String",
|
|
261
290
|
timestamp: "DateTime64(3)",
|
|
@@ -368,10 +397,39 @@ var ClickhouseDB = class extends MastraBase {
|
|
|
368
397
|
return filtered;
|
|
369
398
|
}
|
|
370
399
|
async hasColumn(table, column) {
|
|
371
|
-
return (await
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
400
|
+
return (await this.getTableColumns(table)).has(column);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Adds the bloom-filter skip indexes declared in {@link TABLE_SKIP_INDEXES}
|
|
404
|
+
* for `tableName`. Call after createTable/alterTable so optional columns
|
|
405
|
+
* exist. Indexes (or columns) already present are skipped: on Replicated
|
|
406
|
+
* tables every issued ALTER bumps the metadata version even with
|
|
407
|
+
* `IF NOT EXISTS`.
|
|
408
|
+
*/
|
|
409
|
+
async ensureSkipIndexes(tableName) {
|
|
410
|
+
const columns = TABLE_SKIP_INDEXES[tableName];
|
|
411
|
+
if (!columns?.length) return;
|
|
412
|
+
try {
|
|
413
|
+
const existingColumns = await this.getTableColumns(tableName);
|
|
414
|
+
const result = await this.client.query({
|
|
415
|
+
query: `SELECT name FROM system.data_skipping_indices WHERE database = currentDatabase() AND table = {table:String}`,
|
|
416
|
+
query_params: { table: tableName },
|
|
417
|
+
format: "JSONEachRow"
|
|
418
|
+
});
|
|
419
|
+
const existingIndexes = new Set((await result.json()).map((row) => row.name));
|
|
420
|
+
for (const column of columns) {
|
|
421
|
+
const name = skipIndexName(column);
|
|
422
|
+
if (existingIndexes.has(name) || !existingColumns.has(column)) continue;
|
|
423
|
+
await this.client.command({ query: addOnClusterToDDL(`ALTER TABLE ${tableName} ADD INDEX IF NOT EXISTS ${name} "${column}" TYPE bloom_filter(0.01) GRANULARITY 1`, this.replication) });
|
|
424
|
+
}
|
|
425
|
+
} catch (error) {
|
|
426
|
+
throw new MastraError({
|
|
427
|
+
id: createStorageErrorId("CLICKHOUSE", "ENSURE_SKIP_INDEXES", "FAILED"),
|
|
428
|
+
domain: ErrorDomain.STORAGE,
|
|
429
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
430
|
+
details: { tableName }
|
|
431
|
+
}, error);
|
|
432
|
+
}
|
|
375
433
|
}
|
|
376
434
|
/**
|
|
377
435
|
* Checks if a table exists in the database.
|
|
@@ -764,7 +822,8 @@ var ClickhouseDB = class extends MastraBase {
|
|
|
764
822
|
date_time_input_format: "best_effort",
|
|
765
823
|
date_time_output_format: "iso",
|
|
766
824
|
use_client_time_zone: 1,
|
|
767
|
-
output_format_json_quote_64bit_integers: 0
|
|
825
|
+
output_format_json_quote_64bit_integers: 0,
|
|
826
|
+
...tableName === TABLE_WORKFLOW_SNAPSHOT ? { use_skip_indexes_if_final: 1 } : {}
|
|
768
827
|
}
|
|
769
828
|
});
|
|
770
829
|
if (!result) return null;
|
|
@@ -861,6 +920,9 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
861
920
|
schema: TABLE_SCHEMAS[TABLE_MESSAGES],
|
|
862
921
|
ifNotExists: ["resourceId"]
|
|
863
922
|
});
|
|
923
|
+
await this.#db.ensureSkipIndexes(TABLE_THREADS);
|
|
924
|
+
await this.#db.ensureSkipIndexes(TABLE_MESSAGES);
|
|
925
|
+
await this.#db.ensureSkipIndexes(TABLE_RESOURCES);
|
|
864
926
|
}
|
|
865
927
|
async dangerouslyClearAll() {
|
|
866
928
|
await this.#db.clearTable({ tableName: TABLE_MESSAGES });
|
|
@@ -1203,16 +1265,9 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1203
1265
|
for (const message of messages) {
|
|
1204
1266
|
if (!message.resourceId) throw new Error("Resource ID is required");
|
|
1205
1267
|
if (!message.threadId) throw new Error("Thread ID is required");
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
const
|
|
1209
|
-
await Promise.all(messages.map(async (m) => {
|
|
1210
|
-
if (!m.resourceId) throw new Error("Resource ID is required");
|
|
1211
|
-
if (!m.threadId) throw new Error("Thread ID is required");
|
|
1212
|
-
const thread = await this.getThreadById({ threadId: m.threadId });
|
|
1213
|
-
if (!thread) throw new Error(`Thread ${m.threadId} not found`);
|
|
1214
|
-
threadIdSet.set(m.threadId, thread);
|
|
1215
|
-
}));
|
|
1268
|
+
}
|
|
1269
|
+
const threadIdSet = await this.getLatestThreads([...new Set(messages.map((m) => m.threadId))]);
|
|
1270
|
+
for (const message of messages) if (!threadIdSet.has(message.threadId)) throw new Error(`Thread ${message.threadId} not found`);
|
|
1216
1271
|
try {
|
|
1217
1272
|
const existingRows = await (await this.client.query({
|
|
1218
1273
|
query: `SELECT id, thread_id FROM ${TABLE_MESSAGES} WHERE id IN ({ids:Array(String)})`,
|
|
@@ -1315,6 +1370,38 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1315
1370
|
}, error);
|
|
1316
1371
|
}
|
|
1317
1372
|
}
|
|
1373
|
+
/** Newest version of each requested thread, keyed by id. Missing threads are absent. */
|
|
1374
|
+
async getLatestThreads(threadIds) {
|
|
1375
|
+
const rows = await (await this.client.query({
|
|
1376
|
+
query: `SELECT
|
|
1377
|
+
id,
|
|
1378
|
+
"resourceId",
|
|
1379
|
+
title,
|
|
1380
|
+
metadata,
|
|
1381
|
+
toDateTime64(createdAt, 3) as createdAt,
|
|
1382
|
+
toDateTime64(updatedAt, 3) as updatedAt
|
|
1383
|
+
FROM "${TABLE_THREADS}"
|
|
1384
|
+
WHERE id IN {threadIds:Array(String)}
|
|
1385
|
+
ORDER BY updatedAt DESC
|
|
1386
|
+
LIMIT 1 BY id`,
|
|
1387
|
+
query_params: { threadIds },
|
|
1388
|
+
clickhouse_settings: {
|
|
1389
|
+
date_time_input_format: "best_effort",
|
|
1390
|
+
date_time_output_format: "iso",
|
|
1391
|
+
use_client_time_zone: 1,
|
|
1392
|
+
output_format_json_quote_64bit_integers: 0
|
|
1393
|
+
}
|
|
1394
|
+
})).json();
|
|
1395
|
+
const threads = /* @__PURE__ */ new Map();
|
|
1396
|
+
for (const row of rows.data) {
|
|
1397
|
+
const thread = transformRow(row);
|
|
1398
|
+
threads.set(thread.id, {
|
|
1399
|
+
...thread,
|
|
1400
|
+
metadata: parseMetadata(thread.metadata)
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
return threads;
|
|
1404
|
+
}
|
|
1318
1405
|
async getThreadById({ threadId, resourceId }) {
|
|
1319
1406
|
try {
|
|
1320
1407
|
const thread = transformRow((await (await this.client.query({
|
|
@@ -1489,19 +1576,27 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1489
1576
|
metadataIndex++;
|
|
1490
1577
|
}
|
|
1491
1578
|
}
|
|
1579
|
+
const latestThreads = `
|
|
1580
|
+
latest_threads AS (
|
|
1581
|
+
SELECT
|
|
1582
|
+
id,
|
|
1583
|
+
resourceId,
|
|
1584
|
+
title,
|
|
1585
|
+
metadata,
|
|
1586
|
+
toDateTime64(createdAt, 3) as createdAt,
|
|
1587
|
+
toDateTime64(updatedAt, 3) as updatedAt
|
|
1588
|
+
FROM ${TABLE_THREADS}
|
|
1589
|
+
${filter?.resourceId ? `WHERE id IN (SELECT id FROM ${TABLE_THREADS} WHERE resourceId = {resourceId:String})` : ""}
|
|
1590
|
+
ORDER BY updatedAt DESC
|
|
1591
|
+
LIMIT 1 BY id
|
|
1592
|
+
)`;
|
|
1593
|
+
const latestWhere = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
|
1492
1594
|
const total = (await (await this.client.query({
|
|
1493
1595
|
query: `
|
|
1494
|
-
WITH
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
metadata,
|
|
1499
|
-
ROW_NUMBER() OVER (PARTITION BY id ORDER BY updatedAt DESC) as row_num
|
|
1500
|
-
FROM ${TABLE_THREADS}
|
|
1501
|
-
)
|
|
1502
|
-
SELECT count(*) as total
|
|
1503
|
-
FROM ranked_threads
|
|
1504
|
-
WHERE row_num = 1 ${whereClauses.length > 0 ? `AND ${whereClauses.join(" AND ")}` : ""}
|
|
1596
|
+
WITH ${latestThreads}
|
|
1597
|
+
SELECT count(*) as total
|
|
1598
|
+
FROM latest_threads
|
|
1599
|
+
${latestWhere}
|
|
1505
1600
|
`,
|
|
1506
1601
|
query_params: queryParams,
|
|
1507
1602
|
clickhouse_settings: {
|
|
@@ -1521,17 +1616,7 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1521
1616
|
return {
|
|
1522
1617
|
threads: transformRows((await (await this.client.query({
|
|
1523
1618
|
query: `
|
|
1524
|
-
WITH
|
|
1525
|
-
SELECT
|
|
1526
|
-
id,
|
|
1527
|
-
resourceId,
|
|
1528
|
-
title,
|
|
1529
|
-
metadata,
|
|
1530
|
-
toDateTime64(createdAt, 3) as createdAt,
|
|
1531
|
-
toDateTime64(updatedAt, 3) as updatedAt,
|
|
1532
|
-
ROW_NUMBER() OVER (PARTITION BY id ORDER BY updatedAt DESC) as row_num
|
|
1533
|
-
FROM ${TABLE_THREADS}
|
|
1534
|
-
)
|
|
1619
|
+
WITH ${latestThreads}
|
|
1535
1620
|
SELECT
|
|
1536
1621
|
id,
|
|
1537
1622
|
resourceId,
|
|
@@ -1539,8 +1624,8 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1539
1624
|
metadata,
|
|
1540
1625
|
createdAt,
|
|
1541
1626
|
updatedAt
|
|
1542
|
-
FROM
|
|
1543
|
-
|
|
1627
|
+
FROM latest_threads
|
|
1628
|
+
${latestWhere}
|
|
1544
1629
|
ORDER BY "${field}" ${direction === "DESC" ? "DESC" : "ASC"}
|
|
1545
1630
|
LIMIT {perPage:Int64} OFFSET {offset:Int64}
|
|
1546
1631
|
`,
|
|
@@ -1653,37 +1738,32 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1653
1738
|
clickhouse_settings: {
|
|
1654
1739
|
date_time_input_format: "best_effort",
|
|
1655
1740
|
use_client_time_zone: 1,
|
|
1656
|
-
output_format_json_quote_64bit_integers: 0
|
|
1741
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1742
|
+
mutations_sync: "1"
|
|
1657
1743
|
}
|
|
1658
1744
|
}));
|
|
1659
1745
|
}
|
|
1660
1746
|
}
|
|
1661
1747
|
if (updatePromises.length > 0) await Promise.all(updatePromises);
|
|
1662
|
-
await this.client.
|
|
1663
|
-
query: `
|
|
1748
|
+
const verifyResult = await this.client.query({
|
|
1749
|
+
query: `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${TABLE_MESSAGES} WHERE id IN {messageIds:Array(String)}`,
|
|
1750
|
+
query_params: { messageIds },
|
|
1664
1751
|
clickhouse_settings: {
|
|
1665
1752
|
date_time_input_format: "best_effort",
|
|
1753
|
+
date_time_output_format: "iso",
|
|
1666
1754
|
use_client_time_zone: 1,
|
|
1667
1755
|
output_format_json_quote_64bit_integers: 0
|
|
1668
1756
|
}
|
|
1669
1757
|
});
|
|
1758
|
+
const verifiedById = /* @__PURE__ */ new Map();
|
|
1759
|
+
for (const row of transformRows((await verifyResult.json()).data)) if (!verifiedById.has(row.id)) verifiedById.set(row.id, row);
|
|
1670
1760
|
for (const existingMessage of parsedExistingMessages) {
|
|
1671
1761
|
const updatePayload = messages.find((m) => m.id === existingMessage.id);
|
|
1672
1762
|
if (!updatePayload) continue;
|
|
1673
1763
|
const { id, ...fieldsToUpdate } = updatePayload;
|
|
1674
1764
|
if (Object.keys(fieldsToUpdate).length === 0) continue;
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
query_params: { messageId: id },
|
|
1678
|
-
clickhouse_settings: {
|
|
1679
|
-
date_time_input_format: "best_effort",
|
|
1680
|
-
date_time_output_format: "iso",
|
|
1681
|
-
use_client_time_zone: 1,
|
|
1682
|
-
output_format_json_quote_64bit_integers: 0
|
|
1683
|
-
}
|
|
1684
|
-
})).json();
|
|
1685
|
-
if (verifyRows.data.length > 0) {
|
|
1686
|
-
const updatedMessage = transformRows(verifyRows.data)[0];
|
|
1765
|
+
if (verifiedById.has(id)) {
|
|
1766
|
+
const updatedMessage = verifiedById.get(id);
|
|
1687
1767
|
if (updatedMessage) {
|
|
1688
1768
|
let needsRetry = false;
|
|
1689
1769
|
for (const [key, value] of Object.entries(fieldsToUpdate)) if (key === "content") {
|
|
@@ -1750,68 +1830,42 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1750
1830
|
}
|
|
1751
1831
|
if (threadIdsToUpdate.size > 0) {
|
|
1752
1832
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1753
|
-
const now =
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
const existingThread = threadRows.data[0];
|
|
1767
|
-
await this.client.command({
|
|
1768
|
-
query: `DELETE FROM ${TABLE_THREADS} WHERE id = {threadId:String}`,
|
|
1769
|
-
query_params: { threadId },
|
|
1770
|
-
clickhouse_settings: {
|
|
1771
|
-
date_time_input_format: "best_effort",
|
|
1772
|
-
use_client_time_zone: 1,
|
|
1773
|
-
output_format_json_quote_64bit_integers: 0
|
|
1774
|
-
}
|
|
1775
|
-
});
|
|
1776
|
-
await this.client.insert({
|
|
1777
|
-
table: TABLE_THREADS,
|
|
1778
|
-
format: "JSONEachRow",
|
|
1779
|
-
values: [{
|
|
1780
|
-
id: existingThread.id,
|
|
1781
|
-
resourceId: existingThread.resourceId,
|
|
1782
|
-
title: existingThread.title,
|
|
1783
|
-
metadata: typeof existingThread.metadata === "string" ? existingThread.metadata : serializeMetadata(existingThread.metadata),
|
|
1784
|
-
createdAt: existingThread.createdAt,
|
|
1785
|
-
updatedAt: now
|
|
1786
|
-
}],
|
|
1787
|
-
clickhouse_settings: {
|
|
1788
|
-
date_time_input_format: "best_effort",
|
|
1789
|
-
use_client_time_zone: 1,
|
|
1790
|
-
output_format_json_quote_64bit_integers: 0
|
|
1791
|
-
}
|
|
1792
|
-
});
|
|
1793
|
-
}
|
|
1794
|
-
});
|
|
1795
|
-
await Promise.all(threadUpdatePromises);
|
|
1796
|
-
}
|
|
1797
|
-
const updatedMessages = [];
|
|
1798
|
-
for (const messageId of messageIds) {
|
|
1799
|
-
const updatedRows = await (await this.client.query({
|
|
1800
|
-
query: `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${TABLE_MESSAGES} WHERE id = {messageId:String}`,
|
|
1801
|
-
query_params: { messageId },
|
|
1833
|
+
const now = Date.now();
|
|
1834
|
+
const existingThreads = await this.getLatestThreads(Array.from(threadIdsToUpdate));
|
|
1835
|
+
if (existingThreads.size > 0) await this.client.insert({
|
|
1836
|
+
table: TABLE_THREADS,
|
|
1837
|
+
format: "JSONEachRow",
|
|
1838
|
+
values: Array.from(existingThreads.values()).map((existingThread) => ({
|
|
1839
|
+
id: existingThread.id,
|
|
1840
|
+
resourceId: existingThread.resourceId,
|
|
1841
|
+
title: existingThread.title,
|
|
1842
|
+
metadata: serializeMetadata(existingThread.metadata),
|
|
1843
|
+
createdAt: existingThread.createdAt,
|
|
1844
|
+
updatedAt: new Date(Math.max(now, new Date(existingThread.updatedAt).getTime() + 1)).toISOString().replace("Z", "")
|
|
1845
|
+
})),
|
|
1802
1846
|
clickhouse_settings: {
|
|
1803
1847
|
date_time_input_format: "best_effort",
|
|
1804
|
-
date_time_output_format: "iso",
|
|
1805
1848
|
use_client_time_zone: 1,
|
|
1806
1849
|
output_format_json_quote_64bit_integers: 0
|
|
1807
1850
|
}
|
|
1808
|
-
})
|
|
1809
|
-
if (updatedRows.data.length > 0) {
|
|
1810
|
-
const message = transformRows(updatedRows.data)[0];
|
|
1811
|
-
if (message) updatedMessages.push(message);
|
|
1812
|
-
}
|
|
1851
|
+
});
|
|
1813
1852
|
}
|
|
1814
|
-
|
|
1853
|
+
const updatedResult = await this.client.query({
|
|
1854
|
+
query: `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${TABLE_MESSAGES} WHERE id IN {messageIds:Array(String)}`,
|
|
1855
|
+
query_params: { messageIds },
|
|
1856
|
+
clickhouse_settings: {
|
|
1857
|
+
date_time_input_format: "best_effort",
|
|
1858
|
+
date_time_output_format: "iso",
|
|
1859
|
+
use_client_time_zone: 1,
|
|
1860
|
+
output_format_json_quote_64bit_integers: 0
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
const updatedById = /* @__PURE__ */ new Map();
|
|
1864
|
+
for (const row of transformRows((await updatedResult.json()).data)) if (!updatedById.has(row.id)) updatedById.set(row.id, row);
|
|
1865
|
+
return messageIds.flatMap((messageId) => {
|
|
1866
|
+
const message = updatedById.get(messageId);
|
|
1867
|
+
return message ? [message] : [];
|
|
1868
|
+
}).map((message) => {
|
|
1815
1869
|
if (typeof message.content === "string") try {
|
|
1816
1870
|
message.content = JSON.parse(message.content);
|
|
1817
1871
|
} catch {}
|
|
@@ -1907,35 +1961,9 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1907
1961
|
...existingResource.metadata,
|
|
1908
1962
|
...metadata
|
|
1909
1963
|
},
|
|
1910
|
-
updatedAt:
|
|
1964
|
+
updatedAt: new Date(Math.max(Date.now(), existingResource.updatedAt.getTime() + 1))
|
|
1911
1965
|
};
|
|
1912
|
-
|
|
1913
|
-
ALTER TABLE ${TABLE_RESOURCES}
|
|
1914
|
-
UPDATE workingMemory = {workingMemory:String}, metadata = {metadata:String}, updatedAt = {updatedAt:String}
|
|
1915
|
-
WHERE id = {resourceId:String}
|
|
1916
|
-
`;
|
|
1917
|
-
await this.client.command({
|
|
1918
|
-
query: updateQuery,
|
|
1919
|
-
query_params: {
|
|
1920
|
-
workingMemory: updatedResource.workingMemory,
|
|
1921
|
-
metadata: JSON.stringify(updatedResource.metadata),
|
|
1922
|
-
updatedAt: updatedResource.updatedAt.toISOString().replace("Z", ""),
|
|
1923
|
-
resourceId
|
|
1924
|
-
},
|
|
1925
|
-
clickhouse_settings: {
|
|
1926
|
-
date_time_input_format: "best_effort",
|
|
1927
|
-
use_client_time_zone: 1,
|
|
1928
|
-
output_format_json_quote_64bit_integers: 0
|
|
1929
|
-
}
|
|
1930
|
-
});
|
|
1931
|
-
await this.client.command({
|
|
1932
|
-
query: `OPTIMIZE TABLE ${TABLE_RESOURCES} FINAL`,
|
|
1933
|
-
clickhouse_settings: {
|
|
1934
|
-
date_time_input_format: "best_effort",
|
|
1935
|
-
use_client_time_zone: 1,
|
|
1936
|
-
output_format_json_quote_64bit_integers: 0
|
|
1937
|
-
}
|
|
1938
|
-
});
|
|
1966
|
+
await this.saveResource({ resource: updatedResource });
|
|
1939
1967
|
return updatedResource;
|
|
1940
1968
|
} catch (error) {
|
|
1941
1969
|
throw new MastraError({
|
|
@@ -2383,14 +2411,10 @@ var ObservabilityStorageClickhouse = class extends ObservabilityStorage {
|
|
|
2383
2411
|
}
|
|
2384
2412
|
if (filters.hasChildError !== void 0) {
|
|
2385
2413
|
const finalClause = (TABLE_ENGINES[TABLE_SPANS] ?? "MergeTree()").startsWith("ReplacingMergeTree") ? "FINAL" : "";
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
else conditions.push(`NOT EXISTS (
|
|
2391
|
-
SELECT 1 FROM ${TABLE_SPANS} ${finalClause} c
|
|
2392
|
-
WHERE c.traceId = ${TABLE_SPANS}.traceId AND c.error IS NOT NULL AND c.error != ''
|
|
2393
|
-
)`);
|
|
2414
|
+
conditions.push(`traceId ${filters.hasChildError ? "IN" : "NOT IN"} (
|
|
2415
|
+
SELECT c.traceId FROM ${TABLE_SPANS} ${finalClause} c
|
|
2416
|
+
WHERE c.error IS NOT NULL AND c.error != ''
|
|
2417
|
+
)`);
|
|
2394
2418
|
}
|
|
2395
2419
|
}
|
|
2396
2420
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
@@ -2482,35 +2506,58 @@ var ObservabilityStorageClickhouse = class extends ObservabilityStorage {
|
|
|
2482
2506
|
async batchUpdateSpans(args) {
|
|
2483
2507
|
try {
|
|
2484
2508
|
const now = Date.now();
|
|
2509
|
+
if (args.records.length === 0) return;
|
|
2510
|
+
const keyParams = {};
|
|
2511
|
+
const keyTuples = args.records.map((record, i) => {
|
|
2512
|
+
keyParams[`t_${i}`] = record.traceId;
|
|
2513
|
+
keyParams[`s_${i}`] = record.spanId;
|
|
2514
|
+
return `({t_${i}:String}, {s_${i}:String})`;
|
|
2515
|
+
});
|
|
2516
|
+
const existingResult = await this.client.query({
|
|
2517
|
+
query: `SELECT *, toDateTime64(createdAt, 3) as createdAt, toDateTime64(updatedAt, 3) as updatedAt
|
|
2518
|
+
FROM ${TABLE_SPANS}
|
|
2519
|
+
WHERE (traceId, spanId) IN (${keyTuples.join(", ")})
|
|
2520
|
+
-- Newest version per span, which is what ReplacingMergeTree(updatedAt)
|
|
2521
|
+
-- keeps. FINAL would merge every part the batch's keys touch, which
|
|
2522
|
+
-- costs far more than reading the unmerged versions and sorting them.
|
|
2523
|
+
ORDER BY updatedAt DESC
|
|
2524
|
+
LIMIT 1 BY traceId, spanId`,
|
|
2525
|
+
query_params: keyParams,
|
|
2526
|
+
clickhouse_settings: {
|
|
2527
|
+
date_time_input_format: "best_effort",
|
|
2528
|
+
date_time_output_format: "iso",
|
|
2529
|
+
use_client_time_zone: 1,
|
|
2530
|
+
output_format_json_quote_64bit_integers: 0
|
|
2531
|
+
}
|
|
2532
|
+
});
|
|
2533
|
+
const spanKey = (traceId, spanId) => `${traceId}\u0000${spanId}`;
|
|
2534
|
+
const current = /* @__PURE__ */ new Map();
|
|
2535
|
+
for (const row of transformRows((await existingResult.json()).data)) current.set(spanKey(row.traceId, row.spanId), row);
|
|
2485
2536
|
for (const record of args.records) {
|
|
2486
|
-
const
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2537
|
+
const key = spanKey(record.traceId, record.spanId);
|
|
2538
|
+
const existing = current.get(key);
|
|
2539
|
+
if (!existing) continue;
|
|
2540
|
+
const updates = { ...record.updates };
|
|
2541
|
+
if (updates.startedAt instanceof Date) updates.startedAt = updates.startedAt.getTime();
|
|
2542
|
+
if (updates.endedAt instanceof Date) updates.endedAt = updates.endedAt.getTime();
|
|
2543
|
+
current.set(key, {
|
|
2544
|
+
...existing,
|
|
2545
|
+
...updates,
|
|
2546
|
+
updatedAt: now,
|
|
2547
|
+
__updated: true
|
|
2492
2548
|
});
|
|
2493
|
-
if (existing) {
|
|
2494
|
-
const updates = { ...record.updates };
|
|
2495
|
-
if (updates.startedAt instanceof Date) updates.startedAt = updates.startedAt.getTime();
|
|
2496
|
-
if (updates.endedAt instanceof Date) updates.endedAt = updates.endedAt.getTime();
|
|
2497
|
-
const updated = {
|
|
2498
|
-
...existing,
|
|
2499
|
-
...updates,
|
|
2500
|
-
updatedAt: now
|
|
2501
|
-
};
|
|
2502
|
-
await this.client.insert({
|
|
2503
|
-
table: TABLE_SPANS,
|
|
2504
|
-
values: [updated],
|
|
2505
|
-
format: "JSONEachRow",
|
|
2506
|
-
clickhouse_settings: {
|
|
2507
|
-
date_time_input_format: "best_effort",
|
|
2508
|
-
use_client_time_zone: 1,
|
|
2509
|
-
output_format_json_quote_64bit_integers: 0
|
|
2510
|
-
}
|
|
2511
|
-
});
|
|
2512
|
-
}
|
|
2513
2549
|
}
|
|
2550
|
+
const updatedRows = Array.from(current.values()).filter((row) => row.__updated).map(({ __updated: _updated, ...row }) => row);
|
|
2551
|
+
if (updatedRows.length > 0) await this.client.insert({
|
|
2552
|
+
table: TABLE_SPANS,
|
|
2553
|
+
values: updatedRows,
|
|
2554
|
+
format: "JSONEachRow",
|
|
2555
|
+
clickhouse_settings: {
|
|
2556
|
+
date_time_input_format: "best_effort",
|
|
2557
|
+
use_client_time_zone: 1,
|
|
2558
|
+
output_format_json_quote_64bit_integers: 0
|
|
2559
|
+
}
|
|
2560
|
+
});
|
|
2514
2561
|
} catch (error) {
|
|
2515
2562
|
throw new MastraError({
|
|
2516
2563
|
id: createStorageErrorId("CLICKHOUSE", "BATCH_UPDATE_SPANS", "FAILED"),
|
|
@@ -2739,7 +2786,12 @@ CREATE TABLE IF NOT EXISTS ${TABLE_TRACE_ROOTS} (
|
|
|
2739
2786
|
output Nullable(String),
|
|
2740
2787
|
error Nullable(String),
|
|
2741
2788
|
metadataRaw Nullable(String),
|
|
2742
|
-
requestContext Nullable(String)
|
|
2789
|
+
requestContext Nullable(String),
|
|
2790
|
+
|
|
2791
|
+
-- traceId is second in the sort key, behind startedAt, so point lookups by
|
|
2792
|
+
-- traceId (getRootSpan, trace deletes, trace-query root dedupe) need a skip
|
|
2793
|
+
-- index to avoid scanning every granule.
|
|
2794
|
+
INDEX idx_traceId traceId TYPE bloom_filter(0.01) GRANULARITY 1
|
|
2743
2795
|
)
|
|
2744
2796
|
ENGINE = ReplacingMergeTree
|
|
2745
2797
|
PARTITION BY toDate(endedAt)
|
|
@@ -2813,7 +2865,9 @@ CREATE TABLE IF NOT EXISTS ${TABLE_TRACE_BRANCHES} (
|
|
|
2813
2865
|
output Nullable(String),
|
|
2814
2866
|
error Nullable(String),
|
|
2815
2867
|
metadataRaw Nullable(String),
|
|
2816
|
-
requestContext Nullable(String)
|
|
2868
|
+
requestContext Nullable(String),
|
|
2869
|
+
|
|
2870
|
+
INDEX idx_traceId traceId TYPE bloom_filter(0.01) GRANULARITY 1
|
|
2817
2871
|
)
|
|
2818
2872
|
ENGINE = ReplacingMergeTree
|
|
2819
2873
|
PARTITION BY toDate(endedAt)
|
|
@@ -3042,7 +3096,19 @@ CREATE TABLE IF NOT EXISTS ${TABLE_LOG_EVENTS} (
|
|
|
3042
3096
|
-- Information-only JSON payloads
|
|
3043
3097
|
data Nullable(String),
|
|
3044
3098
|
metadata Nullable(String),
|
|
3045
|
-
scope Nullable(String)
|
|
3099
|
+
scope Nullable(String),
|
|
3100
|
+
|
|
3101
|
+
-- Same ID drilldown indexes as metric_events: these columns are outside
|
|
3102
|
+
-- the sort key, so without them every drilldown scans the time range.
|
|
3103
|
+
INDEX idx_traceId traceId TYPE bloom_filter(0.01) GRANULARITY 2,
|
|
3104
|
+
INDEX idx_threadId threadId TYPE bloom_filter(0.01) GRANULARITY 2,
|
|
3105
|
+
INDEX idx_resourceId resourceId TYPE bloom_filter(0.01) GRANULARITY 2,
|
|
3106
|
+
INDEX idx_userId userId TYPE bloom_filter(0.01) GRANULARITY 2,
|
|
3107
|
+
INDEX idx_organizationId organizationId TYPE bloom_filter(0.01) GRANULARITY 2,
|
|
3108
|
+
INDEX idx_experimentId experimentId TYPE bloom_filter(0.01) GRANULARITY 2,
|
|
3109
|
+
INDEX idx_runId runId TYPE bloom_filter(0.01) GRANULARITY 2,
|
|
3110
|
+
INDEX idx_sessionId sessionId TYPE bloom_filter(0.01) GRANULARITY 2,
|
|
3111
|
+
INDEX idx_requestId requestId TYPE bloom_filter(0.01) GRANULARITY 2
|
|
3046
3112
|
)
|
|
3047
3113
|
ENGINE = ReplacingMergeTree
|
|
3048
3114
|
PARTITION BY toDate(timestamp)
|
|
@@ -3171,7 +3237,8 @@ SETTINGS allow_nullable_key = 1
|
|
|
3171
3237
|
`;
|
|
3172
3238
|
const SCORE_EVENTS_CURRENT_DDL = `
|
|
3173
3239
|
CREATE TABLE IF NOT EXISTS ${TABLE_SCORE_EVENTS_CURRENT} (
|
|
3174
|
-
${SCORE_EVENT_COLUMNS_DDL}
|
|
3240
|
+
${SCORE_EVENT_COLUMNS_DDL},
|
|
3241
|
+
INDEX idx_traceId traceId TYPE bloom_filter(0.01) GRANULARITY 1
|
|
3175
3242
|
)
|
|
3176
3243
|
ENGINE = ReplacingMergeTree(writeVersion)
|
|
3177
3244
|
PARTITION BY cityHash64(scoreId) % 64
|
|
@@ -3299,7 +3366,11 @@ CREATE TABLE IF NOT EXISTS ${TABLE_FEEDBACK_EVENTS} (
|
|
|
3299
3366
|
|
|
3300
3367
|
-- Information-only JSON payloads
|
|
3301
3368
|
metadata Nullable(String),
|
|
3302
|
-
scope Nullable(String)
|
|
3369
|
+
scope Nullable(String),
|
|
3370
|
+
|
|
3371
|
+
-- feedbackId is last in the sort key; id lookups (write versions, review
|
|
3372
|
+
-- updates, deletes, trace-query dedupe) need a skip index.
|
|
3373
|
+
INDEX idx_feedbackId feedbackId TYPE bloom_filter(0.01) GRANULARITY 1
|
|
3303
3374
|
)
|
|
3304
3375
|
ENGINE = ReplacingMergeTree
|
|
3305
3376
|
PARTITION BY toDate(timestamp)
|
|
@@ -3545,11 +3616,15 @@ const ALL_MIGRATIONS = [
|
|
|
3545
3616
|
addColumn(TABLE_SCORE_EVENTS, "rootEntityVersionId", "Nullable(String)"),
|
|
3546
3617
|
addBloomIndex(TABLE_SCORE_EVENTS, "idx_scoreId", "scoreId", 1),
|
|
3547
3618
|
addBloomIndex(TABLE_SCORE_EVENTS_DELTA, "idx_scoreId", "scoreId", 1),
|
|
3619
|
+
addBloomIndex(TABLE_SCORE_EVENTS_CURRENT, "idx_traceId", "traceId", 1),
|
|
3548
3620
|
addColumn(TABLE_FEEDBACK_EVENTS, "writeVersion", "UInt64 DEFAULT 0"),
|
|
3549
3621
|
addColumn(TABLE_FEEDBACK_EVENTS, "entityVersionId", "Nullable(String)"),
|
|
3550
3622
|
addColumn(TABLE_FEEDBACK_EVENTS, "reviewStatus", "LowCardinality(String) DEFAULT 'needs-review'"),
|
|
3551
3623
|
addColumn(TABLE_FEEDBACK_EVENTS, "parentEntityVersionId", "Nullable(String)"),
|
|
3552
3624
|
addColumn(TABLE_FEEDBACK_EVENTS, "rootEntityVersionId", "Nullable(String)"),
|
|
3625
|
+
addBloomIndex(TABLE_FEEDBACK_EVENTS, "idx_feedbackId", "feedbackId", 1),
|
|
3626
|
+
addBloomIndex(TABLE_TRACE_ROOTS, "idx_traceId", "traceId", 1),
|
|
3627
|
+
addBloomIndex(TABLE_TRACE_BRANCHES, "idx_traceId", "traceId", 1),
|
|
3553
3628
|
addBloomIndex(TABLE_METRIC_EVENTS, "idx_traceId", "traceId"),
|
|
3554
3629
|
addBloomIndex(TABLE_METRIC_EVENTS, "idx_threadId", "threadId"),
|
|
3555
3630
|
addBloomIndex(TABLE_METRIC_EVENTS, "idx_resourceId", "resourceId"),
|
|
@@ -3559,6 +3634,17 @@ const ALL_MIGRATIONS = [
|
|
|
3559
3634
|
addBloomIndex(TABLE_METRIC_EVENTS, "idx_runId", "runId"),
|
|
3560
3635
|
addBloomIndex(TABLE_METRIC_EVENTS, "idx_sessionId", "sessionId"),
|
|
3561
3636
|
addBloomIndex(TABLE_METRIC_EVENTS, "idx_requestId", "requestId"),
|
|
3637
|
+
...[
|
|
3638
|
+
"traceId",
|
|
3639
|
+
"threadId",
|
|
3640
|
+
"resourceId",
|
|
3641
|
+
"userId",
|
|
3642
|
+
"organizationId",
|
|
3643
|
+
"experimentId",
|
|
3644
|
+
"runId",
|
|
3645
|
+
"sessionId",
|
|
3646
|
+
"requestId"
|
|
3647
|
+
].map((column) => addBloomIndex(TABLE_LOG_EVENTS, `idx_${column}`, column)),
|
|
3562
3648
|
addBloomIndex(TABLE_DELETION_REQUESTS, "idx_predicateValues", "predicateValues")
|
|
3563
3649
|
];
|
|
3564
3650
|
const ALL_TABLE_NAMES = [
|
|
@@ -4654,6 +4740,11 @@ function validateCursorId(cursor) {
|
|
|
4654
4740
|
text: "Invalid observability delta cursor"
|
|
4655
4741
|
});
|
|
4656
4742
|
}
|
|
4743
|
+
/** ANDs `conditions` onto a `WHERE ...` clause, or starts one when `whereClause` is empty. */
|
|
4744
|
+
function appendWhere(whereClause, ...conditions) {
|
|
4745
|
+
const parts = [whereClause.replace(/^\s*WHERE\s+/i, ""), ...conditions].filter(Boolean);
|
|
4746
|
+
return parts.length > 0 ? `WHERE ${parts.join(" AND ")}` : "";
|
|
4747
|
+
}
|
|
4657
4748
|
//#endregion
|
|
4658
4749
|
//#region src/storage/domains/observability/v-next/feedback.ts
|
|
4659
4750
|
const FEEDBACK_TYPED_COLUMNS = /* @__PURE__ */ new Set([
|
|
@@ -4760,12 +4851,21 @@ function resolveFeedbackGroupBy(groupBy) {
|
|
|
4760
4851
|
function toSeriesName$1(values) {
|
|
4761
4852
|
return values.map((v) => v == null ? "" : String(v)).join("|");
|
|
4762
4853
|
}
|
|
4763
|
-
|
|
4854
|
+
/**
|
|
4855
|
+
* Lets `FINAL` reads use skip indexes on older servers, where it defaults off.
|
|
4856
|
+
* Only for lookups on a column shared by every version of a sort key (so a
|
|
4857
|
+
* skipped granule can never hold a newer version of a matching row).
|
|
4858
|
+
*/
|
|
4859
|
+
const FINAL_SKIP_INDEX_SETTINGS = { use_skip_indexes_if_final: 1 };
|
|
4860
|
+
async function queryJson$2(client, query, params, settings = {}) {
|
|
4764
4861
|
return await (await client.query({
|
|
4765
4862
|
query,
|
|
4766
4863
|
query_params: params,
|
|
4767
4864
|
format: "JSONEachRow",
|
|
4768
|
-
clickhouse_settings:
|
|
4865
|
+
clickhouse_settings: {
|
|
4866
|
+
...CH_SETTINGS,
|
|
4867
|
+
...settings
|
|
4868
|
+
}
|
|
4769
4869
|
})).json();
|
|
4770
4870
|
}
|
|
4771
4871
|
async function feedbackRowsWithWriteVersions(client, feedbacks) {
|
|
@@ -4874,7 +4974,7 @@ async function hasFeedbackDeletionRequest(client, feedbackId, organizationId, re
|
|
|
4874
4974
|
feedbackId,
|
|
4875
4975
|
organizationId: organizationId ?? "",
|
|
4876
4976
|
resourceId: resourceId ?? ""
|
|
4877
|
-
})).length > 0;
|
|
4977
|
+
}, FINAL_SKIP_INDEX_SETTINGS)).length > 0;
|
|
4878
4978
|
}
|
|
4879
4979
|
/**
|
|
4880
4980
|
* Review updates insert a replacement row, so they need only `INSERT` and the
|
|
@@ -4895,7 +4995,7 @@ async function updateFeedbackReviewStatus(client, args, replication) {
|
|
|
4895
4995
|
const existingRow = (await queryJson$2(client, `SELECT * FROM ${TABLE_FEEDBACK_EVENTS} FINAL
|
|
4896
4996
|
WHERE feedbackId = {feedbackId:String}
|
|
4897
4997
|
ORDER BY writeVersion DESC, timestamp DESC
|
|
4898
|
-
LIMIT 1`, { feedbackId }))[0];
|
|
4998
|
+
LIMIT 1`, { feedbackId }, FINAL_SKIP_INDEX_SETTINGS))[0];
|
|
4899
4999
|
if (!existingRow) throw feedbackNotFoundError(feedbackId);
|
|
4900
5000
|
const { organizationId, resourceId } = existingRow;
|
|
4901
5001
|
const redelete = async () => {
|
|
@@ -4972,12 +5072,16 @@ async function queryFeedbackAfterCursor(client, whereClause, params, limit, curs
|
|
|
4972
5072
|
f.timestamp AS timestamp,
|
|
4973
5073
|
f.feedbackId AS feedbackId,
|
|
4974
5074
|
toString(d.cursorId) AS cursorId
|
|
4975
|
-
FROM ${
|
|
4976
|
-
INNER JOIN
|
|
4977
|
-
|
|
5075
|
+
FROM ${TABLE_FEEDBACK_EVENTS} f FINAL
|
|
5076
|
+
INNER JOIN (
|
|
5077
|
+
SELECT cursorId, traceId, timestamp, feedbackId
|
|
5078
|
+
FROM ${TABLE_FEEDBACK_EVENTS_DELTA}
|
|
5079
|
+
WHERE cursorId > {afterCursor:UInt64}
|
|
5080
|
+
) d
|
|
5081
|
+
ON isNotDistinctFrom(f.traceId, d.traceId)
|
|
4978
5082
|
AND f.timestamp = d.timestamp
|
|
4979
5083
|
AND f.feedbackId = d.feedbackId
|
|
4980
|
-
${whereClause
|
|
5084
|
+
${appendWhere(whereClause, `f.timestamp >= (SELECT min(timestamp) FROM ${TABLE_FEEDBACK_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64})`, `(f.timestamp, f.feedbackId) IN (${`SELECT timestamp, feedbackId FROM ${TABLE_FEEDBACK_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64}`})`)}
|
|
4981
5085
|
ORDER BY d.cursorId ASC
|
|
4982
5086
|
LIMIT {fetchLimit:UInt32}
|
|
4983
5087
|
`, {
|
|
@@ -4986,18 +5090,24 @@ async function queryFeedbackAfterCursor(client, whereClause, params, limit, curs
|
|
|
4986
5090
|
fetchLimit: limit + 1
|
|
4987
5091
|
});
|
|
4988
5092
|
}
|
|
5093
|
+
/**
|
|
5094
|
+
* Newest delta cursor whose feedback matches the filters. Without filters
|
|
5095
|
+
* this is the stream head; with filters, the feedback scan is bounded below by
|
|
5096
|
+
* the oldest `timestamp` still in the delta table.
|
|
5097
|
+
*/
|
|
4989
5098
|
async function getDeltaCursor$5(client, whereClause, params) {
|
|
5099
|
+
if (!whereClause) return getStreamHeadCursor$5(client);
|
|
4990
5100
|
const cursorId = (await queryJson$2(client, `
|
|
4991
5101
|
SELECT toString(max(d.cursorId)) AS cursorId
|
|
4992
5102
|
FROM mastra_feedback_events_delta d
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
5103
|
+
WHERE (d.timestamp, d.feedbackId) IN (
|
|
5104
|
+
SELECT f.timestamp, f.feedbackId
|
|
5105
|
+
FROM mastra_feedback_events f FINAL
|
|
5106
|
+
${appendWhere(whereClause, `f.timestamp >= (SELECT min(timestamp) FROM mastra_feedback_events_delta)`)}
|
|
5107
|
+
)
|
|
4998
5108
|
`, params))[0]?.cursorId ?? null;
|
|
4999
5109
|
if (cursorId) return cursorId;
|
|
5000
|
-
return
|
|
5110
|
+
return getStreamHeadCursor$5(client);
|
|
5001
5111
|
}
|
|
5002
5112
|
async function getStreamHeadCursor$5(client) {
|
|
5003
5113
|
return (await queryJson$2(client, `SELECT toString(max(cursorId)) AS cursorId FROM mastra_feedback_events_delta`, {}))[0]?.cursorId ?? "0";
|
|
@@ -5119,26 +5229,22 @@ async function getFeedbackPercentiles(client, args) {
|
|
|
5119
5229
|
const combined = mergeFilters$2(buildFeedbackIdentityFilter(args), buildFeedbackFilterConditions(args.filters));
|
|
5120
5230
|
const whereClause = toWhereClause$2(combined);
|
|
5121
5231
|
if (!Array.isArray(args.percentiles) || args.percentiles.length === 0) throw new Error("Percentiles must include at least one value between 0 and 1.");
|
|
5122
|
-
const
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
}))
|
|
5139
|
-
});
|
|
5140
|
-
}
|
|
5141
|
-
return { series };
|
|
5232
|
+
for (const p of args.percentiles) if (!Number.isFinite(p) || p < 0 || p > 1) throw new Error(`Percentile value must be a finite number between 0 and 1, got ${p}`);
|
|
5233
|
+
const rows = await queryJson$2(client, `
|
|
5234
|
+
SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
|
|
5235
|
+
quantiles(${args.percentiles.join(", ")})(valueNumber) AS pvalues
|
|
5236
|
+
FROM ${TABLE_FEEDBACK_EVENTS} FINAL
|
|
5237
|
+
${whereClause}
|
|
5238
|
+
GROUP BY bucket
|
|
5239
|
+
ORDER BY bucket
|
|
5240
|
+
`, combined.params);
|
|
5241
|
+
return { series: args.percentiles.map((percentile, index) => ({
|
|
5242
|
+
percentile,
|
|
5243
|
+
points: rows.map((row) => ({
|
|
5244
|
+
timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
|
|
5245
|
+
value: Number(row.pvalues?.[index] ?? 0)
|
|
5246
|
+
}))
|
|
5247
|
+
})) };
|
|
5142
5248
|
}
|
|
5143
5249
|
//#endregion
|
|
5144
5250
|
//#region src/storage/domains/observability/v-next/logs.ts
|
|
@@ -5217,6 +5323,7 @@ async function listLogs(client, args, strategy) {
|
|
|
5217
5323
|
};
|
|
5218
5324
|
}
|
|
5219
5325
|
async function queryLogsAfterCursor(client, whereClause, params, limit, cursorId) {
|
|
5326
|
+
const deltaKeys = `SELECT timestamp, logId FROM ${TABLE_LOG_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64}`;
|
|
5220
5327
|
return await (await client.query({
|
|
5221
5328
|
query: `
|
|
5222
5329
|
SELECT
|
|
@@ -5224,11 +5331,15 @@ async function queryLogsAfterCursor(client, whereClause, params, limit, cursorId
|
|
|
5224
5331
|
l.timestamp AS timestamp,
|
|
5225
5332
|
l.logId AS logId,
|
|
5226
5333
|
toString(d.cursorId) AS cursorId
|
|
5227
|
-
FROM ${
|
|
5228
|
-
INNER JOIN
|
|
5334
|
+
FROM ${TABLE_LOG_EVENTS} l
|
|
5335
|
+
INNER JOIN (
|
|
5336
|
+
SELECT cursorId, timestamp, logId
|
|
5337
|
+
FROM ${TABLE_LOG_EVENTS_DELTA}
|
|
5338
|
+
WHERE cursorId > {afterCursor:UInt64}
|
|
5339
|
+
) d
|
|
5229
5340
|
ON l.timestamp = d.timestamp
|
|
5230
5341
|
AND l.logId = d.logId
|
|
5231
|
-
${whereClause
|
|
5342
|
+
${appendWhere(whereClause, `(l.timestamp, l.logId) IN (${deltaKeys})`)}
|
|
5232
5343
|
ORDER BY d.cursorId ASC
|
|
5233
5344
|
LIMIT {fetchLimit:UInt32}
|
|
5234
5345
|
`,
|
|
@@ -5241,26 +5352,29 @@ async function queryLogsAfterCursor(client, whereClause, params, limit, cursorId
|
|
|
5241
5352
|
clickhouse_settings: CH_SETTINGS
|
|
5242
5353
|
})).json();
|
|
5243
5354
|
}
|
|
5355
|
+
/**
|
|
5356
|
+
* Newest delta cursor whose log matches the filters. Without filters this is
|
|
5357
|
+
* the stream head; with filters, the log scan is bounded below by the oldest
|
|
5358
|
+
* `timestamp` still in the delta table.
|
|
5359
|
+
*/
|
|
5244
5360
|
async function getDeltaCursor$4(client, whereClause, params) {
|
|
5361
|
+
if (!whereClause) return getStreamHeadCursor$4(client);
|
|
5245
5362
|
const cursorId = (await (await client.query({
|
|
5246
5363
|
query: `
|
|
5247
5364
|
SELECT toString(max(d.cursorId)) AS cursorId
|
|
5248
5365
|
FROM mastra_log_events_delta d
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5366
|
+
WHERE (d.timestamp, d.logId) IN (
|
|
5367
|
+
SELECT l.timestamp, l.logId
|
|
5368
|
+
FROM mastra_log_events l
|
|
5369
|
+
${appendWhere(whereClause, `l.timestamp >= (SELECT min(timestamp) FROM mastra_log_events_delta)`)}
|
|
5370
|
+
)
|
|
5253
5371
|
`,
|
|
5254
5372
|
query_params: params,
|
|
5255
5373
|
format: "JSONEachRow",
|
|
5256
5374
|
clickhouse_settings: CH_SETTINGS
|
|
5257
5375
|
})).json())[0]?.cursorId ?? null;
|
|
5258
5376
|
if (cursorId) return cursorId;
|
|
5259
|
-
return (
|
|
5260
|
-
query: `SELECT toString(max(cursorId)) AS cursorId FROM mastra_log_events_delta`,
|
|
5261
|
-
format: "JSONEachRow",
|
|
5262
|
-
clickhouse_settings: CH_SETTINGS
|
|
5263
|
-
})).json())[0]?.cursorId ?? "0";
|
|
5377
|
+
return getStreamHeadCursor$4(client);
|
|
5264
5378
|
}
|
|
5265
5379
|
async function getStreamHeadCursor$4(client) {
|
|
5266
5380
|
return (await (await client.query({
|
|
@@ -5486,12 +5600,16 @@ async function queryMetricsAfterCursor(client, whereClause, params, limit, curso
|
|
|
5486
5600
|
m.timestamp AS timestamp,
|
|
5487
5601
|
m.metricId AS metricId,
|
|
5488
5602
|
toString(d.cursorId) AS cursorId
|
|
5489
|
-
FROM ${
|
|
5490
|
-
INNER JOIN
|
|
5603
|
+
FROM ${TABLE_METRIC_EVENTS} m
|
|
5604
|
+
INNER JOIN (
|
|
5605
|
+
SELECT cursorId, name, timestamp, metricId
|
|
5606
|
+
FROM ${TABLE_METRIC_EVENTS_DELTA}
|
|
5607
|
+
WHERE cursorId > {afterCursor:UInt64}
|
|
5608
|
+
) d
|
|
5491
5609
|
ON m.name = d.name
|
|
5492
5610
|
AND m.timestamp = d.timestamp
|
|
5493
5611
|
AND m.metricId = d.metricId
|
|
5494
|
-
${whereClause
|
|
5612
|
+
${appendWhere(whereClause, `(m.name, m.timestamp, m.metricId) IN (${`SELECT name, timestamp, metricId FROM ${TABLE_METRIC_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64}`})`)}
|
|
5495
5613
|
ORDER BY d.cursorId ASC
|
|
5496
5614
|
LIMIT {fetchLimit:UInt32}
|
|
5497
5615
|
`, {
|
|
@@ -5500,18 +5618,24 @@ async function queryMetricsAfterCursor(client, whereClause, params, limit, curso
|
|
|
5500
5618
|
fetchLimit: limit + 1
|
|
5501
5619
|
});
|
|
5502
5620
|
}
|
|
5621
|
+
/**
|
|
5622
|
+
* Newest delta cursor whose metric matches the filters. Without filters this
|
|
5623
|
+
* is the stream head; with filters, the metric scan is bounded below by the
|
|
5624
|
+
* oldest `timestamp` still in the delta table.
|
|
5625
|
+
*/
|
|
5503
5626
|
async function getDeltaCursor$3(client, whereClause, params) {
|
|
5627
|
+
if (!whereClause) return getStreamHeadCursor$3(client);
|
|
5504
5628
|
const cursorId = (await queryJson$1(client, `
|
|
5505
5629
|
SELECT toString(max(d.cursorId)) AS cursorId
|
|
5506
5630
|
FROM mastra_metric_events_delta d
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5631
|
+
WHERE (d.name, d.timestamp, d.metricId) IN (
|
|
5632
|
+
SELECT m.name, m.timestamp, m.metricId
|
|
5633
|
+
FROM mastra_metric_events m
|
|
5634
|
+
${appendWhere(whereClause, `m.timestamp >= (SELECT min(timestamp) FROM mastra_metric_events_delta)`)}
|
|
5635
|
+
)
|
|
5512
5636
|
`, params))[0]?.cursorId ?? null;
|
|
5513
5637
|
if (cursorId) return cursorId;
|
|
5514
|
-
return
|
|
5638
|
+
return getStreamHeadCursor$3(client);
|
|
5515
5639
|
}
|
|
5516
5640
|
async function getStreamHeadCursor$3(client) {
|
|
5517
5641
|
return (await queryJson$1(client, `SELECT toString(max(cursorId)) AS cursorId FROM mastra_metric_events_delta`, {}))[0]?.cursorId ?? "0";
|
|
@@ -5710,29 +5834,26 @@ async function getMetricPercentiles(client, args) {
|
|
|
5710
5834
|
params: { percentileName: args.name }
|
|
5711
5835
|
}, signalFilter);
|
|
5712
5836
|
const whereClause = toWhereClause$1(combined);
|
|
5713
|
-
const
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
});
|
|
5731
|
-
}
|
|
5732
|
-
return { series };
|
|
5837
|
+
for (const p of args.percentiles) if (!Number.isFinite(p) || p < 0 || p > 1) throw new Error(`Percentile value must be a finite number between 0 and 1, got ${p}`);
|
|
5838
|
+
if (args.percentiles.length === 0) return { series: [] };
|
|
5839
|
+
const rows = await queryJson$1(client, `
|
|
5840
|
+
SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
|
|
5841
|
+
quantiles(${args.percentiles.join(", ")})(value) AS pvalues
|
|
5842
|
+
FROM ${TABLE_METRIC_EVENTS}
|
|
5843
|
+
${whereClause}
|
|
5844
|
+
GROUP BY bucket
|
|
5845
|
+
ORDER BY bucket
|
|
5846
|
+
`, combined.params);
|
|
5847
|
+
return { series: args.percentiles.map((percentile, index) => ({
|
|
5848
|
+
percentile,
|
|
5849
|
+
points: rows.map((row) => ({
|
|
5850
|
+
timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
|
|
5851
|
+
value: Number(row.pvalues?.[index] ?? 0)
|
|
5852
|
+
}))
|
|
5853
|
+
})) };
|
|
5733
5854
|
}
|
|
5734
5855
|
async function getMetricNames(client, args) {
|
|
5735
|
-
const conditions = [`kind = 'metricName'`];
|
|
5856
|
+
const conditions = [`kind = 'metricName'`, `key1 = ''`];
|
|
5736
5857
|
const params = {};
|
|
5737
5858
|
if (args.prefix) {
|
|
5738
5859
|
conditions.push(`value LIKE {namePrefix:String}`);
|
|
@@ -6193,6 +6314,22 @@ function currentScoresRelation(sourcePredicate) {
|
|
|
6193
6314
|
${sourcePredicate ? `WHERE ${sourcePredicate}` : ""}
|
|
6194
6315
|
)`;
|
|
6195
6316
|
}
|
|
6317
|
+
/**
|
|
6318
|
+
* Current-state scores narrowed before FINAL merges. `score_events_current` is
|
|
6319
|
+
* keyed by scoreId, so a traceId filter only applies after FINAL has merged
|
|
6320
|
+
* the whole table. When the (unaliased) filter pins traceId, the matching
|
|
6321
|
+
* scoreIds are first read from the history table, whose sort key starts with
|
|
6322
|
+
* traceId. Every current row was written to history, so the candidate set is a
|
|
6323
|
+
* superset; callers still apply the same filter to the current version.
|
|
6324
|
+
*
|
|
6325
|
+
* Time ranges alone are not narrowed this way: scoreIds are hash-distributed,
|
|
6326
|
+
* so the ids from any real time window touch nearly every granule of the
|
|
6327
|
+
* current table and the extra history scan costs more than it saves.
|
|
6328
|
+
*/
|
|
6329
|
+
function prunedCurrentScoresRelation(historyFilter) {
|
|
6330
|
+
if (!historyFilter.conditions.some((condition) => condition.startsWith("traceId "))) return currentScoresRelation();
|
|
6331
|
+
return currentScoresRelation(`scoreId IN (SELECT scoreId FROM ${TABLE_SCORE_EVENTS} WHERE ${historyFilter.conditions.join(" AND ")})`);
|
|
6332
|
+
}
|
|
6196
6333
|
function buildScoreIdentityFilter(args) {
|
|
6197
6334
|
const conditions = ["scorerId = {olapScorerId:String}"];
|
|
6198
6335
|
const params = { olapScorerId: args.scorerId };
|
|
@@ -6337,7 +6474,7 @@ async function listScores(client, args, strategy) {
|
|
|
6337
6474
|
};
|
|
6338
6475
|
}
|
|
6339
6476
|
const currentDeltaCursor = deltaCursorEnabled ? await getDeltaCursor$2(client, whereClause, filter.params) : void 0;
|
|
6340
|
-
const scoresRelation =
|
|
6477
|
+
const scoresRelation = prunedCurrentScoresRelation(buildScoresFilterConditions(parsed.filters));
|
|
6341
6478
|
const countResult = await queryJson(client, `SELECT count() AS total FROM ${scoresRelation} AS s ${whereClause}`, filter.params);
|
|
6342
6479
|
const rows = await queryJson(client, `SELECT * FROM ${scoresRelation} AS s ${whereClause} ORDER BY ${orderBy} LIMIT {limit:UInt32} OFFSET {offset:UInt32}`, {
|
|
6343
6480
|
...filter.params,
|
|
@@ -6375,15 +6512,19 @@ async function queryScoresAfterCursor(client, whereClause, params, limit, cursor
|
|
|
6375
6512
|
s.timestamp AS timestamp,
|
|
6376
6513
|
s.scoreId AS scoreId,
|
|
6377
6514
|
toString(d.cursorId) AS cursorId
|
|
6378
|
-
FROM ${
|
|
6379
|
-
INNER JOIN
|
|
6515
|
+
FROM ${TABLE_SCORE_EVENTS_CURRENT} s FINAL
|
|
6516
|
+
INNER JOIN (
|
|
6517
|
+
SELECT cursorId, scoreId
|
|
6518
|
+
FROM ${TABLE_SCORE_EVENTS_DELTA}
|
|
6519
|
+
WHERE cursorId > {afterCursor:UInt64}
|
|
6520
|
+
AND scoreId NOT IN (
|
|
6521
|
+
SELECT scoreId FROM ${TABLE_SCORE_EVENTS_DELTA}
|
|
6522
|
+
WHERE cursorId <= {afterCursor:UInt64}
|
|
6523
|
+
AND scoreId IN (SELECT scoreId FROM ${TABLE_SCORE_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64})
|
|
6524
|
+
)
|
|
6525
|
+
) d
|
|
6380
6526
|
ON s.scoreId = d.scoreId
|
|
6381
|
-
${whereClause
|
|
6382
|
-
AND d.scoreId NOT IN (
|
|
6383
|
-
SELECT scoreId FROM ${TABLE_SCORE_EVENTS_DELTA}
|
|
6384
|
-
WHERE cursorId <= {afterCursor:UInt64}
|
|
6385
|
-
AND scoreId IN (SELECT scoreId FROM ${TABLE_SCORE_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64})
|
|
6386
|
-
)
|
|
6527
|
+
${appendWhere(whereClause, `s.scoreId IN (SELECT scoreId FROM ${TABLE_SCORE_EVENTS_DELTA} WHERE cursorId > {afterCursor:UInt64})`)}
|
|
6387
6528
|
ORDER BY d.cursorId ASC
|
|
6388
6529
|
LIMIT 1 BY s.scoreId
|
|
6389
6530
|
LIMIT {fetchLimit:UInt32}
|
|
@@ -6393,16 +6534,24 @@ async function queryScoresAfterCursor(client, whereClause, params, limit, cursor
|
|
|
6393
6534
|
fetchLimit: limit + 1
|
|
6394
6535
|
});
|
|
6395
6536
|
}
|
|
6537
|
+
/**
|
|
6538
|
+
* Newest delta cursor whose current score matches the filters. Without
|
|
6539
|
+
* filters this is the stream head; with filters, FINAL only merges the
|
|
6540
|
+
* scoreIds still in the delta table.
|
|
6541
|
+
*/
|
|
6396
6542
|
async function getDeltaCursor$2(client, whereClause, params) {
|
|
6543
|
+
if (!whereClause) return getStreamHeadCursor$2(client);
|
|
6397
6544
|
const cursorId = (await queryJson(client, `
|
|
6398
6545
|
SELECT toString(max(d.cursorId)) AS cursorId
|
|
6399
6546
|
FROM mastra_score_events_delta d
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6547
|
+
WHERE d.scoreId IN (
|
|
6548
|
+
SELECT s.scoreId
|
|
6549
|
+
FROM mastra_score_events_current s FINAL
|
|
6550
|
+
${appendWhere(whereClause, `s.scoreId IN (SELECT scoreId FROM mastra_score_events_delta)`)}
|
|
6551
|
+
)
|
|
6403
6552
|
`, params))[0]?.cursorId ?? null;
|
|
6404
6553
|
if (cursorId) return cursorId;
|
|
6405
|
-
return (
|
|
6554
|
+
return getStreamHeadCursor$2(client);
|
|
6406
6555
|
}
|
|
6407
6556
|
async function getStreamHeadCursor$2(client) {
|
|
6408
6557
|
return (await queryJson(client, `SELECT toString(max(cursorId)) AS cursorId FROM mastra_score_events_delta`, {}))[0]?.cursorId ?? "0";
|
|
@@ -6419,7 +6568,7 @@ async function getScoreAggregate(client, args) {
|
|
|
6419
6568
|
const identity = buildScoreIdentityFilter(args);
|
|
6420
6569
|
const combined = mergeFilters(identity, buildScoresFilterConditions(args.filters));
|
|
6421
6570
|
const whereClause = toWhereClause(combined);
|
|
6422
|
-
const result = await queryJson(client, `SELECT ${aggSql} AS value FROM ${
|
|
6571
|
+
const result = await queryJson(client, `SELECT ${aggSql} AS value FROM ${prunedCurrentScoresRelation(combined)} ${whereClause}`, combined.params);
|
|
6423
6572
|
const value = result[0]?.value == null ? null : Number(result[0]?.value);
|
|
6424
6573
|
if (args.comparePeriod && args.filters?.timestamp) {
|
|
6425
6574
|
const ts = args.filters.timestamp;
|
|
@@ -6454,7 +6603,7 @@ async function getScoreAggregate(client, args) {
|
|
|
6454
6603
|
}
|
|
6455
6604
|
}));
|
|
6456
6605
|
const prevWhereClause = toWhereClause(prevCombined);
|
|
6457
|
-
const prevResult = await queryJson(client, `SELECT ${aggSql} AS value FROM ${
|
|
6606
|
+
const prevResult = await queryJson(client, `SELECT ${aggSql} AS value FROM ${prunedCurrentScoresRelation(prevCombined)} ${prevWhereClause}`, prevCombined.params);
|
|
6458
6607
|
const previousValue = prevResult[0]?.value == null ? null : Number(prevResult[0]?.value);
|
|
6459
6608
|
let changePercent = null;
|
|
6460
6609
|
if (previousValue !== null && previousValue !== 0 && value !== null) changePercent = (value - previousValue) / Math.abs(previousValue) * 100;
|
|
@@ -6472,7 +6621,7 @@ async function getScoreBreakdown(client, args) {
|
|
|
6472
6621
|
const combined = mergeFilters(buildScoreIdentityFilter(args), buildScoresFilterConditions(args.filters));
|
|
6473
6622
|
const whereClause = toWhereClause(combined);
|
|
6474
6623
|
const resolved = resolveScoreGroupBy(args.groupBy);
|
|
6475
|
-
return { groups: (await queryJson(client, `SELECT ${resolved.map((e) => e.selectSql).join(", ")}, ${aggSql} AS value FROM ${
|
|
6624
|
+
return { groups: (await queryJson(client, `SELECT ${resolved.map((e) => e.selectSql).join(", ")}, ${aggSql} AS value FROM ${prunedCurrentScoresRelation(combined)} ${whereClause} GROUP BY ${resolved.map((e) => e.groupSql).join(", ")} ORDER BY value DESC`, combined.params)).map((row) => ({
|
|
6476
6625
|
dimensions: Object.fromEntries(resolved.map((entry, index) => {
|
|
6477
6626
|
const v = row[`group_by_${index}`];
|
|
6478
6627
|
return [entry.key, v == null ? null : String(v)];
|
|
@@ -6491,7 +6640,7 @@ async function getScoreTimeSeries(client, args) {
|
|
|
6491
6640
|
SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
|
|
6492
6641
|
${resolved.map((e) => e.selectSql).join(", ")},
|
|
6493
6642
|
${aggSql} AS value
|
|
6494
|
-
FROM ${
|
|
6643
|
+
FROM ${prunedCurrentScoresRelation(combined)} ${whereClause}
|
|
6495
6644
|
GROUP BY bucket, ${resolved.map((e) => e.groupSql).join(", ")}
|
|
6496
6645
|
ORDER BY bucket
|
|
6497
6646
|
`, combined.params);
|
|
@@ -6513,7 +6662,7 @@ async function getScoreTimeSeries(client, args) {
|
|
|
6513
6662
|
const rows = await queryJson(client, `
|
|
6514
6663
|
SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
|
|
6515
6664
|
${aggSql} AS value
|
|
6516
|
-
FROM ${
|
|
6665
|
+
FROM ${prunedCurrentScoresRelation(combined)} ${whereClause}
|
|
6517
6666
|
GROUP BY bucket
|
|
6518
6667
|
ORDER BY bucket
|
|
6519
6668
|
`, combined.params);
|
|
@@ -6530,26 +6679,22 @@ async function getScorePercentiles(client, args) {
|
|
|
6530
6679
|
const combined = mergeFilters(buildScoreIdentityFilter(args), buildScoresFilterConditions(args.filters));
|
|
6531
6680
|
const whereClause = toWhereClause(combined);
|
|
6532
6681
|
if (!Array.isArray(args.percentiles) || args.percentiles.length === 0) throw new Error("Percentiles must include at least one value between 0 and 1.");
|
|
6533
|
-
const
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
}))
|
|
6550
|
-
});
|
|
6551
|
-
}
|
|
6552
|
-
return { series };
|
|
6682
|
+
for (const p of args.percentiles) if (!Number.isFinite(p) || p < 0 || p > 1) throw new Error(`Percentile value must be a finite number between 0 and 1, got ${p}`);
|
|
6683
|
+
const rows = await queryJson(client, `
|
|
6684
|
+
SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
|
|
6685
|
+
quantiles(${args.percentiles.join(", ")})(score) AS pvalues
|
|
6686
|
+
FROM ${prunedCurrentScoresRelation(combined)}
|
|
6687
|
+
${whereClause}
|
|
6688
|
+
GROUP BY bucket
|
|
6689
|
+
ORDER BY bucket
|
|
6690
|
+
`, combined.params);
|
|
6691
|
+
return { series: args.percentiles.map((percentile, index) => ({
|
|
6692
|
+
percentile,
|
|
6693
|
+
points: rows.map((row) => ({
|
|
6694
|
+
timestamp: row.bucket instanceof Date ? row.bucket : new Date(String(row.bucket)),
|
|
6695
|
+
value: Number(row.pvalues?.[index] ?? 0)
|
|
6696
|
+
}))
|
|
6697
|
+
})) };
|
|
6553
6698
|
}
|
|
6554
6699
|
//#endregion
|
|
6555
6700
|
//#region src/storage/domains/observability/v-next/trace-query.ts
|
|
@@ -6793,6 +6938,22 @@ const TRACE_SELECT = `
|
|
|
6793
6938
|
r.entityType AS entityType,
|
|
6794
6939
|
r.environment AS environment,
|
|
6795
6940
|
${TRACE_STATUS_SQL} AS status`;
|
|
6941
|
+
/** Candidate columns carried through the page-mode window sort (no payload blobs). */
|
|
6942
|
+
const TRACE_PAGE_COLUMNS = [
|
|
6943
|
+
"traceId",
|
|
6944
|
+
"rootSpanId",
|
|
6945
|
+
"name",
|
|
6946
|
+
"entityId",
|
|
6947
|
+
"parentSpanId",
|
|
6948
|
+
"threadId",
|
|
6949
|
+
"resourceId",
|
|
6950
|
+
"startedAt",
|
|
6951
|
+
"endedAt",
|
|
6952
|
+
"entityName",
|
|
6953
|
+
"entityType",
|
|
6954
|
+
"environment",
|
|
6955
|
+
"status"
|
|
6956
|
+
];
|
|
6796
6957
|
var ParameterBuilder = class {
|
|
6797
6958
|
params = {};
|
|
6798
6959
|
#next = 1;
|
|
@@ -6879,13 +7040,12 @@ function collectThreadRelationCollections(predicate, collections) {
|
|
|
6879
7040
|
}
|
|
6880
7041
|
function compilePredicate(predicate, parameters) {
|
|
6881
7042
|
if (predicate.type === "relation") {
|
|
6882
|
-
const
|
|
6883
|
-
SELECT
|
|
7043
|
+
const matching = `(
|
|
7044
|
+
SELECT s.traceId FROM ${predicate.collection === "spans" ? "current_spans" : predicate.collection === "scores" ? "current_scores" : "current_feedback"} s
|
|
6884
7045
|
WHERE isNotNull(s.traceId)
|
|
6885
|
-
AND s.traceId = r.traceId
|
|
6886
7046
|
AND (${predicate.collection === "feedback" ? compileFeedbackScalarPredicate(predicate.predicate, parameters) : compileScalarPredicate(predicate.predicate, predicate.collection === "spans" ? SPAN_FIELDS : SCORE_FIELDS, parameters)})
|
|
6887
7047
|
)`;
|
|
6888
|
-
return predicate.quantifier === "some" ?
|
|
7048
|
+
return `r.traceId ${predicate.quantifier === "some" ? "IN" : "NOT IN"} ${matching}`;
|
|
6889
7049
|
}
|
|
6890
7050
|
if (predicate.type === "boolean") return predicate.args.map((arg) => `(${compilePredicate(arg, parameters)})`).join(predicate.operator === "and" ? " AND " : " OR ");
|
|
6891
7051
|
if (predicate.type === "not") return `NOT (${compilePredicate(predicate.arg, parameters)})`;
|
|
@@ -6893,12 +7053,12 @@ function compilePredicate(predicate, parameters) {
|
|
|
6893
7053
|
}
|
|
6894
7054
|
function compileThreadPredicate(predicate, parameters) {
|
|
6895
7055
|
if (predicate.type === "relation") {
|
|
6896
|
-
const
|
|
6897
|
-
SELECT
|
|
6898
|
-
WHERE r.threadId
|
|
7056
|
+
const matching = `(
|
|
7057
|
+
SELECT r.threadId FROM eligible_roots r
|
|
7058
|
+
WHERE isNotNull(r.threadId)
|
|
6899
7059
|
AND (${compilePredicate(predicate.predicate, parameters)})
|
|
6900
7060
|
)`;
|
|
6901
|
-
return predicate.quantifier === "some" ?
|
|
7061
|
+
return `t.threadId ${predicate.quantifier === "some" ? "IN" : "NOT IN"} ${matching}`;
|
|
6902
7062
|
}
|
|
6903
7063
|
if (predicate.type === "boolean") return predicate.args.map((arg) => `(${compileThreadPredicate(arg, parameters)})`).join(predicate.operator === "and" ? " AND " : " OR ");
|
|
6904
7064
|
return `NOT (${compileThreadPredicate(predicate.arg, parameters)})`;
|
|
@@ -6999,6 +7159,15 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
|
|
|
6999
7159
|
FROM (
|
|
7000
7160
|
SELECT *
|
|
7001
7161
|
FROM ${TABLE_FEEDBACK_EVENTS} FINAL
|
|
7162
|
+
-- ClickHouse cannot push the scope filter through LIMIT 1 BY, so keep
|
|
7163
|
+
-- every version of each feedbackId that ever pointed into the scope
|
|
7164
|
+
-- (traceId leads the sort key). A rewrite may move a feedback to another
|
|
7165
|
+
-- trace, so the current version is still filtered below.
|
|
7166
|
+
WHERE feedbackId IN (
|
|
7167
|
+
SELECT feedbackId
|
|
7168
|
+
FROM ${TABLE_FEEDBACK_EVENTS}
|
|
7169
|
+
WHERE traceId IN (SELECT traceId FROM root_scope)
|
|
7170
|
+
)
|
|
7002
7171
|
ORDER BY feedbackId, writeVersion DESC, timestamp DESC
|
|
7003
7172
|
LIMIT 1 BY feedbackId
|
|
7004
7173
|
) AS current
|
|
@@ -7053,11 +7222,12 @@ LIMIT ${parameters.add(plan.limit + 1, "UInt64")}`,
|
|
|
7053
7222
|
cursorId: "0",
|
|
7054
7223
|
traceId: ""
|
|
7055
7224
|
};
|
|
7225
|
+
const lowerCursor = parameters.add(after.cursorId, "UInt64");
|
|
7056
7226
|
return {
|
|
7057
7227
|
query: `${candidates}, delta_candidates AS (
|
|
7058
7228
|
SELECT traceId, max(cursorId) AS latestCursorId
|
|
7059
7229
|
FROM ${TABLE_TRACE_ROOTS_DELTA}
|
|
7060
|
-
WHERE tuple(cursorId, traceId) > ${`tuple(${
|
|
7230
|
+
WHERE cursorId >= ${lowerCursor} AND tuple(cursorId, traceId) > ${`tuple(${lowerCursor}, ${parameters.add(after.traceId, "String")})`} ${deltaHead ? `AND tuple(cursorId, traceId) <= tuple(${parameters.add(deltaHead.cursorId, "UInt64")}, ${parameters.add(deltaHead.traceId, "String")})` : ""}
|
|
7061
7231
|
GROUP BY traceId
|
|
7062
7232
|
)
|
|
7063
7233
|
SELECT c.*, toString(d.latestCursorId) AS __delta_cursor
|
|
@@ -7071,46 +7241,25 @@ LIMIT ${parameters.add(plan.limit + 1, "UInt64")}`,
|
|
|
7071
7241
|
}
|
|
7072
7242
|
const orderField = resolveOrderField(plan.orderBy.field);
|
|
7073
7243
|
const direction = plan.orderBy.direction === "asc" ? "ASC" : "DESC";
|
|
7074
|
-
if (plan.paginationMode === "page")
|
|
7075
|
-
|
|
7244
|
+
if (plan.paginationMode === "page") {
|
|
7245
|
+
const onPage = `__row_position > ${parameters.add(plan.page * plan.perPage, "UInt64")} AND __row_position <= ${parameters.add((plan.page + 1) * plan.perPage, "UInt64")}`;
|
|
7246
|
+
return {
|
|
7247
|
+
query: `${candidates},
|
|
7076
7248
|
page_rows AS (
|
|
7077
|
-
SELECT
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
),
|
|
7082
|
-
page_total AS (
|
|
7083
|
-
SELECT count() AS total
|
|
7249
|
+
SELECT
|
|
7250
|
+
${TRACE_PAGE_COLUMNS.join(",\n ")},
|
|
7251
|
+
row_number() OVER (ORDER BY ${orderField} ${direction}, traceId ASC) AS __row_position,
|
|
7252
|
+
count() OVER () AS total
|
|
7084
7253
|
FROM candidates
|
|
7085
7254
|
)
|
|
7086
|
-
SELECT
|
|
7255
|
+
SELECT *, if(${onPage}, 0, 1) AS __metadata
|
|
7087
7256
|
FROM page_rows
|
|
7088
|
-
|
|
7089
|
-
UNION ALL
|
|
7090
|
-
SELECT
|
|
7091
|
-
'' AS traceId,
|
|
7092
|
-
'' AS rootSpanId,
|
|
7093
|
-
'' AS name,
|
|
7094
|
-
CAST(NULL, 'Nullable(String)') AS entityId,
|
|
7095
|
-
CAST(NULL, 'Nullable(String)') AS parentSpanId,
|
|
7096
|
-
CAST(NULL, 'Nullable(String)') AS metadata,
|
|
7097
|
-
CAST(NULL, 'Nullable(String)') AS input,
|
|
7098
|
-
CAST(NULL, 'Nullable(String)') AS threadId,
|
|
7099
|
-
CAST(NULL, 'Nullable(String)') AS resourceId,
|
|
7100
|
-
toDateTime64(0, 3, 'UTC') AS startedAt,
|
|
7101
|
-
toDateTime64(0, 3, 'UTC') AS endedAt,
|
|
7102
|
-
CAST(NULL, 'Nullable(String)') AS entityName,
|
|
7103
|
-
CAST(NULL, 'Nullable(String)') AS entityType,
|
|
7104
|
-
CAST(NULL, 'Nullable(String)') AS environment,
|
|
7105
|
-
'' AS status,
|
|
7106
|
-
0 AS __row_position,
|
|
7107
|
-
page_total.total AS total,
|
|
7108
|
-
1 AS __metadata
|
|
7109
|
-
FROM page_total
|
|
7257
|
+
WHERE (${onPage}) OR __row_position = 1
|
|
7110
7258
|
ORDER BY __metadata ASC, __row_position ASC`,
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7259
|
+
query_params: parameters.params,
|
|
7260
|
+
sharedSnapshot: true
|
|
7261
|
+
};
|
|
7262
|
+
}
|
|
7114
7263
|
let pageCondition = "";
|
|
7115
7264
|
if (plan.cursor) {
|
|
7116
7265
|
const comparison = plan.orderBy.direction === "asc" ? ">" : "<";
|
|
@@ -7128,6 +7277,23 @@ LIMIT ${limit}`,
|
|
|
7128
7277
|
query_params: parameters.params
|
|
7129
7278
|
};
|
|
7130
7279
|
}
|
|
7280
|
+
/**
|
|
7281
|
+
* Fetches the metadata/input payloads for page-mode rows. Looks rows up by the
|
|
7282
|
+
* trace_roots sort-key prefix `(startedAt, traceId)`, so only the page's
|
|
7283
|
+
* granules are read. A root can have unmerged versions in different `endedAt`
|
|
7284
|
+
* partitions, so `endedAt` is part of the key: the payload comes from the same
|
|
7285
|
+
* version as the candidate row.
|
|
7286
|
+
*/
|
|
7287
|
+
function compileClickHouseTraceRootPayloads(keys) {
|
|
7288
|
+
const parameters = new ParameterBuilder();
|
|
7289
|
+
return {
|
|
7290
|
+
query: `SELECT traceId, spanId AS rootSpanId, metadataRaw AS metadata, input
|
|
7291
|
+
FROM ${TABLE_TRACE_ROOTS}
|
|
7292
|
+
WHERE (startedAt, traceId, spanId, endedAt) IN (${keys.map((key) => `(${parameters.add(key.startedAt, "DateTime64(3, 'UTC')")}, ${parameters.add(key.traceId, "String")}, ${parameters.add(key.rootSpanId, "String")}, ${parameters.add(key.endedAt, "DateTime64(3, 'UTC')")})`).join(", ")})
|
|
7293
|
+
LIMIT 1 BY traceId, spanId`,
|
|
7294
|
+
query_params: parameters.params
|
|
7295
|
+
};
|
|
7296
|
+
}
|
|
7131
7297
|
function compileClickHouseThreadQuery(plan) {
|
|
7132
7298
|
const parameters = new ParameterBuilder();
|
|
7133
7299
|
const relationCollections = collectRelationCollections(plan.traces.where);
|
|
@@ -7321,7 +7487,21 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
|
|
|
7321
7487
|
if (plan.paginationMode === "page") {
|
|
7322
7488
|
const rows = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs: deltaHead ? remaining() : timeoutMs }, compileClickHouseTraceQuery(plan, deltaHead));
|
|
7323
7489
|
const total = Number(rows.at(-1)?.total ?? 0);
|
|
7324
|
-
const
|
|
7490
|
+
const pageRows = rows.filter((row) => Number(row.__metadata) === 0);
|
|
7491
|
+
const payloads = /* @__PURE__ */ new Map();
|
|
7492
|
+
if (pageRows.length > 0) {
|
|
7493
|
+
const payloadRows = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs: remaining() }, compileClickHouseTraceRootPayloads(pageRows.map((row) => ({
|
|
7494
|
+
traceId: String(row.traceId),
|
|
7495
|
+
rootSpanId: String(row.rootSpanId),
|
|
7496
|
+
startedAt: asIsoTimestamp(row.startedAt),
|
|
7497
|
+
endedAt: asIsoTimestamp(row.endedAt)
|
|
7498
|
+
}))));
|
|
7499
|
+
for (const payload of payloadRows) payloads.set(`${payload.traceId}\u0000${payload.rootSpanId}`, payload);
|
|
7500
|
+
}
|
|
7501
|
+
const traces = pageRows.map((row) => ({
|
|
7502
|
+
...row,
|
|
7503
|
+
...payloads.get(`${row.traceId}\u0000${row.rootSpanId}`)
|
|
7504
|
+
})).map((row) => ({
|
|
7325
7505
|
traceId: String(row.traceId),
|
|
7326
7506
|
rootSpanId: String(row.rootSpanId),
|
|
7327
7507
|
name: row.name,
|
|
@@ -7420,6 +7600,201 @@ async function queryThreads(client, plan, timeoutMs) {
|
|
|
7420
7600
|
}) : null }
|
|
7421
7601
|
});
|
|
7422
7602
|
}
|
|
7603
|
+
/** Compile a span-row filter with exactly the same rules as trace span predicates. */
|
|
7604
|
+
function compileSpanQueryPredicate(predicate) {
|
|
7605
|
+
const parameters = new ParameterBuilder();
|
|
7606
|
+
return {
|
|
7607
|
+
sql: compileScalarPredicate(predicate, SPAN_FIELDS, parameters),
|
|
7608
|
+
params: parameters.params
|
|
7609
|
+
};
|
|
7610
|
+
}
|
|
7611
|
+
//#endregion
|
|
7612
|
+
//#region src/storage/domains/observability/v-next/span-query.ts
|
|
7613
|
+
const identityFields = [
|
|
7614
|
+
"organizationId",
|
|
7615
|
+
"resourceId",
|
|
7616
|
+
"traceId",
|
|
7617
|
+
"spanId"
|
|
7618
|
+
];
|
|
7619
|
+
const identities = identityFields.join(", ");
|
|
7620
|
+
const identitySetColumns = "isNull(organizationId), ifNull(organizationId, ''), isNull(resourceId), ifNull(resourceId, ''), traceId, spanId";
|
|
7621
|
+
const identitySetKey = `tuple(${identitySetColumns})`;
|
|
7622
|
+
const displayFields = [
|
|
7623
|
+
"parentSpanId",
|
|
7624
|
+
"name",
|
|
7625
|
+
"spanType",
|
|
7626
|
+
"entityType",
|
|
7627
|
+
"entityId",
|
|
7628
|
+
"entityName",
|
|
7629
|
+
"startedAt",
|
|
7630
|
+
"endedAt",
|
|
7631
|
+
"model",
|
|
7632
|
+
"provider",
|
|
7633
|
+
"durationMs",
|
|
7634
|
+
"status"
|
|
7635
|
+
];
|
|
7636
|
+
var Parameters = class {
|
|
7637
|
+
values = {};
|
|
7638
|
+
add(value, timestamp = false) {
|
|
7639
|
+
const name = `span_${Object.keys(this.values).length}`;
|
|
7640
|
+
this.values[name] = timestamp ? new Date(value).toISOString().replace("T", " ").replace(/Z$/, "") : value;
|
|
7641
|
+
return `{${name}:${timestamp ? "DateTime64(3, 'UTC')" : typeof value === "number" ? "UInt64" : "String"}}`;
|
|
7642
|
+
}
|
|
7643
|
+
};
|
|
7644
|
+
function identityValues(row) {
|
|
7645
|
+
return [
|
|
7646
|
+
row.organizationId === null ? 1 : 0,
|
|
7647
|
+
row.organizationId ?? "",
|
|
7648
|
+
row.resourceId === null ? 1 : 0,
|
|
7649
|
+
row.resourceId ?? "",
|
|
7650
|
+
row.traceId,
|
|
7651
|
+
row.spanId
|
|
7652
|
+
];
|
|
7653
|
+
}
|
|
7654
|
+
function afterCursor(plan, parameters) {
|
|
7655
|
+
if (!plan.cursor) return "1";
|
|
7656
|
+
const cursor = plan.cursor;
|
|
7657
|
+
const timestamp = parameters.add(cursor.sortValue, true);
|
|
7658
|
+
const field = `s.${plan.orderBy.field}`;
|
|
7659
|
+
const terms = [`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${timestamp}`];
|
|
7660
|
+
const equal = [`${field} = ${timestamp}`];
|
|
7661
|
+
for (const key of identityFields) {
|
|
7662
|
+
const value = cursor[key];
|
|
7663
|
+
if (value === null) equal.push(`isNull(s.${key})`);
|
|
7664
|
+
else {
|
|
7665
|
+
const parameter = parameters.add(value);
|
|
7666
|
+
terms.push(`(${equal.join(" AND ")} AND (s.${key} > ${parameter} OR isNull(s.${key})))`);
|
|
7667
|
+
equal.push(`s.${key} = ${parameter}`);
|
|
7668
|
+
}
|
|
7669
|
+
}
|
|
7670
|
+
return `(${terms.join(" OR ")})`;
|
|
7671
|
+
}
|
|
7672
|
+
function predicateFields(predicate, fields = /* @__PURE__ */ new Set()) {
|
|
7673
|
+
if (!predicate) return fields;
|
|
7674
|
+
if (predicate.type === "boolean") for (const arg of predicate.args) predicateFields(arg, fields);
|
|
7675
|
+
else if (predicate.type === "not") predicateFields(predicate.arg, fields);
|
|
7676
|
+
else fields.add(predicate.field);
|
|
7677
|
+
return fields;
|
|
7678
|
+
}
|
|
7679
|
+
function fieldExpression(field) {
|
|
7680
|
+
if (field === "model" || field === "provider") return `if(JSONType(e.attributes, '${field}') = 'String', JSONExtractString(e.attributes, '${field}'), NULL)`;
|
|
7681
|
+
if (field === "durationMs") return "dateDiff('millisecond', e.startedAt, e.endedAt)";
|
|
7682
|
+
if (field === "status") return "if(isNull(e.error), 'success', 'error')";
|
|
7683
|
+
if (field === "error") return "if(isNull(e.error), NULL, 'error')";
|
|
7684
|
+
return `e.${field}`;
|
|
7685
|
+
}
|
|
7686
|
+
function compileClickHouseSpanQuery(plan) {
|
|
7687
|
+
const parameters = new Parameters();
|
|
7688
|
+
const from = parameters.add(plan.timeRange.from, true);
|
|
7689
|
+
const to = parameters.add(plan.timeRange.to, true);
|
|
7690
|
+
const scope = [];
|
|
7691
|
+
if (plan.scope) {
|
|
7692
|
+
scope.push(`organizationId = ${parameters.add(plan.scope.organizationId)}`);
|
|
7693
|
+
if (plan.scope.resourceId !== void 0) scope.push(`resourceId = ${parameters.add(plan.scope.resourceId)}`);
|
|
7694
|
+
}
|
|
7695
|
+
const predicate = plan.where ? compileSpanQueryPredicate(plan.where) : {
|
|
7696
|
+
sql: "1",
|
|
7697
|
+
params: {}
|
|
7698
|
+
};
|
|
7699
|
+
const selectionFields = [.../* @__PURE__ */ new Set([
|
|
7700
|
+
"startedAt",
|
|
7701
|
+
"endedAt",
|
|
7702
|
+
...predicateFields(plan.where)
|
|
7703
|
+
])].filter((field) => !new Set(identityFields).has(field));
|
|
7704
|
+
const expressions = selectionFields.map(fieldExpression);
|
|
7705
|
+
const after = afterCursor(plan, parameters);
|
|
7706
|
+
return {
|
|
7707
|
+
query: `WITH candidate_ids AS (
|
|
7708
|
+
SELECT ${identitySetKey} AS identity FROM ${TABLE_SPAN_EVENTS}
|
|
7709
|
+
PREWHERE endedAt >= ${from}
|
|
7710
|
+
WHERE startedAt >= ${from} AND startedAt < ${to} ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
|
|
7711
|
+
), candidates AS (
|
|
7712
|
+
SELECT ${identities}, ${selectionFields.map((field, index) => `${expressions[index]} AS ${field}`).join(", ")}
|
|
7713
|
+
FROM ${TABLE_SPAN_EVENTS} e
|
|
7714
|
+
PREWHERE e.endedAt >= ${from}
|
|
7715
|
+
WHERE ${identitySetKey} IN (SELECT identity FROM candidate_ids) ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
|
|
7716
|
+
ORDER BY ${identities}, e.endedAt DESC
|
|
7717
|
+
LIMIT 1 BY ${identities}
|
|
7718
|
+
)
|
|
7719
|
+
SELECT ${[
|
|
7720
|
+
...identityFields,
|
|
7721
|
+
"startedAt",
|
|
7722
|
+
"endedAt"
|
|
7723
|
+
].map((field) => `s.${field}`).join(", ")}
|
|
7724
|
+
FROM candidates s WHERE s.startedAt >= ${from} AND s.startedAt < ${to} AND (${predicate.sql}) AND ${after}
|
|
7725
|
+
ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"}, ${identityFields.map((field) => `s.${field} ASC NULLS LAST`).join(", ")}
|
|
7726
|
+
LIMIT ${parameters.add(plan.limit + 1)}`,
|
|
7727
|
+
query_params: {
|
|
7728
|
+
...parameters.values,
|
|
7729
|
+
...predicate.params
|
|
7730
|
+
}
|
|
7731
|
+
};
|
|
7732
|
+
}
|
|
7733
|
+
function iso(value) {
|
|
7734
|
+
return new Date(value.includes("T") ? value : `${value.replace(" ", "T")}Z`).toISOString();
|
|
7735
|
+
}
|
|
7736
|
+
async function querySpans(client, plan, timeoutMs) {
|
|
7737
|
+
const deadline = performance.now() + coreStorage.resolveTraceQueryTimeoutMs(timeoutMs);
|
|
7738
|
+
const run = async (compiled) => {
|
|
7739
|
+
const remaining = Math.floor(deadline - performance.now());
|
|
7740
|
+
if (remaining <= 0) throw new coreStorage.TraceQueryExecutionError();
|
|
7741
|
+
return await runWithClickHouseTraceQueryTimeout(client, {
|
|
7742
|
+
timeoutMs: remaining,
|
|
7743
|
+
memoryLimitBytes: 512 * 1024 * 1024
|
|
7744
|
+
}, compiled);
|
|
7745
|
+
};
|
|
7746
|
+
const selected = (await run(compileClickHouseSpanQuery(plan))).map((row) => ({
|
|
7747
|
+
...row,
|
|
7748
|
+
startedAt: iso(row.startedAt),
|
|
7749
|
+
endedAt: iso(row.endedAt)
|
|
7750
|
+
}));
|
|
7751
|
+
const page = selected.slice(0, plan.limit);
|
|
7752
|
+
if (!page.length) return {
|
|
7753
|
+
spans: [],
|
|
7754
|
+
page: { next: null }
|
|
7755
|
+
};
|
|
7756
|
+
const payloadWhere = `tuple(${identitySetColumns}, endedAt) IN {page:Array(Tuple(UInt8, String, UInt8, String, String, String, DateTime64(3, 'UTC')))}`;
|
|
7757
|
+
const payloads = await run({
|
|
7758
|
+
query: `SELECT ${identities}, ${displayFields.map((field, index) => `data.${index + 1} AS ${field}`).join(", ")},
|
|
7759
|
+
data.${displayFields.length + 1} AS inputPreview, data.${displayFields.length + 2} AS outputPreview
|
|
7760
|
+
FROM (SELECT ${identities}, any(tuple(${displayFields.map(fieldExpression).join(", ")}, substringUTF8(input, 1, ${coreStorage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}), substringUTF8(output, 1, ${coreStorage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}))) AS data
|
|
7761
|
+
FROM ${TABLE_SPAN_EVENTS} e WHERE ${payloadWhere} GROUP BY ${identities})`,
|
|
7762
|
+
query_params: { page: page.map((row) => new TupleParam([...identityValues(row), row.endedAt.replace("T", " ").replace(/Z$/, "")])) }
|
|
7763
|
+
});
|
|
7764
|
+
const metricWhere = `${identitySetKey} IN {page:Array(Tuple(UInt8, String, UInt8, String, String, String))}`;
|
|
7765
|
+
const metrics = await run({
|
|
7766
|
+
query: `SELECT ${identities}, data.1 AS name, data.2 AS estimatedCost,
|
|
7767
|
+
data.3 AS costUnit, data.4 AS allocation, data.5 AS costError
|
|
7768
|
+
FROM (SELECT ${identities}, metricId, argMax(tuple(name, estimatedCost, costUnit,
|
|
7769
|
+
JSONExtractString(costMetadata, 'allocation'), JSONHas(costMetadata, 'error')), timestamp) AS data
|
|
7770
|
+
FROM ${TABLE_METRIC_EVENTS} WHERE (${metricWhere}) AND name LIKE 'mastra_model_%'
|
|
7771
|
+
GROUP BY ${identities}, metricId)
|
|
7772
|
+
LIMIT ${coreStorage.SPAN_QUERY_MAX_COST_METRICS + 1}`,
|
|
7773
|
+
query_params: { page: page.map((row) => new TupleParam(identityValues(row))) }
|
|
7774
|
+
});
|
|
7775
|
+
if (metrics.length > coreStorage.SPAN_QUERY_MAX_COST_METRICS) throw new coreStorage.TraceQueryResourceLimitError();
|
|
7776
|
+
const byIdentity = new Map(payloads.map((row) => [coreStorage.spanQueryIdentityKey(row), row]));
|
|
7777
|
+
const hydrated = page.flatMap((identity) => {
|
|
7778
|
+
const row = byIdentity.get(coreStorage.spanQueryIdentityKey(identity));
|
|
7779
|
+
if (!row) return [];
|
|
7780
|
+
const { inputPreview: _input, outputPreview: _output, ...metadata } = row;
|
|
7781
|
+
return [{
|
|
7782
|
+
...metadata,
|
|
7783
|
+
...identity,
|
|
7784
|
+
durationMs: Number(row.durationMs)
|
|
7785
|
+
}];
|
|
7786
|
+
});
|
|
7787
|
+
const result = coreStorage.buildSpanQueryResponse(plan, hydrated, payloads, metrics);
|
|
7788
|
+
const last = page.at(-1);
|
|
7789
|
+
result.page.next = selected.length > plan.limit ? coreStorage.encodeSpanQueryCursor(plan, {
|
|
7790
|
+
organizationId: last.organizationId,
|
|
7791
|
+
resourceId: last.resourceId,
|
|
7792
|
+
traceId: last.traceId,
|
|
7793
|
+
spanId: last.spanId,
|
|
7794
|
+
sortValue: last[plan.orderBy.field]
|
|
7795
|
+
}) : null;
|
|
7796
|
+
return result;
|
|
7797
|
+
}
|
|
7423
7798
|
//#endregion
|
|
7424
7799
|
//#region src/storage/domains/observability/v-next/trace-aggregate.ts
|
|
7425
7800
|
/** Trace-root column expressions (alias `r`) for each groupable dimension; same values `where` sees. */
|
|
@@ -7669,18 +8044,16 @@ async function listTraceRows(client, args, strategy, projection, mapRows) {
|
|
|
7669
8044
|
const page = pagination?.page ?? 0;
|
|
7670
8045
|
const perPage = pagination?.perPage ?? 10;
|
|
7671
8046
|
const { conditions, params } = buildTraceFilterConditions(filters, "r");
|
|
7672
|
-
if (filters?.hasChildError != null)
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
AND
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
SELECT 1 FROM ${TABLE_SPAN_EVENTS} c
|
|
7680
|
-
WHERE c.traceId = r.traceId
|
|
7681
|
-
AND c.parentSpanId IS NOT NULL
|
|
8047
|
+
if (filters?.hasChildError != null) {
|
|
8048
|
+
const scope = buildTraceFilterConditions(filters, "scope_r");
|
|
8049
|
+
const traceScope = scope.conditions.length ? `c.traceId IN (SELECT scope_r.traceId FROM ${TABLE_TRACE_ROOTS} scope_r WHERE ${scope.conditions.join(" AND ")})
|
|
8050
|
+
AND ` : "";
|
|
8051
|
+
conditions.push(`r.traceId ${filters.hasChildError ? "IN" : "NOT IN"} (
|
|
8052
|
+
SELECT c.traceId FROM ${TABLE_SPAN_EVENTS} c
|
|
8053
|
+
WHERE ${traceScope}c.parentSpanId IS NOT NULL
|
|
7682
8054
|
AND c.error IS NOT NULL
|
|
7683
8055
|
)`);
|
|
8056
|
+
}
|
|
7684
8057
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
7685
8058
|
const deltaCursorEnabled = deltaPollingSupported(strategy);
|
|
7686
8059
|
if (mode === "delta") {
|
|
@@ -7736,15 +8109,27 @@ async function listTraceRows(client, args, strategy, projection, mapRows) {
|
|
|
7736
8109
|
const rows = await (await client.query({
|
|
7737
8110
|
query: `
|
|
7738
8111
|
SELECT ${projection.outerSelect} FROM (
|
|
8112
|
+
-- Deferred join: pick the page's sort keys from a narrow sort, then
|
|
8113
|
+
-- read full rows only for those keys. LIMIT 1 BY disables ClickHouse's
|
|
8114
|
+
-- own lazy materialization, so sorting SELECT * directly would carry
|
|
8115
|
+
-- every matching row's payload columns through the sort. The filter is
|
|
8116
|
+
-- applied again on the re-read: a dedupeKey can have unmerged versions
|
|
8117
|
+
-- (in different endedAt partitions) and only a matching one may win.
|
|
7739
8118
|
SELECT ${projection.innerSelect}
|
|
7740
8119
|
FROM ${TABLE_TRACE_ROOTS} r
|
|
7741
|
-
${whereClause
|
|
7742
|
-
|
|
8120
|
+
${appendWhere(whereClause, `(r.startedAt, r.traceId, r.dedupeKey) IN (
|
|
8121
|
+
SELECT startedAt, traceId, dedupeKey
|
|
8122
|
+
FROM ${TABLE_TRACE_ROOTS} r
|
|
8123
|
+
${whereClause}
|
|
8124
|
+
ORDER BY ${orderClause}, dedupeKey ASC
|
|
8125
|
+
LIMIT 1 BY dedupeKey
|
|
8126
|
+
LIMIT {limit:UInt32}
|
|
8127
|
+
OFFSET {offset:UInt32}
|
|
8128
|
+
)`)}
|
|
8129
|
+
ORDER BY ${orderClause}, dedupeKey ASC
|
|
7743
8130
|
LIMIT 1 BY dedupeKey
|
|
7744
8131
|
)
|
|
7745
|
-
ORDER BY ${orderClause}
|
|
7746
|
-
LIMIT {limit:UInt32}
|
|
7747
|
-
OFFSET {offset:UInt32}
|
|
8132
|
+
ORDER BY ${orderClause}, dedupeKey ASC
|
|
7748
8133
|
`,
|
|
7749
8134
|
query_params: {
|
|
7750
8135
|
...params,
|
|
@@ -7778,6 +8163,7 @@ async function listTracesLight(client, args, strategy) {
|
|
|
7778
8163
|
return listTraceRows(client, args, strategy, LIGHT_PROJECTION, (rows) => rows.map(rowToLightSpanRecord));
|
|
7779
8164
|
}
|
|
7780
8165
|
async function queryTracesAfterCursor(client, deltaSelect, whereClause, params, limit, cursorId) {
|
|
8166
|
+
const deltaKeys = `SELECT startedAt, traceId, dedupeKey FROM ${TABLE_TRACE_ROOTS_DELTA} WHERE cursorId > {afterCursor:UInt64}`;
|
|
7781
8167
|
return await (await client.query({
|
|
7782
8168
|
query: `
|
|
7783
8169
|
SELECT
|
|
@@ -7786,12 +8172,16 @@ async function queryTracesAfterCursor(client, deltaSelect, whereClause, params,
|
|
|
7786
8172
|
r.traceId AS traceId,
|
|
7787
8173
|
r.dedupeKey AS dedupeKey,
|
|
7788
8174
|
toString(d.cursorId) AS cursorId
|
|
7789
|
-
FROM ${
|
|
7790
|
-
INNER JOIN
|
|
8175
|
+
FROM ${TABLE_TRACE_ROOTS} r
|
|
8176
|
+
INNER JOIN (
|
|
8177
|
+
SELECT cursorId, startedAt, traceId, dedupeKey
|
|
8178
|
+
FROM ${TABLE_TRACE_ROOTS_DELTA}
|
|
8179
|
+
WHERE cursorId > {afterCursor:UInt64}
|
|
8180
|
+
) d
|
|
7791
8181
|
ON r.startedAt = d.startedAt
|
|
7792
8182
|
AND r.traceId = d.traceId
|
|
7793
8183
|
AND r.dedupeKey = d.dedupeKey
|
|
7794
|
-
${whereClause
|
|
8184
|
+
${appendWhere(whereClause, `(r.startedAt, r.traceId, r.dedupeKey) IN (${deltaKeys})`)}
|
|
7795
8185
|
ORDER BY d.cursorId ASC
|
|
7796
8186
|
LIMIT {fetchLimit:UInt32}
|
|
7797
8187
|
`,
|
|
@@ -7804,27 +8194,30 @@ async function queryTracesAfterCursor(client, deltaSelect, whereClause, params,
|
|
|
7804
8194
|
clickhouse_settings: CH_SETTINGS
|
|
7805
8195
|
})).json();
|
|
7806
8196
|
}
|
|
8197
|
+
/**
|
|
8198
|
+
* Newest delta cursor whose trace root matches the filters. Without filters
|
|
8199
|
+
* every delta row qualifies, so this is the stream head. With filters, the
|
|
8200
|
+
* root scan is bounded below by the oldest `startedAt` still in the delta
|
|
8201
|
+
* table (a 2-day TTL window) instead of reading all of trace_roots.
|
|
8202
|
+
*/
|
|
7807
8203
|
async function getDeltaCursor$1(client, whereClause, params) {
|
|
8204
|
+
if (!whereClause) return getStreamHeadCursor$1(client);
|
|
7808
8205
|
const cursorId = (await (await client.query({
|
|
7809
8206
|
query: `
|
|
7810
8207
|
SELECT toString(max(d.cursorId)) AS cursorId
|
|
7811
8208
|
FROM mastra_trace_roots_delta d
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
8209
|
+
WHERE (d.startedAt, d.traceId, d.dedupeKey) IN (
|
|
8210
|
+
SELECT r.startedAt, r.traceId, r.dedupeKey
|
|
8211
|
+
FROM mastra_trace_roots r
|
|
8212
|
+
${appendWhere(whereClause, `r.startedAt >= (SELECT min(startedAt) FROM mastra_trace_roots_delta)`)}
|
|
8213
|
+
)
|
|
7817
8214
|
`,
|
|
7818
8215
|
query_params: params,
|
|
7819
8216
|
format: "JSONEachRow",
|
|
7820
8217
|
clickhouse_settings: CH_SETTINGS
|
|
7821
8218
|
})).json())[0]?.cursorId ?? null;
|
|
7822
8219
|
if (cursorId) return cursorId;
|
|
7823
|
-
return (
|
|
7824
|
-
query: `SELECT toString(max(cursorId)) AS cursorId FROM mastra_trace_roots_delta`,
|
|
7825
|
-
format: "JSONEachRow",
|
|
7826
|
-
clickhouse_settings: CH_SETTINGS
|
|
7827
|
-
})).json())[0]?.cursorId ?? "0";
|
|
8220
|
+
return getStreamHeadCursor$1(client);
|
|
7828
8221
|
}
|
|
7829
8222
|
async function getStreamHeadCursor$1(client) {
|
|
7830
8223
|
return (await (await client.query({
|
|
@@ -8315,15 +8708,28 @@ async function listBranches(client, args, strategy) {
|
|
|
8315
8708
|
const spans = (await (await client.query({
|
|
8316
8709
|
query: `
|
|
8317
8710
|
SELECT * FROM (
|
|
8711
|
+
-- Deferred join: pick the page's sort keys from a narrow sort, then
|
|
8712
|
+
-- read full rows only for those keys. LIMIT 1 BY disables ClickHouse's
|
|
8713
|
+
-- own lazy materialization, so sorting SELECT * directly would carry
|
|
8714
|
+
-- every matching branch's payload columns through the sort. The filter
|
|
8715
|
+
-- is applied again on the re-read: a dedupeKey can have unmerged
|
|
8716
|
+
-- versions (in different endedAt partitions) and only a matching one
|
|
8717
|
+
-- may win.
|
|
8318
8718
|
SELECT *
|
|
8319
8719
|
FROM ${TABLE_TRACE_BRANCHES} b
|
|
8320
|
-
${whereClause
|
|
8321
|
-
|
|
8720
|
+
${appendWhere(whereClause, `(b.spanType, b.startedAt, b.traceId, b.dedupeKey) IN (
|
|
8721
|
+
SELECT b.spanType, b.startedAt, b.traceId, b.dedupeKey
|
|
8722
|
+
FROM ${TABLE_TRACE_BRANCHES} b
|
|
8723
|
+
${whereClause}
|
|
8724
|
+
ORDER BY b.${sortField} ${sortDirection}, b.dedupeKey ASC
|
|
8725
|
+
LIMIT 1 BY b.dedupeKey
|
|
8726
|
+
LIMIT {limit:UInt32}
|
|
8727
|
+
OFFSET {offset:UInt32}
|
|
8728
|
+
)`)}
|
|
8729
|
+
ORDER BY b.${sortField} ${sortDirection}, b.dedupeKey ASC
|
|
8322
8730
|
LIMIT 1 BY b.dedupeKey
|
|
8323
8731
|
)
|
|
8324
8732
|
ORDER BY ${sortField} ${sortDirection}, dedupeKey ASC
|
|
8325
|
-
LIMIT {limit:UInt32}
|
|
8326
|
-
OFFSET {offset:UInt32}
|
|
8327
8733
|
`,
|
|
8328
8734
|
query_params: {
|
|
8329
8735
|
...params,
|
|
@@ -8345,6 +8751,7 @@ async function listBranches(client, args, strategy) {
|
|
|
8345
8751
|
};
|
|
8346
8752
|
}
|
|
8347
8753
|
async function queryBranchesAfterCursor(client, whereClause, params, limit, cursorId) {
|
|
8754
|
+
const deltaKeys = `SELECT spanType, startedAt, traceId, dedupeKey FROM ${TABLE_TRACE_BRANCHES_DELTA} WHERE cursorId > {afterCursor:UInt64}`;
|
|
8348
8755
|
return await (await client.query({
|
|
8349
8756
|
query: `
|
|
8350
8757
|
SELECT
|
|
@@ -8355,14 +8762,18 @@ async function queryBranchesAfterCursor(client, whereClause, params, limit, curs
|
|
|
8355
8762
|
b.spanId AS spanId,
|
|
8356
8763
|
b.dedupeKey AS dedupeKey,
|
|
8357
8764
|
toString(d.cursorId) AS cursorId
|
|
8358
|
-
FROM ${
|
|
8359
|
-
INNER JOIN
|
|
8765
|
+
FROM ${TABLE_TRACE_BRANCHES} b
|
|
8766
|
+
INNER JOIN (
|
|
8767
|
+
SELECT cursorId, spanType, startedAt, traceId, spanId, dedupeKey
|
|
8768
|
+
FROM ${TABLE_TRACE_BRANCHES_DELTA}
|
|
8769
|
+
WHERE cursorId > {afterCursor:UInt64}
|
|
8770
|
+
) d
|
|
8360
8771
|
ON b.spanType = d.spanType
|
|
8361
8772
|
AND b.startedAt = d.startedAt
|
|
8362
8773
|
AND b.traceId = d.traceId
|
|
8363
8774
|
AND b.spanId = d.spanId
|
|
8364
8775
|
AND b.dedupeKey = d.dedupeKey
|
|
8365
|
-
${whereClause
|
|
8776
|
+
${appendWhere(whereClause, `(b.spanType, b.startedAt, b.traceId, b.dedupeKey) IN (${deltaKeys})`)}
|
|
8366
8777
|
ORDER BY d.cursorId ASC
|
|
8367
8778
|
LIMIT {fetchLimit:UInt32}
|
|
8368
8779
|
`,
|
|
@@ -8375,29 +8786,29 @@ async function queryBranchesAfterCursor(client, whereClause, params, limit, curs
|
|
|
8375
8786
|
clickhouse_settings: CH_SETTINGS
|
|
8376
8787
|
})).json();
|
|
8377
8788
|
}
|
|
8789
|
+
/**
|
|
8790
|
+
* Newest delta cursor whose branch matches the filters. Without filters this
|
|
8791
|
+
* is the stream head; with filters, the branch scan is bounded below by the
|
|
8792
|
+
* oldest `startedAt` still in the delta table.
|
|
8793
|
+
*/
|
|
8378
8794
|
async function getDeltaCursor(client, whereClause, params) {
|
|
8795
|
+
if (!whereClause) return getStreamHeadCursor(client);
|
|
8379
8796
|
const cursorId = (await (await client.query({
|
|
8380
8797
|
query: `
|
|
8381
8798
|
SELECT toString(max(d.cursorId)) AS cursorId
|
|
8382
8799
|
FROM mastra_trace_branches_delta d
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
AND b.dedupeKey = d.dedupeKey
|
|
8389
|
-
${whereClause}
|
|
8800
|
+
WHERE (d.spanType, d.startedAt, d.traceId, d.spanId, d.dedupeKey) IN (
|
|
8801
|
+
SELECT b.spanType, b.startedAt, b.traceId, b.spanId, b.dedupeKey
|
|
8802
|
+
FROM mastra_trace_branches b
|
|
8803
|
+
${appendWhere(whereClause, `b.startedAt >= (SELECT min(startedAt) FROM mastra_trace_branches_delta)`)}
|
|
8804
|
+
)
|
|
8390
8805
|
`,
|
|
8391
8806
|
query_params: params,
|
|
8392
8807
|
format: "JSONEachRow",
|
|
8393
8808
|
clickhouse_settings: CH_SETTINGS
|
|
8394
8809
|
})).json())[0]?.cursorId ?? null;
|
|
8395
8810
|
if (cursorId) return cursorId;
|
|
8396
|
-
return (
|
|
8397
|
-
query: `SELECT toString(max(cursorId)) AS cursorId FROM mastra_trace_branches_delta`,
|
|
8398
|
-
format: "JSONEachRow",
|
|
8399
|
-
clickhouse_settings: CH_SETTINGS
|
|
8400
|
-
})).json())[0]?.cursorId ?? "0";
|
|
8811
|
+
return getStreamHeadCursor(client);
|
|
8401
8812
|
}
|
|
8402
8813
|
async function getStreamHeadCursor(client) {
|
|
8403
8814
|
return (await (await client.query({
|
|
@@ -8411,6 +8822,7 @@ function buildBranchCursor(row) {
|
|
|
8411
8822
|
}
|
|
8412
8823
|
//#endregion
|
|
8413
8824
|
//#region src/storage/domains/observability/v-next/index.ts
|
|
8825
|
+
const spanQueryFeatures = typeof coreStorage.planSpanQuery === "function" ? ["span-query"] : [];
|
|
8414
8826
|
function buildSignalMigrationRequiredMessage(args) {
|
|
8415
8827
|
const tableList = args.tables.map((table) => ` - ${table.table} (${table.engine})`).join("\n");
|
|
8416
8828
|
return `
|
|
@@ -8848,7 +9260,8 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
|
|
|
8848
9260
|
"trace-query-tenant-scope",
|
|
8849
9261
|
"feedback",
|
|
8850
9262
|
"trace-query-context-ids",
|
|
8851
|
-
"trace-aggregate"
|
|
9263
|
+
"trace-aggregate",
|
|
9264
|
+
...spanQueryFeatures
|
|
8852
9265
|
];
|
|
8853
9266
|
return [
|
|
8854
9267
|
"metrics",
|
|
@@ -8867,7 +9280,8 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
|
|
|
8867
9280
|
"trace-query-tenant-scope",
|
|
8868
9281
|
"feedback",
|
|
8869
9282
|
"trace-query-context-ids",
|
|
8870
|
-
"trace-aggregate"
|
|
9283
|
+
"trace-aggregate",
|
|
9284
|
+
...spanQueryFeatures
|
|
8871
9285
|
];
|
|
8872
9286
|
}
|
|
8873
9287
|
async createSpan(args) {
|
|
@@ -8982,6 +9396,18 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
|
|
|
8982
9396
|
}, error);
|
|
8983
9397
|
}
|
|
8984
9398
|
}
|
|
9399
|
+
async querySpans(plan) {
|
|
9400
|
+
try {
|
|
9401
|
+
return await querySpans(this.#client, plan, this.#traceQueryTimeoutMs);
|
|
9402
|
+
} catch (error) {
|
|
9403
|
+
if (error instanceof MastraError || error instanceof coreStorage.TraceQueryExecutionError || error instanceof coreStorage.TraceQueryCursorError || error instanceof coreStorage.TraceQueryResourceLimitError) throw error;
|
|
9404
|
+
throw new MastraError({
|
|
9405
|
+
id: createStorageErrorId("CLICKHOUSE", "QUERY_SPANS", "FAILED"),
|
|
9406
|
+
domain: ErrorDomain.STORAGE,
|
|
9407
|
+
category: ErrorCategory.THIRD_PARTY
|
|
9408
|
+
}, error);
|
|
9409
|
+
}
|
|
9410
|
+
}
|
|
8985
9411
|
async queryTraces(plan) {
|
|
8986
9412
|
try {
|
|
8987
9413
|
return await queryTraces(this.#client, plan, this.#traceQueryTimeoutMs, this.#deltaCursorStrategy);
|
|
@@ -9548,6 +9974,7 @@ var ScoresStorageClickhouse = class extends ScoresStorage {
|
|
|
9548
9974
|
tableName: TABLE_SCORERS,
|
|
9549
9975
|
schema: TABLE_SCHEMAS[TABLE_SCORERS]
|
|
9550
9976
|
});
|
|
9977
|
+
await this.#db.ensureSkipIndexes(TABLE_SCORERS);
|
|
9551
9978
|
}
|
|
9552
9979
|
async dangerouslyClearAll() {
|
|
9553
9980
|
await this.#db.clearTable({ tableName: TABLE_SCORERS });
|
|
@@ -9958,6 +10385,7 @@ var WorkflowsStorageClickhouse = class extends WorkflowsStorage {
|
|
|
9958
10385
|
schema,
|
|
9959
10386
|
ifNotExists: ["resourceId"]
|
|
9960
10387
|
});
|
|
10388
|
+
await this.#db.ensureSkipIndexes(TABLE_WORKFLOW_SNAPSHOT);
|
|
9961
10389
|
}
|
|
9962
10390
|
async dangerouslyClearAll() {
|
|
9963
10391
|
await this.#db.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
|
|
@@ -10154,7 +10582,8 @@ var WorkflowsStorageClickhouse = class extends WorkflowsStorage {
|
|
|
10154
10582
|
ORDER BY createdAt DESC LIMIT 1
|
|
10155
10583
|
`,
|
|
10156
10584
|
query_params: values,
|
|
10157
|
-
format: "JSONEachRow"
|
|
10585
|
+
format: "JSONEachRow",
|
|
10586
|
+
clickhouse_settings: { use_skip_indexes_if_final: 1 }
|
|
10158
10587
|
})).json();
|
|
10159
10588
|
if (!Array.isArray(resultJson) || resultJson.length === 0) return null;
|
|
10160
10589
|
return this.parseWorkflowRun(resultJson[0]);
|