@mastra/duckdb 1.13.0-alpha.1 → 1.13.0-alpha.3
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 +1 -1
- package/dist/index.js +1 -1
- package/dist/{observability-DNuNWZFX.cjs → observability-DBm19qBS.cjs} +251 -70
- package/dist/observability-DBm19qBS.cjs.map +1 -0
- package/dist/{observability-Bk_y3hof.js → observability-x50aDnBm.js} +251 -70
- package/dist/observability-x50aDnBm.js.map +1 -0
- package/dist/storage/domains/observability/helpers.d.ts +17 -0
- package/dist/storage/domains/observability/helpers.d.ts.map +1 -1
- package/dist/storage/domains/observability/index.d.ts +2 -0
- package/dist/storage/domains/observability/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/migration.d.ts +1 -0
- package/dist/storage/domains/observability/migration.d.ts.map +1 -1
- package/dist/storage/domains/observability/span-query.d.ts.map +1 -1
- package/dist/storage/domains/observability/trace-aggregate.d.ts +9 -2
- package/dist/storage/domains/observability/trace-aggregate.d.ts.map +1 -1
- package/dist/storage/domains/observability/trace-query.d.ts.map +1 -1
- package/dist/storage/domains/observability/tracing.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/observability-Bk_y3hof.js.map +0 -1
- package/dist/observability-DNuNWZFX.cjs.map +0 -1
package/dist/docs/SKILL.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -556,7 +556,7 @@ var ObservabilityStorageDuckDB = class extends _mastra_core_storage.Observabilit
|
|
|
556
556
|
async loadDelegate() {
|
|
557
557
|
if (this.delegate) return this.delegate;
|
|
558
558
|
if (this.unavailableError) return null;
|
|
559
|
-
if (!this.loadPromise) this.loadPromise = Promise.resolve().then(() => require("./observability-
|
|
559
|
+
if (!this.loadPromise) this.loadPromise = Promise.resolve().then(() => require("./observability-DBm19qBS.cjs")).then(({ ObservabilityStorageDuckDB }) => {
|
|
560
560
|
const delegate = new ObservabilityStorageDuckDB({ db: this.db });
|
|
561
561
|
this.delegate = delegate;
|
|
562
562
|
return delegate;
|
package/dist/index.js
CHANGED
|
@@ -555,7 +555,7 @@ var ObservabilityStorageDuckDB = class extends ObservabilityStorage {
|
|
|
555
555
|
async loadDelegate() {
|
|
556
556
|
if (this.delegate) return this.delegate;
|
|
557
557
|
if (this.unavailableError) return null;
|
|
558
|
-
if (!this.loadPromise) this.loadPromise = import("./observability-
|
|
558
|
+
if (!this.loadPromise) this.loadPromise = import("./observability-x50aDnBm.js").then(({ ObservabilityStorageDuckDB }) => {
|
|
559
559
|
const delegate = new ObservabilityStorageDuckDB({ db: this.db });
|
|
560
560
|
this.delegate = delegate;
|
|
561
561
|
return delegate;
|
|
@@ -688,6 +688,33 @@ function jsonV(val) {
|
|
|
688
688
|
return require_features.DuckDBConnection.sqlValue(JSON.stringify(val));
|
|
689
689
|
}
|
|
690
690
|
/**
|
|
691
|
+
* `span_events` JSON columns that can hold large payloads. Missing values are
|
|
692
|
+
* stored as the JSON literal `null` instead of SQL NULL.
|
|
693
|
+
*
|
|
694
|
+
* DuckDB only reads the selected rows of a large-string column when the
|
|
695
|
+
* column's validity mask is constant (no NULLs in the row group). Any SQL NULL
|
|
696
|
+
* makes it fall back to reading the whole 2,048-row vector, which loads every
|
|
697
|
+
* large string in it even when the query needs one row. Keeping these columns
|
|
698
|
+
* NULL-free lets point lookups and page reads load only what they return.
|
|
699
|
+
*/
|
|
700
|
+
const SPAN_PAYLOAD_COLUMNS = [
|
|
701
|
+
"attributes",
|
|
702
|
+
"input",
|
|
703
|
+
"output",
|
|
704
|
+
"requestContext"
|
|
705
|
+
];
|
|
706
|
+
/** Stored in {@link SPAN_PAYLOAD_COLUMNS} in place of SQL NULL; read back as missing. */
|
|
707
|
+
const JSON_NULL_PLACEHOLDER = "null";
|
|
708
|
+
/** Like {@link jsonV}, but writes the JSON `null` placeholder instead of SQL NULL. */
|
|
709
|
+
function payloadJsonV(val) {
|
|
710
|
+
if (val === null || val === void 0) return `'${JSON_NULL_PLACEHOLDER}'`;
|
|
711
|
+
return jsonV(val);
|
|
712
|
+
}
|
|
713
|
+
/** SQL for a payload column that maps the JSON `null` placeholder back to SQL NULL. */
|
|
714
|
+
function payloadColumnSql(column) {
|
|
715
|
+
return `NULLIF(${column}, '${JSON_NULL_PLACEHOLDER}')`;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
691
718
|
* Trim, drop blank, and dedupe tags before they are written, matching the
|
|
692
719
|
* PostgreSQL and ClickHouse stores so tag predicates and discovery agree.
|
|
693
720
|
*/
|
|
@@ -2935,7 +2962,7 @@ const TRACE_SELECT = `
|
|
|
2935
2962
|
r.entityId AS entityId,
|
|
2936
2963
|
r.parentSpanId AS parentSpanId,
|
|
2937
2964
|
r.metadata AS metadata,
|
|
2938
|
-
r.input AS input,
|
|
2965
|
+
${payloadColumnSql("r.input")} AS input,
|
|
2939
2966
|
r.threadId AS threadId,
|
|
2940
2967
|
r.resourceId AS resourceId,
|
|
2941
2968
|
r.startedAt AS startedAt,
|
|
@@ -3218,6 +3245,11 @@ function compileDuckDBTraceScope(relatedCollections, scope, timeRange, rootPredi
|
|
|
3218
3245
|
GROUP BY r.traceId
|
|
3219
3246
|
HAVING bool_or(r.timestamp >= CAST(? AS TIMESTAMP) AND r.timestamp < CAST(? AS TIMESTAMP))
|
|
3220
3247
|
AND bool_or(${rowConditions.join("\n AND ")})
|
|
3248
|
+
)`,
|
|
3249
|
+
`root_bounds AS (
|
|
3250
|
+
SELECT min(timestamp) AS minTs, max(timestamp) AS maxTs
|
|
3251
|
+
FROM span_events
|
|
3252
|
+
WHERE parentSpanId IS NULL AND traceId IN (SELECT traceId FROM root_trace_ids)
|
|
3221
3253
|
)`,
|
|
3222
3254
|
`root_events AS (
|
|
3223
3255
|
SELECT
|
|
@@ -3228,6 +3260,8 @@ function compileDuckDBTraceScope(relatedCollections, scope, timeRange, rootPredi
|
|
|
3228
3260
|
END AS startedAt
|
|
3229
3261
|
FROM span_events
|
|
3230
3262
|
WHERE parentSpanId IS NULL
|
|
3263
|
+
AND timestamp >= (SELECT minTs FROM root_bounds)
|
|
3264
|
+
AND timestamp <= (SELECT maxTs FROM root_bounds)
|
|
3231
3265
|
AND traceId IN (SELECT traceId FROM root_trace_ids)
|
|
3232
3266
|
)`,
|
|
3233
3267
|
`current_roots AS (
|
|
@@ -3250,7 +3284,15 @@ function compileDuckDBTraceScope(relatedCollections, scope, timeRange, rootPredi
|
|
|
3250
3284
|
)`
|
|
3251
3285
|
];
|
|
3252
3286
|
if (relatedCollections.has("spans")) {
|
|
3253
|
-
ctes.push(`
|
|
3287
|
+
ctes.push(`span_bounds AS (
|
|
3288
|
+
-- Exact event-time range of the in-scope traces, from a narrow
|
|
3289
|
+
-- (traceId, timestamp) pass. The join below cannot be pushed into the
|
|
3290
|
+
-- scan, so the range keeps it from decompressing every span row.
|
|
3291
|
+
SELECT min(timestamp) AS minTs, max(timestamp) AS maxTs
|
|
3292
|
+
FROM span_events
|
|
3293
|
+
WHERE traceId IN (SELECT traceId FROM root_scope)
|
|
3294
|
+
),
|
|
3295
|
+
current_span_rows AS (
|
|
3254
3296
|
SELECT
|
|
3255
3297
|
e.*,
|
|
3256
3298
|
coalesce(
|
|
@@ -3262,7 +3304,9 @@ function compileDuckDBTraceScope(relatedCollections, scope, timeRange, rootPredi
|
|
|
3262
3304
|
ORDER BY CASE WHEN e.endedAt IS NULL THEN 1 ELSE 0 END ASC, e.cursorId DESC
|
|
3263
3305
|
) AS currentRank
|
|
3264
3306
|
FROM span_events e
|
|
3265
|
-
INNER JOIN root_scope roots ON roots.traceId = e.traceId
|
|
3307
|
+
INNER JOIN root_scope roots ON roots.traceId = e.traceId
|
|
3308
|
+
WHERE e.timestamp >= (SELECT minTs FROM span_bounds)
|
|
3309
|
+
AND e.timestamp <= (SELECT maxTs FROM span_bounds)${tenantConditions("e").map((condition) => `\n AND ${condition}`).join("")}
|
|
3266
3310
|
),
|
|
3267
3311
|
current_spans AS (
|
|
3268
3312
|
SELECT
|
|
@@ -3812,7 +3856,7 @@ async function querySpans(db, plan) {
|
|
|
3812
3856
|
return `(${key} AND cursorId IS NOT DISTINCT FROM ? AND endedAt = ?)`;
|
|
3813
3857
|
}).join(" OR ");
|
|
3814
3858
|
const payloads = await query(`SELECT DISTINCT ${identities},
|
|
3815
|
-
substring(CAST(input AS VARCHAR), 1, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS inputPreview, substring(CAST(output AS VARCHAR), 1, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS outputPreview
|
|
3859
|
+
substring(CAST(${payloadColumnSql("input")} AS VARCHAR), 1, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS inputPreview, substring(CAST(${payloadColumnSql("output")} AS VARCHAR), 1, ${_mastra_core_storage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}) AS outputPreview
|
|
3816
3860
|
FROM span_events WHERE ${payloadWhere}`, payloadValues);
|
|
3817
3861
|
const metricValues = [];
|
|
3818
3862
|
const metricWhere = keys(page, metricValues);
|
|
@@ -3855,18 +3899,7 @@ const COMPARISON_SQL = {
|
|
|
3855
3899
|
gt: ">",
|
|
3856
3900
|
gte: ">="
|
|
3857
3901
|
};
|
|
3858
|
-
|
|
3859
|
-
* Compiles a trusted aggregate plan. The plan's caps are enforced by the planner and trusted here.
|
|
3860
|
-
*
|
|
3861
|
-
* Every candidate root is first reduced to a `facts` row (dimension values, duration, error flag,
|
|
3862
|
-
* countDistinct inputs, bucket) so grouping, measures, and joins only reference plain columns.
|
|
3863
|
-
* Without `interval`, groups are filtered, ordered, and limited in one grouped select. With
|
|
3864
|
-
* `interval`, a `ranked` CTE ranks groups on whole-window measures, then only the top `limit`
|
|
3865
|
-
* groups are expanded into per-bucket rows.
|
|
3866
|
-
*
|
|
3867
|
-
* Parameters are positional, so `values` is appended in the order placeholders appear in the text.
|
|
3868
|
-
*/
|
|
3869
|
-
function compileDuckDBTraceAggregate(plan) {
|
|
3902
|
+
function compileDuckDBTraceAggregate(plan, options = {}) {
|
|
3870
3903
|
const { ctes, values } = compileDuckDBTraceCandidates(plan, "r.*");
|
|
3871
3904
|
const dimensionSql = (field) => {
|
|
3872
3905
|
if (field.startsWith("metadata.")) return {
|
|
@@ -3906,10 +3939,31 @@ function compileDuckDBTraceAggregate(plan) {
|
|
|
3906
3939
|
if (intervalMs === void 0) throw new Error(`Unsupported trusted trace-aggregate interval: ${plan.interval}`);
|
|
3907
3940
|
factColumns.push(`epoch_ms((epoch_ms(r.startedAt) // ${intervalMs}) * ${intervalMs}) AS bucket`);
|
|
3908
3941
|
}
|
|
3909
|
-
|
|
3942
|
+
const measureKinds = new Set(plan.measures.map((measure) => measureRule(measure.name)?.kind));
|
|
3943
|
+
const hasCost = measureKinds.has("cost");
|
|
3944
|
+
if (!hasCost && !measureKinds.has("tokens")) ctes.push(`facts AS (
|
|
3910
3945
|
SELECT ${factColumns.join(",\n ")}
|
|
3911
3946
|
FROM candidates r
|
|
3912
3947
|
)`);
|
|
3948
|
+
else {
|
|
3949
|
+
ctes.push(`base_facts AS (
|
|
3950
|
+
SELECT r.traceId, ${factColumns.join(",\n ")}
|
|
3951
|
+
FROM candidates r
|
|
3952
|
+
)`);
|
|
3953
|
+
ctes.push(...compileUsageCtes(plan, values, {
|
|
3954
|
+
hasCost,
|
|
3955
|
+
dedupe: !options.metricIdsUnique
|
|
3956
|
+
}));
|
|
3957
|
+
const usageColumns = USAGE_METRIC_NAMES.map((_, index) => `CASE WHEN u.traceId IS NOT NULL THEN COALESCE(u.t${index}, 0) END AS t${index}`);
|
|
3958
|
+
usageColumns.push(`u.traceId IS NOT NULL AS usageBearing`);
|
|
3959
|
+
if (hasCost) usageColumns.push(`CASE WHEN u.priced THEN u.cost END AS traceCost`, `COALESCE(u.priced AND NOT u.pricingFailure, FALSE) AS covered`, `u.unitMin AS unitMin`, `u.unitMax AS unitMax`);
|
|
3960
|
+
ctes.push(`facts AS (
|
|
3961
|
+
SELECT b.*,
|
|
3962
|
+
${usageColumns.join(",\n ")}
|
|
3963
|
+
FROM base_facts b
|
|
3964
|
+
LEFT JOIN usage u ON u.traceId = b.traceId
|
|
3965
|
+
)`);
|
|
3966
|
+
}
|
|
3913
3967
|
const measureSql = (name, alias = "") => {
|
|
3914
3968
|
const column = (name) => `${alias}${name}`;
|
|
3915
3969
|
if (name === "count") return "CAST(count(*) AS DOUBLE)";
|
|
@@ -3920,6 +3974,15 @@ function compileDuckDBTraceAggregate(plan) {
|
|
|
3920
3974
|
if (name === "duration.max") return `max(${column("durationMs")})`;
|
|
3921
3975
|
const percentile = PERCENTILES[name];
|
|
3922
3976
|
if (percentile !== void 0) return `quantile_cont(${column("durationMs")}, ${percentile})`;
|
|
3977
|
+
const rule = measureRule(name);
|
|
3978
|
+
if (rule?.kind === "tokens" && rule.metricNames) {
|
|
3979
|
+
const value = rule.metricNames.map((metricName) => column(`t${usageMetricIndex(metricName)}`)).join(" + ");
|
|
3980
|
+
return `CAST(${rule.statistic === "avg" ? "avg" : "sum"}(${value}) AS DOUBLE)`;
|
|
3981
|
+
}
|
|
3982
|
+
if (rule?.kind === "cost") {
|
|
3983
|
+
const aggregate = rule.statistic === "avg" ? "avg" : "sum";
|
|
3984
|
+
return `CASE WHEN ${mixedUnitsSql(alias)} THEN NULL ELSE CAST(${aggregate}(${column("traceCost")}) AS DOUBLE) END`;
|
|
3985
|
+
}
|
|
3923
3986
|
const measure = plan.measures.find((candidate) => candidate.type === "countDistinct" && candidate.name === name);
|
|
3924
3987
|
const distinctColumn = measure && distinctColumns.get(measure.field);
|
|
3925
3988
|
if (distinctColumn === void 0) throw new Error(`Unsupported trusted trace-aggregate measure: ${name}`);
|
|
@@ -3953,7 +4016,11 @@ function compileDuckDBTraceAggregate(plan) {
|
|
|
3953
4016
|
const orderSql = [`${orderTarget} ${direction} NULLS LAST`, ...dimensionColumns.map((column) => `${column} ASC NULLS LAST`)].join(", ");
|
|
3954
4017
|
const groupSql = dimensionColumns.length > 0 ? `\nGROUP BY ${dimensionColumns.join(", ")}` : "";
|
|
3955
4018
|
if (plan.interval === void 0) {
|
|
3956
|
-
const projected = [
|
|
4019
|
+
const projected = [
|
|
4020
|
+
...dimensionColumns,
|
|
4021
|
+
...plan.measures.map((measure, index) => `${measureSql(measure.name)} AS m${index}`),
|
|
4022
|
+
...hasCost ? costColumns("") : []
|
|
4023
|
+
];
|
|
3957
4024
|
values.push(...havingValues, plan.limit + 1);
|
|
3958
4025
|
return {
|
|
3959
4026
|
sql: `WITH ${ctes.join(",\n ")}
|
|
@@ -3978,6 +4045,7 @@ LIMIT ?`,
|
|
|
3978
4045
|
...dimensionColumns.map((column) => `f.${column}`),
|
|
3979
4046
|
"f.bucket",
|
|
3980
4047
|
...plan.measures.map((measure, index) => `${measureSql(measure.name, "f.")} AS m${index}`),
|
|
4048
|
+
...hasCost ? costColumns("f.") : [],
|
|
3981
4049
|
`(SELECT count(*) FROM ranked) > ? AS truncated`
|
|
3982
4050
|
];
|
|
3983
4051
|
values.push(plan.limit, plan.limit);
|
|
@@ -3996,8 +4064,97 @@ ORDER BY g.rank ASC, f.bucket ASC`,
|
|
|
3996
4064
|
values
|
|
3997
4065
|
};
|
|
3998
4066
|
}
|
|
3999
|
-
|
|
4000
|
-
|
|
4067
|
+
const USAGE_METRIC_NAMES = _mastra_core_storage.TRACE_AGGREGATE_USAGE_METRIC_NAMES;
|
|
4068
|
+
function measureRule(name) {
|
|
4069
|
+
return _mastra_core_storage.isTraceAggregateCanonicalMeasure(name) ? _mastra_core_storage.TRACE_AGGREGATE_MEASURE_REGISTRY[name] : void 0;
|
|
4070
|
+
}
|
|
4071
|
+
function usageMetricIndex(name) {
|
|
4072
|
+
const index = USAGE_METRIC_NAMES.indexOf(name);
|
|
4073
|
+
if (index === -1) throw new Error(`Unsupported trusted trace-aggregate token metric: ${name}`);
|
|
4074
|
+
return index;
|
|
4075
|
+
}
|
|
4076
|
+
/** More than one distinct priced unit in the group; NULL (treated as false) when nothing is priced. */
|
|
4077
|
+
function mixedUnitsSql(alias) {
|
|
4078
|
+
return `min(${alias}unitMin) <> max(${alias}unitMax)`;
|
|
4079
|
+
}
|
|
4080
|
+
/** Row cost fields, projected only for cost requests. */
|
|
4081
|
+
function costColumns(alias) {
|
|
4082
|
+
const mixed = _mastra_core_storage.TRACE_AGGREGATE_MIXED_COST_UNIT.replaceAll("'", "''");
|
|
4083
|
+
return [`CAST(count_if(${alias}covered) AS DOUBLE) / NULLIF(count_if(${alias}usageBearing), 0) AS costCoverage`, `CASE WHEN ${mixedUnitsSql(alias)} THEN '${mixed}' ELSE min(${alias}unitMin) END AS costUnit`];
|
|
4084
|
+
}
|
|
4085
|
+
/**
|
|
4086
|
+
* Builds `usage_bounds`, `usage_rows`, and `usage`: one row per candidate trace that has at least
|
|
4087
|
+
* one token metric row, holding per-name token sums (`t<i>`, indexed by
|
|
4088
|
+
* `TRACE_AGGREGATE_USAGE_METRIC_NAMES`) and, for cost requests, the trace's pricing state. Metric
|
|
4089
|
+
* rows are matched by traceId, usage metric name, tenant scope, and `timestamp >= from` only: a
|
|
4090
|
+
* trace that starts in the window can emit metrics after `to`, and the semi-join bounds the scan.
|
|
4091
|
+
*
|
|
4092
|
+
* DuckDB cannot push the `traceId IN (...)` semi-join into the table scan, so `usage_bounds`
|
|
4093
|
+
* first reads the exact timestamp range of the qualifying rows from narrow columns, and
|
|
4094
|
+
* `usage_rows` decompresses the wide columns only inside that range.
|
|
4095
|
+
*
|
|
4096
|
+
* Without the `metricId` primary key, retried copies are collapsed with `DISTINCT ON`; copies are
|
|
4097
|
+
* identical, so whichever survives is the same.
|
|
4098
|
+
*
|
|
4099
|
+
* Placeholders are bound in text order: `param` pushes each value as its placeholder is emitted.
|
|
4100
|
+
*/
|
|
4101
|
+
function compileUsageCtes(plan, values, { hasCost, dedupe }) {
|
|
4102
|
+
const param = (value) => {
|
|
4103
|
+
values.push(value);
|
|
4104
|
+
return "?";
|
|
4105
|
+
};
|
|
4106
|
+
const list = (items) => items.map(param).join(", ");
|
|
4107
|
+
const scope = (alias) => {
|
|
4108
|
+
if (!plan.scope) return "";
|
|
4109
|
+
let sql = `\n AND ${alias}organizationId = ${param(plan.scope.organizationId)}`;
|
|
4110
|
+
if (plan.scope.resourceId !== void 0) sql += `\n AND ${alias}resourceId = ${param(plan.scope.resourceId)}`;
|
|
4111
|
+
return sql;
|
|
4112
|
+
};
|
|
4113
|
+
const costNames = _mastra_core_storage.TRACE_AGGREGATE_COST_METRIC_NAMES;
|
|
4114
|
+
const bounds = `usage_bounds AS (
|
|
4115
|
+
SELECT min(timestamp) AS minTs, max(timestamp) AS maxTs
|
|
4116
|
+
FROM metric_events
|
|
4117
|
+
WHERE traceId IN (SELECT traceId FROM base_facts)
|
|
4118
|
+
AND name IN (${list(USAGE_METRIC_NAMES)})
|
|
4119
|
+
AND timestamp >= CAST(${param(plan.timeRange.from)} AS TIMESTAMP)${scope("")}
|
|
4120
|
+
)`;
|
|
4121
|
+
const costSelect = hasCost ? `,
|
|
4122
|
+
m.estimatedCost,
|
|
4123
|
+
m.costUnit,
|
|
4124
|
+
m.name IN (${list(costNames)}) AS costRow,
|
|
4125
|
+
CASE WHEN m.name IN (${list(costNames)})
|
|
4126
|
+
THEN COALESCE(json_type(m.costMetadata, '$.error'), 'NULL') <> 'NULL'
|
|
4127
|
+
ELSE FALSE
|
|
4128
|
+
END AS hasError` : "";
|
|
4129
|
+
const rows = `usage_rows AS (
|
|
4130
|
+
SELECT ${dedupe ? "DISTINCT ON (m.metricId) " : ""}m.traceId, m.name, m.value${costSelect}
|
|
4131
|
+
FROM metric_events m
|
|
4132
|
+
WHERE m.timestamp >= (SELECT minTs FROM usage_bounds)
|
|
4133
|
+
AND m.timestamp <= (SELECT maxTs FROM usage_bounds)
|
|
4134
|
+
AND m.traceId IN (SELECT traceId FROM base_facts)
|
|
4135
|
+
AND m.name IN (${list(USAGE_METRIC_NAMES)})${scope("m.")}
|
|
4136
|
+
)`;
|
|
4137
|
+
const columns = USAGE_METRIC_NAMES.map((name) => `sum(value) FILTER (WHERE name = ${param(name)})`).map((sql, index) => `${sql} AS t${index}`);
|
|
4138
|
+
if (hasCost) {
|
|
4139
|
+
const priced = "costRow AND estimatedCost IS NOT NULL AND costUnit IS NOT NULL AND NOT hasError";
|
|
4140
|
+
columns.push(`sum(estimatedCost) FILTER (WHERE ${priced}) AS cost`, `bool_or(${priced}) AS priced`, `bool_or(costRow AND (hasError OR (estimatedCost IS NOT NULL AND costUnit IS NULL))) AS pricingFailure`, `min(costUnit) FILTER (WHERE ${priced}) AS unitMin`, `max(costUnit) FILTER (WHERE ${priced}) AS unitMax`);
|
|
4141
|
+
}
|
|
4142
|
+
return [
|
|
4143
|
+
bounds,
|
|
4144
|
+
rows,
|
|
4145
|
+
`usage AS (
|
|
4146
|
+
SELECT traceId,
|
|
4147
|
+
${columns.join(",\n ")}
|
|
4148
|
+
FROM usage_rows
|
|
4149
|
+
GROUP BY traceId
|
|
4150
|
+
)`
|
|
4151
|
+
];
|
|
4152
|
+
}
|
|
4153
|
+
function nullableNumber(value) {
|
|
4154
|
+
return value === null || value === void 0 ? null : Number(value);
|
|
4155
|
+
}
|
|
4156
|
+
async function aggregateTraces(db, plan, options = {}) {
|
|
4157
|
+
const compiled = compileDuckDBTraceAggregate(plan, options);
|
|
4001
4158
|
let result;
|
|
4002
4159
|
try {
|
|
4003
4160
|
result = await db.query(compiled.sql, compiled.values);
|
|
@@ -4016,7 +4173,11 @@ async function aggregateTraces(db, plan) {
|
|
|
4016
4173
|
}
|
|
4017
4174
|
return {
|
|
4018
4175
|
rows: resultRows.map((row) => {
|
|
4019
|
-
const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name,
|
|
4176
|
+
const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, nullableNumber(row[`m${index}`])])) };
|
|
4177
|
+
if ("costCoverage" in row) shaped.cost = {
|
|
4178
|
+
coverage: nullableNumber(row.costCoverage),
|
|
4179
|
+
unit: row.costUnit ?? null
|
|
4180
|
+
};
|
|
4020
4181
|
if (plan.dimensions.length > 0) shaped.dimensions = Object.fromEntries(plan.dimensions.map((dimension, index) => {
|
|
4021
4182
|
const value = row[`d${index}`];
|
|
4022
4183
|
return [dimension, value == null ? null : String(value)];
|
|
@@ -4067,13 +4228,15 @@ const COLUMNS_SQL = [
|
|
|
4067
4228
|
"error",
|
|
4068
4229
|
"requestContext"
|
|
4069
4230
|
].join(", ");
|
|
4231
|
+
const PAYLOAD_COLUMN_SET = new Set(SPAN_PAYLOAD_COLUMNS);
|
|
4070
4232
|
/**
|
|
4071
4233
|
* Reconstruction query uses `arg_max(field, timestamp) FILTER (WHERE field IS NOT NULL)`
|
|
4072
4234
|
* so that the final end event supplies the terminal span fields without wiping
|
|
4073
|
-
* stable values emitted on the start event.
|
|
4235
|
+
* stable values emitted on the start event. Payload columns also skip the JSON
|
|
4236
|
+
* `null` placeholder, which stands in for a missing value.
|
|
4074
4237
|
*/
|
|
4075
4238
|
function argMaxNonNull(col) {
|
|
4076
|
-
return `arg_max(${col}, timestamp) FILTER (WHERE ${col} IS NOT NULL) as ${col}`;
|
|
4239
|
+
return `arg_max(${col}, timestamp) FILTER (WHERE ${col} IS NOT NULL${PAYLOAD_COLUMN_SET.has(col) ? ` AND ${col} <> '${JSON_NULL_PLACEHOLDER}'` : ""}) as ${col}`;
|
|
4077
4240
|
}
|
|
4078
4241
|
const SPAN_RECONSTRUCT_SELECT = `
|
|
4079
4242
|
SELECT
|
|
@@ -4184,28 +4347,55 @@ function buildPostAggReconstructSelect(postAgg, orderByField) {
|
|
|
4184
4347
|
`;
|
|
4185
4348
|
}
|
|
4186
4349
|
/**
|
|
4187
|
-
* Reconstruct spans
|
|
4188
|
-
*
|
|
4350
|
+
* Reconstruct the spans of one list page, reading payload columns only for
|
|
4351
|
+
* the page's own events.
|
|
4352
|
+
*
|
|
4353
|
+
* Two queries:
|
|
4354
|
+
* 1. `anchorSql` (a `WITH ...` prefix defining `anchorCte`) picks the page's
|
|
4355
|
+
* `(traceId, spanId)` anchors, and a narrow pass over `traceId`, `spanId`
|
|
4356
|
+
* and `timestamp` finds the exact time range of their events.
|
|
4357
|
+
* 2. Reconstruction scans only that range for exactly those anchors.
|
|
4189
4358
|
*
|
|
4190
|
-
* The `(traceId, spanId) IN (
|
|
4191
|
-
*
|
|
4192
|
-
*
|
|
4193
|
-
*
|
|
4359
|
+
* The `(traceId, spanId) IN (...)` semi-join cannot be pushed into the table
|
|
4360
|
+
* scan, so without a range DuckDB decompresses every payload column of every
|
|
4361
|
+
* row it reads. A lower bound alone (the earliest anchor) prunes nothing for
|
|
4362
|
+
* the oldest pages, which then read the whole table and can run out of memory.
|
|
4363
|
+
* Both bounds are passed as literal parameters so they always reach the scan
|
|
4364
|
+
* as plain range filters that zone maps can prune on.
|
|
4194
4365
|
*
|
|
4195
|
-
*
|
|
4196
|
-
*
|
|
4197
|
-
*
|
|
4198
|
-
*
|
|
4366
|
+
* Anchors and bounds are frozen after the first query, so spans written in
|
|
4367
|
+
* between cannot change which rows are on the page. An event a page span
|
|
4368
|
+
* receives in between is left for the next read, as if the page had been
|
|
4369
|
+
* read a moment earlier.
|
|
4199
4370
|
*/
|
|
4200
|
-
function
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4371
|
+
async function reconstructAnchoredPage(db, anchorSql, anchorCte, params, reconstructSelect, orderBySql) {
|
|
4372
|
+
const anchors = await db.query(`
|
|
4373
|
+
${anchorSql}
|
|
4374
|
+
SELECT anchors.traceId, anchors.spanId, bounds.minTs, bounds.maxTs
|
|
4375
|
+
FROM ${anchorCte} AS anchors,
|
|
4376
|
+
(
|
|
4377
|
+
SELECT min(timestamp) AS minTs, max(timestamp) AS maxTs
|
|
4378
|
+
FROM span_events
|
|
4379
|
+
WHERE (traceId, spanId) IN (SELECT traceId, spanId FROM ${anchorCte})
|
|
4380
|
+
) AS bounds
|
|
4381
|
+
`, params);
|
|
4382
|
+
if (anchors.length === 0) return [];
|
|
4383
|
+
const { minTs, maxTs } = anchors[0];
|
|
4384
|
+
const anchorValues = anchors.map(() => "(?, ?)").join(", ");
|
|
4385
|
+
return db.query(`
|
|
4386
|
+
${reconstructSelect}
|
|
4387
|
+
WHERE timestamp >= ? AND timestamp <= ?
|
|
4388
|
+
AND (traceId, spanId) IN (SELECT * FROM (VALUES ${anchorValues}))
|
|
4389
|
+
GROUP BY traceId, spanId
|
|
4390
|
+
${orderBySql}
|
|
4391
|
+
`, [
|
|
4392
|
+
minTs,
|
|
4393
|
+
maxTs,
|
|
4394
|
+
...anchors.flatMap((anchor) => [anchor.traceId, anchor.spanId])
|
|
4395
|
+
]);
|
|
4206
4396
|
}
|
|
4207
4397
|
/**
|
|
4208
|
-
* Same time-bound trick as {@link
|
|
4398
|
+
* Same time-bound trick as {@link reconstructAnchoredPage}, but with the bound
|
|
4209
4399
|
* precomputed in JS and passed as a `?` parameter. The delta-poll query shape
|
|
4210
4400
|
* references its candidate CTE multiple times, which makes DuckDB materialize
|
|
4211
4401
|
* it and lose the dynamic-filter pushdown a scalar subquery bound relies on —
|
|
@@ -4439,15 +4629,15 @@ function toValuesTuple(row) {
|
|
|
4439
4629
|
v(row.environment),
|
|
4440
4630
|
v(row.source),
|
|
4441
4631
|
v(row.serviceName),
|
|
4442
|
-
|
|
4632
|
+
payloadJsonV(row.attributes),
|
|
4443
4633
|
jsonV(row.metadata),
|
|
4444
4634
|
jsonV(row.tags),
|
|
4445
4635
|
jsonV(row.scope),
|
|
4446
4636
|
jsonV(row.links),
|
|
4447
|
-
|
|
4448
|
-
|
|
4637
|
+
payloadJsonV(row.input),
|
|
4638
|
+
payloadJsonV(row.output),
|
|
4449
4639
|
jsonV(row.error),
|
|
4450
|
-
|
|
4640
|
+
payloadJsonV(row.requestContext)
|
|
4451
4641
|
].join(", ");
|
|
4452
4642
|
}
|
|
4453
4643
|
async function insertSpanEvents(db, rows) {
|
|
@@ -4661,7 +4851,7 @@ async function listTraceRows(db, args, reconstructSelect, mapRow, toSpans) {
|
|
|
4661
4851
|
},
|
|
4662
4852
|
spans: []
|
|
4663
4853
|
};
|
|
4664
|
-
const
|
|
4854
|
+
const spans = (await reconstructAnchoredPage(db, `
|
|
4665
4855
|
WITH page_roots AS (
|
|
4666
4856
|
SELECT traceId, spanId, min(timestamp) AS anchorStartedAt
|
|
4667
4857
|
FROM span_events AS ${outerAlias}
|
|
@@ -4670,14 +4860,11 @@ async function listTraceRows(db, args, reconstructSelect, mapRow, toSpans) {
|
|
|
4670
4860
|
${prefilterOrderBy}
|
|
4671
4861
|
LIMIT ? OFFSET ?
|
|
4672
4862
|
)
|
|
4673
|
-
|
|
4674
|
-
${buildOrderByClause(orderBy)}, traceId, spanId
|
|
4675
|
-
`;
|
|
4676
|
-
const spans = (await db.query(pageSql, [
|
|
4863
|
+
`, "page_roots", [
|
|
4677
4864
|
...prefilterParams,
|
|
4678
4865
|
perPage,
|
|
4679
4866
|
offset
|
|
4680
|
-
])).map((row) => mapRow(row));
|
|
4867
|
+
], reconstructSelect, `${buildOrderByClause(orderBy)}, traceId, spanId`)).map((row) => mapRow(row));
|
|
4681
4868
|
return {
|
|
4682
4869
|
pagination: {
|
|
4683
4870
|
total,
|
|
@@ -4726,20 +4913,17 @@ async function listTraceRows(db, args, reconstructSelect, mapRow, toSpans) {
|
|
|
4726
4913
|
},
|
|
4727
4914
|
spans: []
|
|
4728
4915
|
};
|
|
4729
|
-
const
|
|
4916
|
+
const spans = (await reconstructAnchoredPage(db, `
|
|
4730
4917
|
${cteSql},
|
|
4731
4918
|
page_roots AS (
|
|
4732
4919
|
SELECT traceId, spanId, startedAt AS anchorStartedAt
|
|
4733
4920
|
FROM root_spans ${postAggWhere} ${orderByClause} ${paginationClause}
|
|
4734
4921
|
)
|
|
4735
|
-
|
|
4736
|
-
${orderByClause}
|
|
4737
|
-
`;
|
|
4738
|
-
const spans = (await db.query(dataSql, [
|
|
4922
|
+
`, "page_roots", [
|
|
4739
4923
|
...prefilterParams,
|
|
4740
4924
|
...postAggParams,
|
|
4741
4925
|
...paginationParams
|
|
4742
|
-
])).map((row) => mapRow(row));
|
|
4926
|
+
], reconstructSelect, orderByClause)).map((row) => mapRow(row));
|
|
4743
4927
|
return {
|
|
4744
4928
|
pagination: {
|
|
4745
4929
|
total,
|
|
@@ -5043,7 +5227,7 @@ async function listBranches(db, args) {
|
|
|
5043
5227
|
branches: [],
|
|
5044
5228
|
...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
|
|
5045
5229
|
};
|
|
5046
|
-
const
|
|
5230
|
+
const spans = (await reconstructAnchoredPage(db, `
|
|
5047
5231
|
WITH page_anchors AS (
|
|
5048
5232
|
SELECT traceId, spanId, min(timestamp) AS anchorStartedAt
|
|
5049
5233
|
FROM span_events AS ${outerAlias}
|
|
@@ -5052,14 +5236,11 @@ async function listBranches(db, args) {
|
|
|
5052
5236
|
${prefilterOrderBy}
|
|
5053
5237
|
LIMIT ? OFFSET ?
|
|
5054
5238
|
)
|
|
5055
|
-
|
|
5056
|
-
${buildOrderByClause(orderBy)}, traceId, spanId
|
|
5057
|
-
`;
|
|
5058
|
-
const spans = (await db.query(pageSql, [
|
|
5239
|
+
`, "page_anchors", [
|
|
5059
5240
|
...prefilterParams,
|
|
5060
5241
|
perPage,
|
|
5061
5242
|
offset
|
|
5062
|
-
])).map((row) => rowToSpanRecord(row));
|
|
5243
|
+
], SPAN_RECONSTRUCT_SELECT, `${buildOrderByClause(orderBy)}, traceId, spanId`)).map((row) => rowToSpanRecord(row));
|
|
5063
5244
|
return {
|
|
5064
5245
|
pagination: {
|
|
5065
5246
|
total,
|
|
@@ -5106,20 +5287,17 @@ async function listBranches(db, args) {
|
|
|
5106
5287
|
branches: [],
|
|
5107
5288
|
...deltaPollingFeatureEnabled() ? { deltaCursor: currentDeltaCursor } : {}
|
|
5108
5289
|
};
|
|
5109
|
-
const
|
|
5290
|
+
const spans = (await reconstructAnchoredPage(db, `
|
|
5110
5291
|
${cteSql},
|
|
5111
5292
|
page_anchors AS (
|
|
5112
5293
|
SELECT traceId, spanId, startedAt AS anchorStartedAt
|
|
5113
5294
|
FROM branch_anchors ${postAggWhere} ${orderByClause} ${paginationClause}
|
|
5114
5295
|
)
|
|
5115
|
-
|
|
5116
|
-
${orderByClause}
|
|
5117
|
-
`;
|
|
5118
|
-
const spans = (await db.query(dataSql, [
|
|
5296
|
+
`, "page_anchors", [
|
|
5119
5297
|
...prefilterParams,
|
|
5120
5298
|
...postAggParams,
|
|
5121
5299
|
...paginationParams
|
|
5122
|
-
])).map((row) => rowToSpanRecord(row));
|
|
5300
|
+
], SPAN_RECONSTRUCT_SELECT, orderByClause)).map((row) => rowToSpanRecord(row));
|
|
5123
5301
|
return {
|
|
5124
5302
|
pagination: {
|
|
5125
5303
|
total,
|
|
@@ -5270,6 +5448,8 @@ var ObservabilityStorageDuckDB = class ObservabilityStorageDuckDB extends _mastr
|
|
|
5270
5448
|
indexed: false
|
|
5271
5449
|
}
|
|
5272
5450
|
};
|
|
5451
|
+
/** Set by `init()`; until then aggregate queries dedupe metric rows on `metricId`. */
|
|
5452
|
+
metricIdsUnique = false;
|
|
5273
5453
|
db;
|
|
5274
5454
|
constructor(config) {
|
|
5275
5455
|
super();
|
|
@@ -5306,6 +5486,7 @@ var ObservabilityStorageDuckDB = class ObservabilityStorageDuckDB extends _mastr
|
|
|
5306
5486
|
});
|
|
5307
5487
|
await this.db.executeBatch([...ALL_DDL, ...ALL_MIGRATIONS]);
|
|
5308
5488
|
await dropLegacyCursorIdDefaults(this.db);
|
|
5489
|
+
this.metricIdsUnique = await hasPrimaryKey(this.db, "metric_events");
|
|
5309
5490
|
}
|
|
5310
5491
|
/**
|
|
5311
5492
|
* Manually migrate legacy signal tables to the signal-ID primary-key schema.
|
|
@@ -5419,7 +5600,7 @@ var ObservabilityStorageDuckDB = class ObservabilityStorageDuckDB extends _mastr
|
|
|
5419
5600
|
return queryTraces(this.db, plan);
|
|
5420
5601
|
}
|
|
5421
5602
|
async aggregateTraces(plan) {
|
|
5422
|
-
return aggregateTraces(this.db, plan);
|
|
5603
|
+
return aggregateTraces(this.db, plan, { metricIdsUnique: this.metricIdsUnique });
|
|
5423
5604
|
}
|
|
5424
5605
|
async getTraceQueryObservedFields(plan) {
|
|
5425
5606
|
return getTraceQueryObservedFields(this.db, plan);
|
|
@@ -5543,4 +5724,4 @@ var ObservabilityStorageDuckDB = class ObservabilityStorageDuckDB extends _mastr
|
|
|
5543
5724
|
//#endregion
|
|
5544
5725
|
exports.ObservabilityStorageDuckDB = ObservabilityStorageDuckDB;
|
|
5545
5726
|
|
|
5546
|
-
//# sourceMappingURL=observability-
|
|
5727
|
+
//# sourceMappingURL=observability-DBm19qBS.cjs.map
|