@mastra/clickhouse 1.22.1 → 1.23.0-alpha.1
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 +430 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +431 -18
- package/dist/index.js.map +1 -1
- package/dist/storage/domains/observability/v-next/index.d.ts +4 -2
- package/dist/storage/domains/observability/v-next/index.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-aggregate.d.ts +16 -0
- package/dist/storage/domains/observability/v-next/trace-aggregate.d.ts.map +1 -0
- package/dist/storage/domains/observability/v-next/trace-query.d.ts +34 -3
- package/dist/storage/domains/observability/v-next/trace-query.d.ts.map +1 -1
- package/package.json +5 -5
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";
|
|
@@ -6557,6 +6557,10 @@ const TRACE_STATUS_SQL = `if(isNotNull(r.error), 'error', 'success')`;
|
|
|
6557
6557
|
function durationMsSql(startedAt, endedAt) {
|
|
6558
6558
|
return `dateDiff('millisecond', ${startedAt}, ${endedAt})`;
|
|
6559
6559
|
}
|
|
6560
|
+
/** Top-level metadata string value on a trace root (alias `r`); `key` is a bound parameter placeholder. */
|
|
6561
|
+
function traceMetadataValueSql(key) {
|
|
6562
|
+
return `coalesce(if(mapContains(r.metadataSearch, ${key}), r.metadataSearch[${key}], NULL), nullIf(trim(JSONExtractString(r.metadataRaw, ${key})), ''))`;
|
|
6563
|
+
}
|
|
6560
6564
|
const TRACE_FIELDS = {
|
|
6561
6565
|
traceId: {
|
|
6562
6566
|
sql: "r.traceId",
|
|
@@ -6815,9 +6819,8 @@ function compileScalarPredicate(predicate, registry, parameters, allowMetadata =
|
|
|
6815
6819
|
if (predicate.type === "not") return `NOT (${compileScalarPredicate(predicate.arg, registry, parameters, allowMetadata)})`;
|
|
6816
6820
|
const field = isMetadataField(predicate.field) ? (() => {
|
|
6817
6821
|
if (!allowMetadata) throw new Error(`Unsupported trusted trace-query field: ${predicate.field}`);
|
|
6818
|
-
const key = parameters.add(predicate.field.slice(9), "String");
|
|
6819
6822
|
return {
|
|
6820
|
-
sql:
|
|
6823
|
+
sql: traceMetadataValueSql(parameters.add(predicate.field.slice(9), "String")),
|
|
6821
6824
|
parameterType: "String"
|
|
6822
6825
|
};
|
|
6823
6826
|
})() : fieldDefinition(registry, predicate.field);
|
|
@@ -6910,7 +6913,7 @@ function compileTenantScope(scope, parameters) {
|
|
|
6910
6913
|
if (scope.resourceId !== void 0) sql += `\n AND resourceId = ${parameters.add(scope.resourceId, "String")}`;
|
|
6911
6914
|
return sql;
|
|
6912
6915
|
}
|
|
6913
|
-
function compileClickHouseTraceScope(selection, relationCollections, parameters, scope) {
|
|
6916
|
+
function compileClickHouseTraceScope(selection, relationCollections, parameters, scope, seedConjuncts = []) {
|
|
6914
6917
|
const from = parameters.add(selection.timeRange.from, "DateTime64(3, 'UTC')");
|
|
6915
6918
|
const to = parameters.add(selection.timeRange.to, "DateTime64(3, 'UTC')");
|
|
6916
6919
|
const tenant = compileTenantScope(scope, parameters);
|
|
@@ -6920,9 +6923,9 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
|
|
|
6920
6923
|
FROM ${TABLE_TRACE_ROOTS}
|
|
6921
6924
|
WHERE traceId IN (
|
|
6922
6925
|
SELECT traceId
|
|
6923
|
-
FROM ${TABLE_TRACE_ROOTS}
|
|
6926
|
+
FROM ${TABLE_TRACE_ROOTS} r
|
|
6924
6927
|
WHERE startedAt >= ${from}
|
|
6925
|
-
AND startedAt < ${to}${tenant}
|
|
6928
|
+
AND startedAt < ${to}${tenant}${seedConjuncts.map((conjunct) => `\n AND (${compilePredicate(conjunct, parameters)})`).join("")}
|
|
6926
6929
|
)
|
|
6927
6930
|
ORDER BY dedupeKey
|
|
6928
6931
|
LIMIT 1 BY dedupeKey
|
|
@@ -7015,16 +7018,25 @@ function parseDeltaWatermark(value) {
|
|
|
7015
7018
|
throw new coreStorage.TraceQueryCursorError("TRACE_QUERY_CURSOR_MALFORMED");
|
|
7016
7019
|
}
|
|
7017
7020
|
}
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7021
|
+
/**
|
|
7022
|
+
* Builds the CTE chain ending in `candidates`: the completed, current trace roots in the
|
|
7023
|
+
* selection's time range and tenant scope that match its `where` predicate. Trace queries and
|
|
7024
|
+
* trace aggregates both select from this CTE so they always see the same population.
|
|
7025
|
+
*/
|
|
7026
|
+
function compileClickHouseTraceCandidates(selection, columns, parameters) {
|
|
7027
|
+
const rootConjuncts = selection.where ? (selection.where.type === "boolean" && selection.where.operator === "and" ? selection.where.args : [selection.where]).filter((conjunct) => collectRelationCollections(conjunct).size === 0) : [];
|
|
7028
|
+
const ctes = compileClickHouseTraceScope(selection, collectRelationCollections(selection.where), parameters, selection.scope, rootConjuncts);
|
|
7029
|
+
const predicate = selection.where ? compilePredicate(selection.where, parameters) : "1";
|
|
7022
7030
|
ctes.push(`candidates AS (
|
|
7023
|
-
SELECT ${
|
|
7031
|
+
SELECT ${columns}
|
|
7024
7032
|
FROM root_scope r
|
|
7025
7033
|
WHERE ${predicate}
|
|
7026
7034
|
)`);
|
|
7027
|
-
|
|
7035
|
+
return ctes;
|
|
7036
|
+
}
|
|
7037
|
+
function compileClickHouseTraceQuery(plan, deltaHead) {
|
|
7038
|
+
const parameters = new ParameterBuilder();
|
|
7039
|
+
const candidates = `WITH ${compileClickHouseTraceCandidates(plan, TRACE_SELECT, parameters).join(",\n")}`;
|
|
7028
7040
|
if (plan.result === "groups") return {
|
|
7029
7041
|
query: `${candidates}
|
|
7030
7042
|
SELECT threadId
|
|
@@ -7200,10 +7212,8 @@ function compileClickHouseTraceQueryValues(plan) {
|
|
|
7200
7212
|
const parameters = new ParameterBuilder();
|
|
7201
7213
|
const ctes = compileClickHouseTraceScope(plan, discoveryCollections(plan.predicateScope), parameters, plan.scope);
|
|
7202
7214
|
let field;
|
|
7203
|
-
if (plan.predicateScope === "trace" && plan.path.startsWith("metadata."))
|
|
7204
|
-
|
|
7205
|
-
field = `coalesce(if(mapContains(r.metadataSearch, ${key}), r.metadataSearch[${key}], NULL), nullIf(trim(JSONExtractString(r.metadataRaw, ${key})), ''))`;
|
|
7206
|
-
} else if (plan.predicateScope === "trace" && plan.path === "tags") field = `arrayJoin(arrayDistinct(${TRACE_FIELDS.tags.sql}))`;
|
|
7215
|
+
if (plan.predicateScope === "trace" && plan.path.startsWith("metadata.")) field = traceMetadataValueSql(parameters.add(plan.path.slice(9), "String"));
|
|
7216
|
+
else if (plan.predicateScope === "trace" && plan.path === "tags") field = `arrayJoin(arrayDistinct(${TRACE_FIELDS.tags.sql}))`;
|
|
7207
7217
|
else field = fieldDefinition(discoveryRegistry(plan.predicateScope), plan.path).sql;
|
|
7208
7218
|
const search = plan.search ? `AND positionCaseInsensitiveUTF8(value, ${parameters.add(plan.search, "String")}) > 0` : "";
|
|
7209
7219
|
const limit = parameters.add(plan.limit + 1, "UInt64");
|
|
@@ -7410,6 +7420,380 @@ async function queryThreads(client, plan, timeoutMs) {
|
|
|
7410
7420
|
}) : null }
|
|
7411
7421
|
});
|
|
7412
7422
|
}
|
|
7423
|
+
/** Compile a span-row filter with exactly the same rules as trace span predicates. */
|
|
7424
|
+
function compileSpanQueryPredicate(predicate) {
|
|
7425
|
+
const parameters = new ParameterBuilder();
|
|
7426
|
+
return {
|
|
7427
|
+
sql: compileScalarPredicate(predicate, SPAN_FIELDS, parameters),
|
|
7428
|
+
params: parameters.params
|
|
7429
|
+
};
|
|
7430
|
+
}
|
|
7431
|
+
//#endregion
|
|
7432
|
+
//#region src/storage/domains/observability/v-next/span-query.ts
|
|
7433
|
+
const identityFields = [
|
|
7434
|
+
"organizationId",
|
|
7435
|
+
"resourceId",
|
|
7436
|
+
"traceId",
|
|
7437
|
+
"spanId"
|
|
7438
|
+
];
|
|
7439
|
+
const identities = identityFields.join(", ");
|
|
7440
|
+
const identitySetColumns = "isNull(organizationId), ifNull(organizationId, ''), isNull(resourceId), ifNull(resourceId, ''), traceId, spanId";
|
|
7441
|
+
const identitySetKey = `tuple(${identitySetColumns})`;
|
|
7442
|
+
const displayFields = [
|
|
7443
|
+
"parentSpanId",
|
|
7444
|
+
"name",
|
|
7445
|
+
"spanType",
|
|
7446
|
+
"entityType",
|
|
7447
|
+
"entityId",
|
|
7448
|
+
"entityName",
|
|
7449
|
+
"startedAt",
|
|
7450
|
+
"endedAt",
|
|
7451
|
+
"model",
|
|
7452
|
+
"provider",
|
|
7453
|
+
"durationMs",
|
|
7454
|
+
"status"
|
|
7455
|
+
];
|
|
7456
|
+
var Parameters = class {
|
|
7457
|
+
values = {};
|
|
7458
|
+
add(value, timestamp = false) {
|
|
7459
|
+
const name = `span_${Object.keys(this.values).length}`;
|
|
7460
|
+
this.values[name] = timestamp ? new Date(value).toISOString().replace("T", " ").replace(/Z$/, "") : value;
|
|
7461
|
+
return `{${name}:${timestamp ? "DateTime64(3, 'UTC')" : typeof value === "number" ? "UInt64" : "String"}}`;
|
|
7462
|
+
}
|
|
7463
|
+
};
|
|
7464
|
+
function identityValues(row) {
|
|
7465
|
+
return [
|
|
7466
|
+
row.organizationId === null ? 1 : 0,
|
|
7467
|
+
row.organizationId ?? "",
|
|
7468
|
+
row.resourceId === null ? 1 : 0,
|
|
7469
|
+
row.resourceId ?? "",
|
|
7470
|
+
row.traceId,
|
|
7471
|
+
row.spanId
|
|
7472
|
+
];
|
|
7473
|
+
}
|
|
7474
|
+
function afterCursor(plan, parameters) {
|
|
7475
|
+
if (!plan.cursor) return "1";
|
|
7476
|
+
const cursor = plan.cursor;
|
|
7477
|
+
const timestamp = parameters.add(cursor.sortValue, true);
|
|
7478
|
+
const field = `s.${plan.orderBy.field}`;
|
|
7479
|
+
const terms = [`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${timestamp}`];
|
|
7480
|
+
const equal = [`${field} = ${timestamp}`];
|
|
7481
|
+
for (const key of identityFields) {
|
|
7482
|
+
const value = cursor[key];
|
|
7483
|
+
if (value === null) equal.push(`isNull(s.${key})`);
|
|
7484
|
+
else {
|
|
7485
|
+
const parameter = parameters.add(value);
|
|
7486
|
+
terms.push(`(${equal.join(" AND ")} AND (s.${key} > ${parameter} OR isNull(s.${key})))`);
|
|
7487
|
+
equal.push(`s.${key} = ${parameter}`);
|
|
7488
|
+
}
|
|
7489
|
+
}
|
|
7490
|
+
return `(${terms.join(" OR ")})`;
|
|
7491
|
+
}
|
|
7492
|
+
function predicateFields(predicate, fields = /* @__PURE__ */ new Set()) {
|
|
7493
|
+
if (!predicate) return fields;
|
|
7494
|
+
if (predicate.type === "boolean") for (const arg of predicate.args) predicateFields(arg, fields);
|
|
7495
|
+
else if (predicate.type === "not") predicateFields(predicate.arg, fields);
|
|
7496
|
+
else fields.add(predicate.field);
|
|
7497
|
+
return fields;
|
|
7498
|
+
}
|
|
7499
|
+
function fieldExpression(field) {
|
|
7500
|
+
if (field === "model" || field === "provider") return `if(JSONType(e.attributes, '${field}') = 'String', JSONExtractString(e.attributes, '${field}'), NULL)`;
|
|
7501
|
+
if (field === "durationMs") return "dateDiff('millisecond', e.startedAt, e.endedAt)";
|
|
7502
|
+
if (field === "status") return "if(isNull(e.error), 'success', 'error')";
|
|
7503
|
+
if (field === "error") return "if(isNull(e.error), NULL, 'error')";
|
|
7504
|
+
return `e.${field}`;
|
|
7505
|
+
}
|
|
7506
|
+
function compileClickHouseSpanQuery(plan) {
|
|
7507
|
+
const parameters = new Parameters();
|
|
7508
|
+
const from = parameters.add(plan.timeRange.from, true);
|
|
7509
|
+
const to = parameters.add(plan.timeRange.to, true);
|
|
7510
|
+
const scope = [];
|
|
7511
|
+
if (plan.scope) {
|
|
7512
|
+
scope.push(`organizationId = ${parameters.add(plan.scope.organizationId)}`);
|
|
7513
|
+
if (plan.scope.resourceId !== void 0) scope.push(`resourceId = ${parameters.add(plan.scope.resourceId)}`);
|
|
7514
|
+
}
|
|
7515
|
+
const predicate = plan.where ? compileSpanQueryPredicate(plan.where) : {
|
|
7516
|
+
sql: "1",
|
|
7517
|
+
params: {}
|
|
7518
|
+
};
|
|
7519
|
+
const selectionFields = [.../* @__PURE__ */ new Set([
|
|
7520
|
+
"startedAt",
|
|
7521
|
+
"endedAt",
|
|
7522
|
+
...predicateFields(plan.where)
|
|
7523
|
+
])].filter((field) => !new Set(identityFields).has(field));
|
|
7524
|
+
const expressions = selectionFields.map(fieldExpression);
|
|
7525
|
+
const after = afterCursor(plan, parameters);
|
|
7526
|
+
return {
|
|
7527
|
+
query: `WITH candidate_ids AS (
|
|
7528
|
+
SELECT ${identitySetKey} AS identity FROM ${TABLE_SPAN_EVENTS}
|
|
7529
|
+
PREWHERE endedAt >= ${from}
|
|
7530
|
+
WHERE startedAt >= ${from} AND startedAt < ${to} ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
|
|
7531
|
+
), candidates AS (
|
|
7532
|
+
SELECT ${identities}, ${selectionFields.map((field, index) => `${expressions[index]} AS ${field}`).join(", ")}
|
|
7533
|
+
FROM ${TABLE_SPAN_EVENTS} e
|
|
7534
|
+
PREWHERE e.endedAt >= ${from}
|
|
7535
|
+
WHERE ${identitySetKey} IN (SELECT identity FROM candidate_ids) ${scope.length ? `AND ${scope.join(" AND ")}` : ""}
|
|
7536
|
+
ORDER BY ${identities}, e.endedAt DESC
|
|
7537
|
+
LIMIT 1 BY ${identities}
|
|
7538
|
+
)
|
|
7539
|
+
SELECT ${[
|
|
7540
|
+
...identityFields,
|
|
7541
|
+
"startedAt",
|
|
7542
|
+
"endedAt"
|
|
7543
|
+
].map((field) => `s.${field}`).join(", ")}
|
|
7544
|
+
FROM candidates s WHERE s.startedAt >= ${from} AND s.startedAt < ${to} AND (${predicate.sql}) AND ${after}
|
|
7545
|
+
ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"}, ${identityFields.map((field) => `s.${field} ASC NULLS LAST`).join(", ")}
|
|
7546
|
+
LIMIT ${parameters.add(plan.limit + 1)}`,
|
|
7547
|
+
query_params: {
|
|
7548
|
+
...parameters.values,
|
|
7549
|
+
...predicate.params
|
|
7550
|
+
}
|
|
7551
|
+
};
|
|
7552
|
+
}
|
|
7553
|
+
function iso(value) {
|
|
7554
|
+
return new Date(value.includes("T") ? value : `${value.replace(" ", "T")}Z`).toISOString();
|
|
7555
|
+
}
|
|
7556
|
+
async function querySpans(client, plan, timeoutMs) {
|
|
7557
|
+
const deadline = performance.now() + coreStorage.resolveTraceQueryTimeoutMs(timeoutMs);
|
|
7558
|
+
const run = async (compiled) => {
|
|
7559
|
+
const remaining = Math.floor(deadline - performance.now());
|
|
7560
|
+
if (remaining <= 0) throw new coreStorage.TraceQueryExecutionError();
|
|
7561
|
+
return await runWithClickHouseTraceQueryTimeout(client, {
|
|
7562
|
+
timeoutMs: remaining,
|
|
7563
|
+
memoryLimitBytes: 512 * 1024 * 1024
|
|
7564
|
+
}, compiled);
|
|
7565
|
+
};
|
|
7566
|
+
const selected = (await run(compileClickHouseSpanQuery(plan))).map((row) => ({
|
|
7567
|
+
...row,
|
|
7568
|
+
startedAt: iso(row.startedAt),
|
|
7569
|
+
endedAt: iso(row.endedAt)
|
|
7570
|
+
}));
|
|
7571
|
+
const page = selected.slice(0, plan.limit);
|
|
7572
|
+
if (!page.length) return {
|
|
7573
|
+
spans: [],
|
|
7574
|
+
page: { next: null }
|
|
7575
|
+
};
|
|
7576
|
+
const payloadWhere = `tuple(${identitySetColumns}, endedAt) IN {page:Array(Tuple(UInt8, String, UInt8, String, String, String, DateTime64(3, 'UTC')))}`;
|
|
7577
|
+
const payloads = await run({
|
|
7578
|
+
query: `SELECT ${identities}, ${displayFields.map((field, index) => `data.${index + 1} AS ${field}`).join(", ")},
|
|
7579
|
+
data.${displayFields.length + 1} AS inputPreview, data.${displayFields.length + 2} AS outputPreview
|
|
7580
|
+
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
|
|
7581
|
+
FROM ${TABLE_SPAN_EVENTS} e WHERE ${payloadWhere} GROUP BY ${identities})`,
|
|
7582
|
+
query_params: { page: page.map((row) => new TupleParam([...identityValues(row), row.endedAt.replace("T", " ").replace(/Z$/, "")])) }
|
|
7583
|
+
});
|
|
7584
|
+
const metricWhere = `${identitySetKey} IN {page:Array(Tuple(UInt8, String, UInt8, String, String, String))}`;
|
|
7585
|
+
const metrics = await run({
|
|
7586
|
+
query: `SELECT ${identities}, data.1 AS name, data.2 AS estimatedCost,
|
|
7587
|
+
data.3 AS costUnit, data.4 AS allocation, data.5 AS costError
|
|
7588
|
+
FROM (SELECT ${identities}, metricId, argMax(tuple(name, estimatedCost, costUnit,
|
|
7589
|
+
JSONExtractString(costMetadata, 'allocation'), JSONHas(costMetadata, 'error')), timestamp) AS data
|
|
7590
|
+
FROM ${TABLE_METRIC_EVENTS} WHERE (${metricWhere}) AND name LIKE 'mastra_model_%'
|
|
7591
|
+
GROUP BY ${identities}, metricId)
|
|
7592
|
+
LIMIT ${coreStorage.SPAN_QUERY_MAX_COST_METRICS + 1}`,
|
|
7593
|
+
query_params: { page: page.map((row) => new TupleParam(identityValues(row))) }
|
|
7594
|
+
});
|
|
7595
|
+
if (metrics.length > coreStorage.SPAN_QUERY_MAX_COST_METRICS) throw new coreStorage.TraceQueryResourceLimitError();
|
|
7596
|
+
const byIdentity = new Map(payloads.map((row) => [coreStorage.spanQueryIdentityKey(row), row]));
|
|
7597
|
+
const hydrated = page.flatMap((identity) => {
|
|
7598
|
+
const row = byIdentity.get(coreStorage.spanQueryIdentityKey(identity));
|
|
7599
|
+
if (!row) return [];
|
|
7600
|
+
const { inputPreview: _input, outputPreview: _output, ...metadata } = row;
|
|
7601
|
+
return [{
|
|
7602
|
+
...metadata,
|
|
7603
|
+
...identity,
|
|
7604
|
+
durationMs: Number(row.durationMs)
|
|
7605
|
+
}];
|
|
7606
|
+
});
|
|
7607
|
+
const result = coreStorage.buildSpanQueryResponse(plan, hydrated, payloads, metrics);
|
|
7608
|
+
const last = page.at(-1);
|
|
7609
|
+
result.page.next = selected.length > plan.limit ? coreStorage.encodeSpanQueryCursor(plan, {
|
|
7610
|
+
organizationId: last.organizationId,
|
|
7611
|
+
resourceId: last.resourceId,
|
|
7612
|
+
traceId: last.traceId,
|
|
7613
|
+
spanId: last.spanId,
|
|
7614
|
+
sortValue: last[plan.orderBy.field]
|
|
7615
|
+
}) : null;
|
|
7616
|
+
return result;
|
|
7617
|
+
}
|
|
7618
|
+
//#endregion
|
|
7619
|
+
//#region src/storage/domains/observability/v-next/trace-aggregate.ts
|
|
7620
|
+
/** Trace-root column expressions (alias `r`) for each groupable dimension; same values `where` sees. */
|
|
7621
|
+
const DIMENSION_SQL = {
|
|
7622
|
+
entityType: "r.entityType",
|
|
7623
|
+
entityName: "r.entityName",
|
|
7624
|
+
environment: "r.environment",
|
|
7625
|
+
status: TRACE_STATUS_SQL,
|
|
7626
|
+
serviceName: "r.serviceName",
|
|
7627
|
+
executionSource: "r.executionSource",
|
|
7628
|
+
threadId: "r.threadId",
|
|
7629
|
+
resourceId: "r.resourceId",
|
|
7630
|
+
userId: "r.userId",
|
|
7631
|
+
sessionId: "r.sessionId",
|
|
7632
|
+
organizationId: "r.organizationId",
|
|
7633
|
+
experimentId: "r.experimentId"
|
|
7634
|
+
};
|
|
7635
|
+
const PERCENTILES = {
|
|
7636
|
+
"duration.p50": .5,
|
|
7637
|
+
"duration.p90": .9,
|
|
7638
|
+
"duration.p95": .95,
|
|
7639
|
+
"duration.p99": .99
|
|
7640
|
+
};
|
|
7641
|
+
const COMPARISON_SQL = {
|
|
7642
|
+
eq: "=",
|
|
7643
|
+
ne: "!=",
|
|
7644
|
+
lt: "<",
|
|
7645
|
+
lte: "<=",
|
|
7646
|
+
gt: ">",
|
|
7647
|
+
gte: ">="
|
|
7648
|
+
};
|
|
7649
|
+
/**
|
|
7650
|
+
* Compiles a trusted aggregate plan. The plan's caps are enforced by the planner and trusted here.
|
|
7651
|
+
*
|
|
7652
|
+
* Every candidate root is first reduced to a `facts` row (dimension values, duration, error flag,
|
|
7653
|
+
* countDistinct inputs, bucket). Without `interval`, groups are filtered, ordered, and limited in
|
|
7654
|
+
* one grouped select. With `interval`, one `GROUPING SETS` pass computes both the whole-window
|
|
7655
|
+
* group rows and the per-bucket rows: group rows are ranked (having → orderBy), and each bucket
|
|
7656
|
+
* row inherits its group's rank. ClickHouse re-executes a CTE at every reference, so this keeps
|
|
7657
|
+
* the candidate scan to a single pass instead of joining bucket rows back to a ranked CTE.
|
|
7658
|
+
*/
|
|
7659
|
+
function compileClickHouseTraceAggregate(plan) {
|
|
7660
|
+
const parameters = new ParameterBuilder();
|
|
7661
|
+
const ctes = compileClickHouseTraceCandidates(plan, "*", parameters);
|
|
7662
|
+
const dimensionSql = (field) => {
|
|
7663
|
+
if (field.startsWith("metadata.")) return traceMetadataValueSql(parameters.add(field.slice(9), "String"));
|
|
7664
|
+
const sql = Object.hasOwn(DIMENSION_SQL, field) ? DIMENSION_SQL[field] : void 0;
|
|
7665
|
+
if (sql === void 0) throw new Error(`Unsupported trusted trace-aggregate field: ${field}`);
|
|
7666
|
+
return sql;
|
|
7667
|
+
};
|
|
7668
|
+
const distinctSql = (field) => field === "traceId" ? "r.traceId" : dimensionSql(field);
|
|
7669
|
+
const dimensionColumns = plan.dimensions.map((_, index) => `d${index}`);
|
|
7670
|
+
const distinctColumns = /* @__PURE__ */ new Map();
|
|
7671
|
+
const factColumns = plan.dimensions.map((dimension, index) => `CAST(${dimensionSql(dimension)} AS Nullable(String)) AS d${index}`);
|
|
7672
|
+
for (const measure of plan.measures) {
|
|
7673
|
+
if (measure.type !== "countDistinct" || distinctColumns.has(measure.field)) continue;
|
|
7674
|
+
const column = `cd${distinctColumns.size}`;
|
|
7675
|
+
distinctColumns.set(measure.field, column);
|
|
7676
|
+
factColumns.push(`CAST(${distinctSql(measure.field)} AS Nullable(String)) AS ${column}`);
|
|
7677
|
+
}
|
|
7678
|
+
factColumns.push(`toFloat64(${durationMsSql("r.startedAt", "r.endedAt")}) AS durationMs`, `isNotNull(r.error) AS isError`, `cityHash64(r.traceId) AS traceSeed`);
|
|
7679
|
+
if (plan.interval !== void 0) {
|
|
7680
|
+
const intervalMs = coreStorage.TRACE_AGGREGATE_INTERVAL_MS[plan.interval];
|
|
7681
|
+
if (intervalMs === void 0) throw new Error(`Unsupported trusted trace-aggregate interval: ${plan.interval}`);
|
|
7682
|
+
factColumns.push(`fromUnixTimestamp64Milli(intDiv(toUnixTimestamp64Milli(r.startedAt), ${intervalMs}) * ${intervalMs}, 'UTC') AS bucket`);
|
|
7683
|
+
}
|
|
7684
|
+
ctes.push(`facts AS (
|
|
7685
|
+
SELECT ${factColumns.join(",\n ")}
|
|
7686
|
+
FROM candidates r
|
|
7687
|
+
)`);
|
|
7688
|
+
const measureSql = (name) => {
|
|
7689
|
+
if (name === "count") return "toFloat64(count())";
|
|
7690
|
+
if (name === "errorCount") return "toFloat64(countIf(isError))";
|
|
7691
|
+
if (name === "errorRate") return "countIf(isError) / count()";
|
|
7692
|
+
if (name === "duration.avg") return "avg(durationMs)";
|
|
7693
|
+
if (name === "duration.min") return "min(durationMs)";
|
|
7694
|
+
if (name === "duration.max") return "max(durationMs)";
|
|
7695
|
+
const percentile = PERCENTILES[name];
|
|
7696
|
+
if (percentile !== void 0) return `quantileDeterministic(${percentile})(durationMs, traceSeed)`;
|
|
7697
|
+
const measure = plan.measures.find((candidate) => candidate.type === "countDistinct" && candidate.name === name);
|
|
7698
|
+
const distinctColumn = measure && distinctColumns.get(measure.field);
|
|
7699
|
+
if (distinctColumn === void 0) throw new Error(`Unsupported trusted trace-aggregate measure: ${name}`);
|
|
7700
|
+
return `toFloat64(uniqExact(${distinctColumn}))`;
|
|
7701
|
+
};
|
|
7702
|
+
const compileHaving = (predicate) => {
|
|
7703
|
+
if (predicate.type === "boolean") return predicate.args.map((arg) => `(${compileHaving(arg)})`).join(predicate.operator === "and" ? " AND " : " OR ");
|
|
7704
|
+
if (predicate.type === "not") return `NOT (${compileHaving(predicate.arg)})`;
|
|
7705
|
+
const measure = measureSql(predicate.measure);
|
|
7706
|
+
if (predicate.type === "membership") {
|
|
7707
|
+
if (predicate.values.length === 0) return predicate.operator === "in" ? "0" : "1";
|
|
7708
|
+
const list = predicate.values.map((value) => parameters.add(value, "Float64")).join(", ");
|
|
7709
|
+
return `${measure} ${predicate.operator === "in" ? "IN" : "NOT IN"} (${list})`;
|
|
7710
|
+
}
|
|
7711
|
+
const operator = COMPARISON_SQL[predicate.operator];
|
|
7712
|
+
if (operator === void 0) throw new Error(`Unsupported trusted trace-aggregate operator: ${predicate.operator}`);
|
|
7713
|
+
return `${measure} ${operator} ${parameters.add(predicate.value, "Float64")}`;
|
|
7714
|
+
};
|
|
7715
|
+
const havingSql = ["count() > 0", ...plan.having ? [`(${compileHaving(plan.having)})`] : []].join(" AND ");
|
|
7716
|
+
const direction = plan.orderBy.direction === "asc" ? "ASC" : "DESC";
|
|
7717
|
+
let orderTarget;
|
|
7718
|
+
if (plan.orderBy.target === "measure") orderTarget = measureSql(plan.orderBy.measure);
|
|
7719
|
+
else {
|
|
7720
|
+
const index = plan.dimensions.indexOf(plan.orderBy.dimension);
|
|
7721
|
+
if (index === -1) throw new Error(`Unsupported trusted trace-aggregate order dimension: ${plan.orderBy.dimension}`);
|
|
7722
|
+
orderTarget = dimensionColumns[index];
|
|
7723
|
+
}
|
|
7724
|
+
const tiebreakSql = dimensionColumns.map((column) => `${column} ASC NULLS LAST`);
|
|
7725
|
+
const measureColumns = plan.measures.map((measure, index) => `${measureSql(measure.name)} AS m${index}`);
|
|
7726
|
+
const measureNames = plan.measures.map((_, index) => `m${index}`);
|
|
7727
|
+
if (plan.interval === void 0) {
|
|
7728
|
+
const limit = parameters.add(plan.limit + 1, "UInt64");
|
|
7729
|
+
return {
|
|
7730
|
+
query: `WITH ${ctes.join(",\n")}
|
|
7731
|
+
SELECT ${[...dimensionColumns, ...measureColumns].join(", ")}
|
|
7732
|
+
FROM facts
|
|
7733
|
+
${dimensionColumns.length > 0 ? `GROUP BY ${dimensionColumns.join(", ")}\n` : ""}HAVING ${havingSql}
|
|
7734
|
+
ORDER BY ${[`${orderTarget} ${direction} NULLS LAST`, ...tiebreakSql].join(", ")}
|
|
7735
|
+
LIMIT ${limit}`,
|
|
7736
|
+
query_params: parameters.params
|
|
7737
|
+
};
|
|
7738
|
+
}
|
|
7739
|
+
const maxRank = parameters.add(plan.limit, "UInt64");
|
|
7740
|
+
const groupOrderSql = [`__order ${direction} NULLS LAST`, ...tiebreakSql].join(", ");
|
|
7741
|
+
const groupPartition = dimensionColumns.length > 0 ? `PARTITION BY ${dimensionColumns.join(", ")}` : "";
|
|
7742
|
+
ctes.push(`grouped AS (
|
|
7743
|
+
SELECT ${[
|
|
7744
|
+
...dimensionColumns,
|
|
7745
|
+
"bucket",
|
|
7746
|
+
"grouping(bucket) = 1 AS __isGroup",
|
|
7747
|
+
...measureColumns,
|
|
7748
|
+
`(${havingSql}) AS __keep`,
|
|
7749
|
+
`${orderTarget} AS __order`
|
|
7750
|
+
].join(", ")}
|
|
7751
|
+
FROM facts
|
|
7752
|
+
GROUP BY GROUPING SETS ((${[...dimensionColumns, "bucket"].join(", ")}), (${dimensionColumns.join(", ")}))
|
|
7753
|
+
)`, `ranked AS (
|
|
7754
|
+
SELECT *,
|
|
7755
|
+
if(__isGroup AND __keep, row_number() OVER (PARTITION BY __isGroup, __keep ORDER BY ${groupOrderSql}), 0) AS __groupRank,
|
|
7756
|
+
countIf(__isGroup AND __keep) OVER () AS __survivingGroups
|
|
7757
|
+
FROM grouped
|
|
7758
|
+
)`, `expanded AS (
|
|
7759
|
+
SELECT *, maxIf(__groupRank, __isGroup) OVER (${groupPartition}) AS __rank
|
|
7760
|
+
FROM ranked
|
|
7761
|
+
)`);
|
|
7762
|
+
return {
|
|
7763
|
+
query: `WITH ${ctes.join(",\n")}
|
|
7764
|
+
SELECT ${[
|
|
7765
|
+
...dimensionColumns,
|
|
7766
|
+
"bucket",
|
|
7767
|
+
...measureNames,
|
|
7768
|
+
`toUInt8(__survivingGroups > ${maxRank}) AS truncated`
|
|
7769
|
+
].join(", ")}
|
|
7770
|
+
FROM expanded
|
|
7771
|
+
WHERE NOT __isGroup AND __rank BETWEEN 1 AND ${maxRank}
|
|
7772
|
+
ORDER BY __rank ASC, bucket ASC`,
|
|
7773
|
+
query_params: parameters.params
|
|
7774
|
+
};
|
|
7775
|
+
}
|
|
7776
|
+
async function aggregateTraces(client, plan, timeoutMs) {
|
|
7777
|
+
const result = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs }, compileClickHouseTraceAggregate(plan));
|
|
7778
|
+
let truncated;
|
|
7779
|
+
let resultRows;
|
|
7780
|
+
if (plan.interval === void 0) {
|
|
7781
|
+
truncated = result.length > plan.limit;
|
|
7782
|
+
resultRows = result.slice(0, plan.limit);
|
|
7783
|
+
} else {
|
|
7784
|
+
truncated = Number(result[0]?.truncated ?? 0) === 1;
|
|
7785
|
+
resultRows = result;
|
|
7786
|
+
}
|
|
7787
|
+
return {
|
|
7788
|
+
rows: resultRows.map((row) => {
|
|
7789
|
+
const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, Number(row[`m${index}`])])) };
|
|
7790
|
+
if (plan.dimensions.length > 0) shaped.dimensions = Object.fromEntries(plan.dimensions.map((dimension, index) => [dimension, row[`d${index}`] ?? null]));
|
|
7791
|
+
if (plan.interval !== void 0) shaped.bucket = new Date(row.bucket).toISOString();
|
|
7792
|
+
return shaped;
|
|
7793
|
+
}),
|
|
7794
|
+
truncated
|
|
7795
|
+
};
|
|
7796
|
+
}
|
|
7413
7797
|
//#endregion
|
|
7414
7798
|
//#region src/storage/domains/observability/v-next/trace-roots.ts
|
|
7415
7799
|
/**
|
|
@@ -8222,6 +8606,7 @@ function buildBranchCursor(row) {
|
|
|
8222
8606
|
}
|
|
8223
8607
|
//#endregion
|
|
8224
8608
|
//#region src/storage/domains/observability/v-next/index.ts
|
|
8609
|
+
const spanQueryFeatures = typeof coreStorage.planSpanQuery === "function" ? ["span-query"] : [];
|
|
8225
8610
|
function buildSignalMigrationRequiredMessage(args) {
|
|
8226
8611
|
const tableList = args.tables.map((table) => ` - ${table.table} (${table.engine})`).join("\n");
|
|
8227
8612
|
return `
|
|
@@ -8653,12 +9038,14 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
|
|
|
8653
9038
|
"tag-discovery",
|
|
8654
9039
|
"metric-discovery",
|
|
8655
9040
|
"trace-query",
|
|
9041
|
+
...spanQueryFeatures,
|
|
8656
9042
|
"trace-query-root-duration",
|
|
8657
9043
|
"trace-query-discovery",
|
|
8658
9044
|
"thread-query",
|
|
8659
9045
|
"trace-query-tenant-scope",
|
|
8660
9046
|
"feedback",
|
|
8661
|
-
"trace-query-context-ids"
|
|
9047
|
+
"trace-query-context-ids",
|
|
9048
|
+
"trace-aggregate"
|
|
8662
9049
|
];
|
|
8663
9050
|
return [
|
|
8664
9051
|
"metrics",
|
|
@@ -8671,12 +9058,14 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
|
|
|
8671
9058
|
"metric-discovery",
|
|
8672
9059
|
"delta-polling",
|
|
8673
9060
|
"trace-query",
|
|
9061
|
+
...spanQueryFeatures,
|
|
8674
9062
|
"trace-query-root-duration",
|
|
8675
9063
|
"trace-query-discovery",
|
|
8676
9064
|
"thread-query",
|
|
8677
9065
|
"trace-query-tenant-scope",
|
|
8678
9066
|
"feedback",
|
|
8679
|
-
"trace-query-context-ids"
|
|
9067
|
+
"trace-query-context-ids",
|
|
9068
|
+
"trace-aggregate"
|
|
8680
9069
|
];
|
|
8681
9070
|
}
|
|
8682
9071
|
async createSpan(args) {
|
|
@@ -8791,6 +9180,18 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
|
|
|
8791
9180
|
}, error);
|
|
8792
9181
|
}
|
|
8793
9182
|
}
|
|
9183
|
+
async querySpans(plan) {
|
|
9184
|
+
try {
|
|
9185
|
+
return await querySpans(this.#client, plan, this.#traceQueryTimeoutMs);
|
|
9186
|
+
} catch (error) {
|
|
9187
|
+
if (error instanceof MastraError || error instanceof coreStorage.TraceQueryExecutionError || error instanceof coreStorage.TraceQueryCursorError || error instanceof coreStorage.TraceQueryResourceLimitError) throw error;
|
|
9188
|
+
throw new MastraError({
|
|
9189
|
+
id: createStorageErrorId("CLICKHOUSE", "QUERY_SPANS", "FAILED"),
|
|
9190
|
+
domain: ErrorDomain.STORAGE,
|
|
9191
|
+
category: ErrorCategory.THIRD_PARTY
|
|
9192
|
+
}, error);
|
|
9193
|
+
}
|
|
9194
|
+
}
|
|
8794
9195
|
async queryTraces(plan) {
|
|
8795
9196
|
try {
|
|
8796
9197
|
return await queryTraces(this.#client, plan, this.#traceQueryTimeoutMs, this.#deltaCursorStrategy);
|
|
@@ -8803,6 +9204,18 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
|
|
|
8803
9204
|
}, error);
|
|
8804
9205
|
}
|
|
8805
9206
|
}
|
|
9207
|
+
async aggregateTraces(plan) {
|
|
9208
|
+
try {
|
|
9209
|
+
return await aggregateTraces(this.#client, plan, this.#traceQueryTimeoutMs);
|
|
9210
|
+
} catch (error) {
|
|
9211
|
+
if (error instanceof MastraError || error instanceof coreStorage.TraceQueryExecutionError || error instanceof coreStorage.TraceQueryResourceLimitError) throw error;
|
|
9212
|
+
throw new MastraError({
|
|
9213
|
+
id: createStorageErrorId("CLICKHOUSE", "AGGREGATE_TRACES", "FAILED"),
|
|
9214
|
+
domain: ErrorDomain.STORAGE,
|
|
9215
|
+
category: ErrorCategory.THIRD_PARTY
|
|
9216
|
+
}, error);
|
|
9217
|
+
}
|
|
9218
|
+
}
|
|
8806
9219
|
async getTraceQueryObservedFields(plan) {
|
|
8807
9220
|
try {
|
|
8808
9221
|
return await getTraceQueryObservedFields(this.#client, plan, this.#traceQueryDiscoveryLimits);
|