@jeffjassky/telemetry 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/cli.cjs +12 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +12 -1
- package/dist/cli.js.map +1 -1
- package/dist/core.cjs +12 -1
- package/dist/core.cjs.map +1 -1
- package/dist/core.js +12 -1
- package/dist/core.js.map +1 -1
- package/dist/electron.cjs +12 -1
- package/dist/electron.cjs.map +1 -1
- package/dist/electron.js +12 -1
- package/dist/electron.js.map +1 -1
- package/dist/index.cjs +1437 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1423 -64
- package/dist/index.js.map +1 -1
- package/dist/mcp.cjs +1468 -78
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js +1468 -78
- package/dist/mcp.js.map +1 -1
- package/dist/react.cjs +12 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +12 -1
- package/dist/react.js.map +1 -1
- package/dist/ui/_assets/{index-CXiDp_v6.css → index-COswHpSX.css} +1 -1
- package/dist/ui/_assets/index-Deqhu-hT.js +41 -0
- package/dist/ui/_assets/index-Deqhu-hT.js.map +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/vue.cjs +12 -1
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +12 -1
- package/dist/vue.js.map +1 -1
- package/dist/web.cjs +34 -2
- package/dist/web.cjs.map +1 -1
- package/dist/web.js +34 -3
- package/dist/web.js.map +1 -1
- package/package.json +1 -1
- package/types/core.d.ts +6 -0
- package/types/index.d.ts +484 -11
- package/types/test-d.ts +173 -2
- package/types/web.d.ts +8 -0
- package/dist/ui/_assets/index-3jcToTuP.js +0 -41
- package/dist/ui/_assets/index-3jcToTuP.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -59,8 +59,24 @@ var newCounters = () => ({
|
|
|
59
59
|
capped: 0,
|
|
60
60
|
rollupSkipped: 0,
|
|
61
61
|
deduped: 0,
|
|
62
|
-
truncated: 0
|
|
62
|
+
truncated: 0,
|
|
63
|
+
rollupSkippedBy: {},
|
|
64
|
+
undeclaredAttrs: {}
|
|
63
65
|
});
|
|
66
|
+
var COUNTER_MAP_MAX = 1e3;
|
|
67
|
+
var COUNTER_OVERFLOW_KEY = "(other)|(other)";
|
|
68
|
+
var bumpCounterMap = (map, key) => {
|
|
69
|
+
const seen = map[key];
|
|
70
|
+
if (seen !== void 0) {
|
|
71
|
+
map[key] = seen + 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (map[COUNTER_OVERFLOW_KEY] !== void 0 || Object.keys(map).length >= COUNTER_MAP_MAX) {
|
|
75
|
+
map[COUNTER_OVERFLOW_KEY] = (map[COUNTER_OVERFLOW_KEY] ?? 0) + 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
map[key] = 1;
|
|
79
|
+
};
|
|
64
80
|
var traceKeep = (traceId, rate) => {
|
|
65
81
|
if (rate >= 1) return true;
|
|
66
82
|
if (!traceId) return Math.random() < rate;
|
|
@@ -388,15 +404,15 @@ function buildBaseSchema(collection, registry, counters, opts) {
|
|
|
388
404
|
if (this.kind === TelemetryKind.State && !this.state?.to) {
|
|
389
405
|
throw new Error("telemetry: state requires state.to");
|
|
390
406
|
}
|
|
391
|
-
const check = (
|
|
407
|
+
const check = (label3, m, zschema) => {
|
|
392
408
|
const obj = Object.fromEntries(m ?? []);
|
|
393
409
|
if (!zschema) {
|
|
394
|
-
if (Object.keys(obj).length) throw new Error(`telemetry: "${this.name}" declares no ${
|
|
410
|
+
if (Object.keys(obj).length) throw new Error(`telemetry: "${this.name}" declares no ${label3}`);
|
|
395
411
|
return;
|
|
396
412
|
}
|
|
397
413
|
const s = zschema.strict?.() ?? zschema;
|
|
398
414
|
const r = s.safeParse(obj);
|
|
399
|
-
if (!r.success) throw new Error(`telemetry: ${
|
|
415
|
+
if (!r.success) throw new Error(`telemetry: ${label3} invalid for "${this.name}": ${r.error.message}`);
|
|
400
416
|
};
|
|
401
417
|
check("attrs", this.attrs, spec.attrs);
|
|
402
418
|
check("metrics", this.metrics, spec.metrics);
|
|
@@ -545,6 +561,7 @@ async function recordRollup(RollupModel, doc, name, spec, counters) {
|
|
|
545
561
|
if (v == null || v === "") {
|
|
546
562
|
if (spec.dimDefault === void 0) {
|
|
547
563
|
counters.rollupSkipped++;
|
|
564
|
+
bumpCounterMap(counters.rollupSkippedBy, `${as}|${label(src)}`);
|
|
548
565
|
return;
|
|
549
566
|
}
|
|
550
567
|
v = spec.dimDefault;
|
|
@@ -636,6 +653,16 @@ function createCheckpointFactory(CheckpointModel, logger) {
|
|
|
636
653
|
}
|
|
637
654
|
|
|
638
655
|
// src/server/emit.ts
|
|
656
|
+
function noteUndeclaredAttrs(counters, name, spec, attrs) {
|
|
657
|
+
if (!attrs || typeof attrs !== "object") return;
|
|
658
|
+
const keys = attrs instanceof Map ? [...attrs.keys()] : Object.keys(attrs);
|
|
659
|
+
const shape = spec.attrs?.shape;
|
|
660
|
+
for (const raw of keys) {
|
|
661
|
+
const key = String(raw).replace(/\./g, "_");
|
|
662
|
+
if (shape && Object.prototype.hasOwnProperty.call(shape, key)) continue;
|
|
663
|
+
bumpCounterMap(counters.undeclaredAttrs, `${name}|${key}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
639
666
|
function createEmitter(ctx) {
|
|
640
667
|
const { registry, byKind, RollupModel, rejects, counters } = ctx;
|
|
641
668
|
const burstBuckets = /* @__PURE__ */ new Map();
|
|
@@ -682,6 +709,7 @@ function createEmitter(ctx) {
|
|
|
682
709
|
attrs: safe(doc.attrs),
|
|
683
710
|
metrics: safe(doc.metrics)
|
|
684
711
|
};
|
|
712
|
+
noteUndeclaredAttrs(counters, name, spec, doc.attrs);
|
|
685
713
|
const onFail = async (e) => {
|
|
686
714
|
counters.rejected++;
|
|
687
715
|
await rejects().insertOne({ at: /* @__PURE__ */ new Date(), name, reason: String(e), raw: plain(doc) }).catch(() => {
|
|
@@ -887,10 +915,10 @@ function parseKeyString(raw) {
|
|
|
887
915
|
if (!raw) return null;
|
|
888
916
|
const m = KEY_RE.exec(raw.trim());
|
|
889
917
|
if (!m) return null;
|
|
890
|
-
const [, prefix,
|
|
918
|
+
const [, prefix, label3, id, secret] = m;
|
|
891
919
|
if (prefix === "sk" && !secret) return null;
|
|
892
920
|
if (prefix === "pk" && secret) return null;
|
|
893
|
-
return { kind: prefix === "pk" ? KeyKind.Publishable : KeyKind.Secret, label:
|
|
921
|
+
return { kind: prefix === "pk" ? KeyKind.Publishable : KeyKind.Secret, label: label3, id, secret };
|
|
894
922
|
}
|
|
895
923
|
var SCRYPT_VERSION = "scrypt1";
|
|
896
924
|
var SCRYPT = { N: 16384, r: 8, p: 1, keylen: 32 };
|
|
@@ -948,7 +976,7 @@ async function createKey(KeyModel, input) {
|
|
|
948
976
|
tenantId,
|
|
949
977
|
service,
|
|
950
978
|
env,
|
|
951
|
-
label:
|
|
979
|
+
label: label3 = "live",
|
|
952
980
|
origins = [],
|
|
953
981
|
allowedNames,
|
|
954
982
|
maxPerMinute = 600
|
|
@@ -981,7 +1009,7 @@ async function createKey(KeyModel, input) {
|
|
|
981
1009
|
createdAt: /* @__PURE__ */ new Date()
|
|
982
1010
|
});
|
|
983
1011
|
const prefix = kind === KeyKind.Publishable ? "pk" : "sk";
|
|
984
|
-
return { key: secret ? `${prefix}_${
|
|
1012
|
+
return { key: secret ? `${prefix}_${label3}_${id}_${secret}` : `${prefix}_${label3}_${id}`, id };
|
|
985
1013
|
}
|
|
986
1014
|
var BATCH_MAX = 100;
|
|
987
1015
|
function createIngest(opts) {
|
|
@@ -1183,6 +1211,7 @@ function createIngest(opts) {
|
|
|
1183
1211
|
const occurredRaw = rec.occurredAt ? Date.parse(rec.occurredAt) : NaN;
|
|
1184
1212
|
const occurredAt = Number.isFinite(occurredRaw) ? new Date(occurredRaw - clockSkewMs) : receivedAt;
|
|
1185
1213
|
const safeMap = (o) => o && typeof o === "object" ? new Map(Object.entries(o).map(([k, v]) => [k.replace(/\./g, "_"), v])) : /* @__PURE__ */ new Map();
|
|
1214
|
+
noteUndeclaredAttrs(t.counters, name, spec, rec.attrs);
|
|
1186
1215
|
const Model = t.models.byKind[spec.kind];
|
|
1187
1216
|
const d = new Model({
|
|
1188
1217
|
// facts the wire may not assert: tenant, service, env, origin, plane
|
|
@@ -1253,6 +1282,292 @@ function createIngest(opts) {
|
|
|
1253
1282
|
return router;
|
|
1254
1283
|
}
|
|
1255
1284
|
|
|
1285
|
+
// src/server/catalog.ts
|
|
1286
|
+
var label2 = (src) => src.slice(src.indexOf(":") + 1);
|
|
1287
|
+
var LEAF_TYPES = {
|
|
1288
|
+
string: "string",
|
|
1289
|
+
number: "number",
|
|
1290
|
+
int: "number",
|
|
1291
|
+
bigint: "number",
|
|
1292
|
+
boolean: "boolean",
|
|
1293
|
+
date: "date"
|
|
1294
|
+
};
|
|
1295
|
+
function walkAttr(schema) {
|
|
1296
|
+
let node = schema;
|
|
1297
|
+
let optional = false;
|
|
1298
|
+
for (let depth = 0; node && depth < 20; depth++) {
|
|
1299
|
+
const def = node._zod?.def ?? node.def;
|
|
1300
|
+
if (!def?.type) break;
|
|
1301
|
+
switch (def.type) {
|
|
1302
|
+
// these three all mean "the value may be absent from a stored record",
|
|
1303
|
+
// which is the only thing `optional` claims
|
|
1304
|
+
case "optional":
|
|
1305
|
+
case "nullable":
|
|
1306
|
+
case "default":
|
|
1307
|
+
optional = true;
|
|
1308
|
+
node = def.innerType;
|
|
1309
|
+
continue;
|
|
1310
|
+
case "catch":
|
|
1311
|
+
case "readonly":
|
|
1312
|
+
node = def.innerType;
|
|
1313
|
+
continue;
|
|
1314
|
+
// a pipe is `in -> out`; the INPUT side is what a caller may send and so
|
|
1315
|
+
// what a stored value was validated as. The output of a transform is
|
|
1316
|
+
// frequently a shape no filter could ever be written against.
|
|
1317
|
+
case "pipe":
|
|
1318
|
+
node = def.in;
|
|
1319
|
+
continue;
|
|
1320
|
+
case "enum": {
|
|
1321
|
+
const options = Array.isArray(node.options) ? node.options : Object.values(def.entries ?? {});
|
|
1322
|
+
return { type: "enum", values: options.map(String), optional };
|
|
1323
|
+
}
|
|
1324
|
+
case "literal":
|
|
1325
|
+
return { type: "enum", values: [...def.values ?? []].map(String), optional };
|
|
1326
|
+
default:
|
|
1327
|
+
return { type: LEAF_TYPES[def.type] ?? "string", optional };
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return { type: "string", optional };
|
|
1331
|
+
}
|
|
1332
|
+
var dim = (key, type, o = {}) => ({
|
|
1333
|
+
key,
|
|
1334
|
+
// the two pseudo-dims are derived at query time from subjectKeys / actor, so
|
|
1335
|
+
// they carry no `field:` prefix and label to themselves
|
|
1336
|
+
label: key.startsWith("field:") ? key.slice(6) : key,
|
|
1337
|
+
type,
|
|
1338
|
+
...o.values ? { values: [...o.values] } : {},
|
|
1339
|
+
optional: o.optional ?? false,
|
|
1340
|
+
indexed: o.indexed ?? false
|
|
1341
|
+
});
|
|
1342
|
+
var envelopeDims = (platforms) => [
|
|
1343
|
+
dim("field:kind", "enum", { values: TELEMETRY_KINDS, indexed: true }),
|
|
1344
|
+
dim("field:name", "string", { indexed: true }),
|
|
1345
|
+
dim("field:severity", "enum", { values: Object.values(LogLevel) }),
|
|
1346
|
+
dim("field:env", "enum", { values: Object.values(Env) }),
|
|
1347
|
+
dim("field:service", "string"),
|
|
1348
|
+
dim("field:release", "string"),
|
|
1349
|
+
dim("field:origin", "enum", { values: Object.values(Origin) }),
|
|
1350
|
+
// client context is absent on server-origin records, so both of its dims are optional
|
|
1351
|
+
dim("field:client.platform", "enum", { values: platforms, optional: true }),
|
|
1352
|
+
dim("field:client.appVersion", "string", { optional: true }),
|
|
1353
|
+
dim("subjectType", "string", { optional: true, indexed: true }),
|
|
1354
|
+
dim("actorType", "string", { optional: true })
|
|
1355
|
+
];
|
|
1356
|
+
var kindDims = (kind) => {
|
|
1357
|
+
switch (kind) {
|
|
1358
|
+
case TelemetryKind.Usage:
|
|
1359
|
+
return [
|
|
1360
|
+
dim("field:usage.meter", "string", { indexed: true }),
|
|
1361
|
+
dim("field:usage.billedTo", "string"),
|
|
1362
|
+
dim("field:usage.unit", "string")
|
|
1363
|
+
];
|
|
1364
|
+
case TelemetryKind.State:
|
|
1365
|
+
return [
|
|
1366
|
+
dim("field:state.key", "string", { indexed: true }),
|
|
1367
|
+
dim("field:state.to", "string", { indexed: true })
|
|
1368
|
+
];
|
|
1369
|
+
case TelemetryKind.Error:
|
|
1370
|
+
return [dim("field:error.type", "string"), dim("field:error.handled", "boolean")];
|
|
1371
|
+
default:
|
|
1372
|
+
return [];
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
var RAW_OPS = ["avg", "p50", "p95", "p99"];
|
|
1376
|
+
function deriveCatalog(registry, opts = {}) {
|
|
1377
|
+
const platforms = [.../* @__PURE__ */ new Set([...BUILTIN_PLATFORMS, ...opts.platforms ?? []])];
|
|
1378
|
+
const families = {};
|
|
1379
|
+
for (const [name, spec] of Object.entries(registry)) {
|
|
1380
|
+
for (const r of spec.rollups ?? []) {
|
|
1381
|
+
const as = r.as ?? name;
|
|
1382
|
+
const seen = families[as];
|
|
1383
|
+
if (!seen) {
|
|
1384
|
+
families[as] = {
|
|
1385
|
+
as,
|
|
1386
|
+
by: [...r.by],
|
|
1387
|
+
labels: r.by.map(label2),
|
|
1388
|
+
bucket: r.bucket ?? null,
|
|
1389
|
+
lifetime: !r.bucket,
|
|
1390
|
+
// `subjects` only means anything when there is a subject dim to
|
|
1391
|
+
// restrict; without one it selects nothing and claiming it would
|
|
1392
|
+
// offer a subject filter the family cannot answer
|
|
1393
|
+
subjectTypes: r.by.includes("subject") ? [...r.subjects ?? []] : [],
|
|
1394
|
+
sums: [...r.sum ?? []],
|
|
1395
|
+
capture: (r.capture ?? []).map(label2),
|
|
1396
|
+
feeders: [name],
|
|
1397
|
+
retentionDays: r.retentionDays ?? null
|
|
1398
|
+
};
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
for (const k of r.sum ?? []) if (!seen.sums.includes(k)) seen.sums.push(k);
|
|
1402
|
+
for (const c of r.capture ?? []) {
|
|
1403
|
+
const l = label2(c);
|
|
1404
|
+
if (!seen.capture.includes(l)) seen.capture.push(l);
|
|
1405
|
+
}
|
|
1406
|
+
if (!seen.feeders.includes(name)) seen.feeders.push(name);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
const events = {};
|
|
1410
|
+
const namespaces = {};
|
|
1411
|
+
const subjectTypes = [];
|
|
1412
|
+
const noteSubject = (t) => {
|
|
1413
|
+
if (!subjectTypes.includes(t)) subjectTypes.push(t);
|
|
1414
|
+
};
|
|
1415
|
+
for (const [name, spec] of Object.entries(registry)) {
|
|
1416
|
+
const dot = name.indexOf(".");
|
|
1417
|
+
const namespace = dot === -1 ? name : name.slice(0, dot);
|
|
1418
|
+
(namespaces[namespace] ??= []).push(name);
|
|
1419
|
+
for (const s of spec.subjects) noteSubject(s);
|
|
1420
|
+
const indexedAttrs = [...spec.indexedAttrs ?? []];
|
|
1421
|
+
const dims = Object.entries(spec.attrs?.shape ?? {}).map(
|
|
1422
|
+
([key, schema]) => {
|
|
1423
|
+
const walked = walkAttr(schema);
|
|
1424
|
+
return {
|
|
1425
|
+
key: `attr:${key}`,
|
|
1426
|
+
label: key,
|
|
1427
|
+
type: walked.type,
|
|
1428
|
+
...walked.values ? { values: walked.values } : {},
|
|
1429
|
+
optional: walked.optional,
|
|
1430
|
+
indexed: indexedAttrs.includes(key)
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
);
|
|
1434
|
+
dims.push(...kindDims(spec.kind));
|
|
1435
|
+
const eventFamilies = [];
|
|
1436
|
+
const ownSums = /* @__PURE__ */ new Map();
|
|
1437
|
+
for (const r of spec.rollups ?? []) {
|
|
1438
|
+
const as = r.as ?? name;
|
|
1439
|
+
if (!eventFamilies.includes(as)) eventFamilies.push(as);
|
|
1440
|
+
const set = ownSums.get(as) ?? /* @__PURE__ */ new Set();
|
|
1441
|
+
for (const k of r.sum ?? []) set.add(k);
|
|
1442
|
+
ownSums.set(as, set);
|
|
1443
|
+
for (const s of r.subjects ?? []) noteSubject(s);
|
|
1444
|
+
}
|
|
1445
|
+
const measures = [{ key: "count", exactVia: [] }];
|
|
1446
|
+
for (const k of Object.keys(spec.metrics?.shape ?? {})) {
|
|
1447
|
+
measures.push({
|
|
1448
|
+
key: `sum:${k}`,
|
|
1449
|
+
metric: k,
|
|
1450
|
+
exactVia: eventFamilies.filter((as) => ownSums.get(as)?.has(k))
|
|
1451
|
+
});
|
|
1452
|
+
for (const op of RAW_OPS) measures.push({ key: `${op}:${k}`, metric: k, exactVia: [] });
|
|
1453
|
+
}
|
|
1454
|
+
if (spec.kind === TelemetryKind.Span) {
|
|
1455
|
+
for (const op of RAW_OPS) {
|
|
1456
|
+
measures.push({ key: `${op}:durationMs`, metric: "durationMs", exactVia: [] });
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
events[name] = {
|
|
1460
|
+
kind: spec.kind,
|
|
1461
|
+
origin: spec.origin,
|
|
1462
|
+
subjects: [...spec.subjects],
|
|
1463
|
+
description: spec.description,
|
|
1464
|
+
namespace,
|
|
1465
|
+
dims,
|
|
1466
|
+
measures,
|
|
1467
|
+
families: eventFamilies,
|
|
1468
|
+
indexedAttrs,
|
|
1469
|
+
indexedMetrics: [...spec.indexedMetrics ?? []],
|
|
1470
|
+
// `hasOwnProperty` rather than `??`, exactly as model.ts stamps expiresAt:
|
|
1471
|
+
// an explicit `retentionDays: null` means immortal and must not fall
|
|
1472
|
+
// through to the per-kind default
|
|
1473
|
+
retentionDays: Object.prototype.hasOwnProperty.call(spec, "retentionDays") ? spec.retentionDays ?? null : RETENTION_DAYS[spec.kind]
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
return { events, families, namespaces, envelope: envelopeDims(platforms), subjectTypes };
|
|
1477
|
+
}
|
|
1478
|
+
function projectRegistry(catalog) {
|
|
1479
|
+
return Object.fromEntries(
|
|
1480
|
+
Object.entries(catalog.events).map(([name, e]) => [
|
|
1481
|
+
name,
|
|
1482
|
+
{
|
|
1483
|
+
kind: e.kind,
|
|
1484
|
+
origin: e.origin,
|
|
1485
|
+
subjects: e.subjects,
|
|
1486
|
+
description: e.description,
|
|
1487
|
+
attrKeys: e.dims.filter((d) => d.key.startsWith("attr:")).map((d) => d.label),
|
|
1488
|
+
// every metric key gets exactly one `sum:` measure and nothing else does
|
|
1489
|
+
metricKeys: e.measures.filter((m) => m.key.startsWith("sum:")).map((m) => m.metric),
|
|
1490
|
+
indexedAttrs: e.indexedAttrs,
|
|
1491
|
+
indexedMetrics: e.indexedMetrics,
|
|
1492
|
+
rollups: e.families.map((as) => {
|
|
1493
|
+
const f = catalog.families[as];
|
|
1494
|
+
return { as: f.as, by: f.by, bucket: f.bucket, sum: f.sums, subjects: f.subjectTypes };
|
|
1495
|
+
})
|
|
1496
|
+
}
|
|
1497
|
+
])
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// src/server/suggest.ts
|
|
1502
|
+
var UNREGISTERED_REASON = "unregistered event";
|
|
1503
|
+
var MAX_SUGGESTIONS = 50;
|
|
1504
|
+
var NAME_MAX = 120;
|
|
1505
|
+
var quote = (s) => /^[A-Za-z0-9_.:$-]+$/.test(s) ? `'${s}'` : JSON.stringify(s);
|
|
1506
|
+
var prop = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s) ? s : quote(s);
|
|
1507
|
+
var times = (n) => `${n} time${n === 1 ? "" : "s"}`;
|
|
1508
|
+
var split = (k) => {
|
|
1509
|
+
const i = k.indexOf("|");
|
|
1510
|
+
return i === -1 ? [k, ""] : [k.slice(0, i), k.slice(i + 1)];
|
|
1511
|
+
};
|
|
1512
|
+
function deriveSuggestions(input) {
|
|
1513
|
+
const { counters, catalog, quarantine = [] } = input;
|
|
1514
|
+
const out = [];
|
|
1515
|
+
for (const [k, count2] of Object.entries(counters.undeclaredAttrs ?? {})) {
|
|
1516
|
+
if (k === COUNTER_OVERFLOW_KEY || !count2) continue;
|
|
1517
|
+
const [name, key] = split(k);
|
|
1518
|
+
if (!name || !key) continue;
|
|
1519
|
+
const facet = catalog.events[name];
|
|
1520
|
+
const line = `${prop(key)}: z.string().max(64),`;
|
|
1521
|
+
const hasAttrs = !!facet?.dims.some((d) => d.key.startsWith("attr:"));
|
|
1522
|
+
out.push({
|
|
1523
|
+
kind: "undeclared_attr",
|
|
1524
|
+
target: name,
|
|
1525
|
+
key,
|
|
1526
|
+
count: count2,
|
|
1527
|
+
message: `\`${name}\` has been sent with attr \`${key}\` ${times(count2)} \u2014 not declared`,
|
|
1528
|
+
fix: hasAttrs ? line : `attrs: z.object({ ${prop(key)}: z.string().max(64) }),`
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
for (const [k, count2] of Object.entries(counters.rollupSkippedBy ?? {})) {
|
|
1532
|
+
if (k === COUNTER_OVERFLOW_KEY || !count2) continue;
|
|
1533
|
+
const [as, dim2] = split(k);
|
|
1534
|
+
if (!as || !dim2) continue;
|
|
1535
|
+
const feeders = catalog.families[as]?.feeders ?? [];
|
|
1536
|
+
const where = feeders.length ? `// on the \`${as}\` rollup of ${feeders.map((f) => `\`${f}\``).join(", ")}
|
|
1537
|
+
` : "";
|
|
1538
|
+
out.push({
|
|
1539
|
+
kind: "missing_dim_default",
|
|
1540
|
+
target: as,
|
|
1541
|
+
key: dim2,
|
|
1542
|
+
count: count2,
|
|
1543
|
+
message: `\`${as}\` skipped ${count2} record${count2 === 1 ? "" : "s"} with no \`${dim2}\` \u2014 declare \`dimDefault\``,
|
|
1544
|
+
fix: `${where}dimDefault: 'unknown',`
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
const unregistered = /* @__PURE__ */ new Map();
|
|
1548
|
+
for (const row of quarantine) {
|
|
1549
|
+
if (typeof row?.reason !== "string" || !row.reason.includes(UNREGISTERED_REASON)) continue;
|
|
1550
|
+
const name = typeof row.name === "string" ? row.name.slice(0, NAME_MAX) : "";
|
|
1551
|
+
if (!name || name === "(unnamed)") continue;
|
|
1552
|
+
unregistered.set(name, (unregistered.get(name) ?? 0) + 1);
|
|
1553
|
+
}
|
|
1554
|
+
for (const [name, count2] of unregistered) {
|
|
1555
|
+
out.push({
|
|
1556
|
+
kind: "unregistered_event",
|
|
1557
|
+
target: name,
|
|
1558
|
+
count: count2,
|
|
1559
|
+
message: `\`${name}\` was rejected ${times(count2)} \u2014 not in the registry`,
|
|
1560
|
+
// the minimum that boots: validateRegistry wants a kind, an origin, and
|
|
1561
|
+
// a subjects array, and nothing here can guess the rest
|
|
1562
|
+
fix: `${quote(name)}: { kind: 'event', origin: 'client', subjects: [], description: '' },`
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
out.sort(
|
|
1566
|
+
(a, b) => b.count - a.count || a.target.localeCompare(b.target) || (a.key ?? "").localeCompare(b.key ?? "")
|
|
1567
|
+
);
|
|
1568
|
+
return out.slice(0, MAX_SUGGESTIONS);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1256
1571
|
// src/server/funnel.ts
|
|
1257
1572
|
var DAY_MS = 864e5;
|
|
1258
1573
|
function median(values) {
|
|
@@ -1460,6 +1775,10 @@ var DEFAULT_LIMITS = {
|
|
|
1460
1775
|
rollups: 500,
|
|
1461
1776
|
trace: 500,
|
|
1462
1777
|
journey: 500,
|
|
1778
|
+
breakdown: 50,
|
|
1779
|
+
// top groups — a starting point, to be measured on real hosts
|
|
1780
|
+
values: 200,
|
|
1781
|
+
// top values of one dimension — a picker, not a table
|
|
1463
1782
|
distribution: 1e5,
|
|
1464
1783
|
distinct: 1e5,
|
|
1465
1784
|
funnel: 5e3
|
|
@@ -1473,7 +1792,10 @@ function buildMatch(scope, range, f) {
|
|
|
1473
1792
|
occurredAt: { $gte: range.from, $lt: range.to }
|
|
1474
1793
|
};
|
|
1475
1794
|
for (const k of ["kind", "name", "severity", "env", "service", "release", "traceId"]) {
|
|
1476
|
-
|
|
1795
|
+
const v = f[k];
|
|
1796
|
+
if (Array.isArray(v)) {
|
|
1797
|
+
if (v.length) match[k] = { $in: v };
|
|
1798
|
+
} else if (v) match[k] = v;
|
|
1477
1799
|
}
|
|
1478
1800
|
if (f.subject) match.subjectKeys = f.subject;
|
|
1479
1801
|
for (const [k, v] of Object.entries(f.attrs ?? {})) match[`attrs.${k}`] = v;
|
|
@@ -1496,6 +1818,68 @@ function buildMatch(scope, range, f) {
|
|
|
1496
1818
|
}
|
|
1497
1819
|
return match;
|
|
1498
1820
|
}
|
|
1821
|
+
var INTERVALS = ["hour", "day", "week", "month"];
|
|
1822
|
+
var truncTo = (path3, unit) => ({
|
|
1823
|
+
$dateTrunc: { date: path3, unit, ...unit === "week" ? { startOfWeek: "monday" } : {} }
|
|
1824
|
+
});
|
|
1825
|
+
function measureAccumulator(measure) {
|
|
1826
|
+
const m = /^(sum|avg):(.+)$/.exec(measure);
|
|
1827
|
+
if (!m) return { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } };
|
|
1828
|
+
const path3 = m[2] === "durationMs" ? "$durationMs" : `$metrics.${m[2]}`;
|
|
1829
|
+
return m[1] === "sum" ? { $sum: path3 } : { $avg: path3 };
|
|
1830
|
+
}
|
|
1831
|
+
var badRequest = (message) => Object.assign(new Error(`telemetry: breakdown() \u2014 ${message}`), { status: 400 });
|
|
1832
|
+
var BREAKDOWN_FIELDS = [
|
|
1833
|
+
"kind",
|
|
1834
|
+
"name",
|
|
1835
|
+
"severity",
|
|
1836
|
+
"env",
|
|
1837
|
+
"service",
|
|
1838
|
+
"release",
|
|
1839
|
+
"origin",
|
|
1840
|
+
"client.platform",
|
|
1841
|
+
"client.appVersion",
|
|
1842
|
+
"usage.meter",
|
|
1843
|
+
"usage.billedTo",
|
|
1844
|
+
"usage.unit",
|
|
1845
|
+
"state.key",
|
|
1846
|
+
"state.to",
|
|
1847
|
+
"error.type",
|
|
1848
|
+
"error.handled"
|
|
1849
|
+
];
|
|
1850
|
+
var typePrefix = (ref) => ({
|
|
1851
|
+
$let: {
|
|
1852
|
+
vars: { ref },
|
|
1853
|
+
in: {
|
|
1854
|
+
$cond: [
|
|
1855
|
+
{ $eq: [{ $type: "$$ref" }, "string"] },
|
|
1856
|
+
{ $arrayElemAt: [{ $split: ["$$ref", ":"] }, 0] },
|
|
1857
|
+
null
|
|
1858
|
+
]
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
function dimExpression(dim2) {
|
|
1863
|
+
if (dim2.startsWith("attr:")) {
|
|
1864
|
+
const key = dim2.slice(5);
|
|
1865
|
+
if (!key) throw badRequest('`attr:` needs a key, e.g. "attr:plan"');
|
|
1866
|
+
return { $ifNull: [`$attrs.${key}`, null] };
|
|
1867
|
+
}
|
|
1868
|
+
if (dim2.startsWith("field:")) {
|
|
1869
|
+
const path3 = dim2.slice(6);
|
|
1870
|
+
if (!BREAKDOWN_FIELDS.includes(path3)) {
|
|
1871
|
+
throw badRequest(
|
|
1872
|
+
`"field:${path3}" is not groupable. Allowed paths: ${BREAKDOWN_FIELDS.join(", ")}. Grouping by \`data.*\`, \`body\`, or an arbitrary path is refused \u2014 those are unindexed free-form content, and a $group over them scans it all. Use \`attr:<key>\` for a declared attr.`
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
return { $ifNull: [`$${path3}`, null] };
|
|
1876
|
+
}
|
|
1877
|
+
if (dim2 === "subjectType") return typePrefix({ $arrayElemAt: ["$subjectKeys", 0] });
|
|
1878
|
+
if (dim2 === "actorType") return typePrefix("$actor");
|
|
1879
|
+
throw badRequest(
|
|
1880
|
+
`"${dim2}" is not a dimension. Use "attr:<key>", "field:<path>", "subjectType" or "actorType".`
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1499
1883
|
var QueryCache = class {
|
|
1500
1884
|
constructor(ttlMs, cap) {
|
|
1501
1885
|
this.ttlMs = ttlMs;
|
|
@@ -1569,16 +1953,9 @@ function createQueries(ctx) {
|
|
|
1569
1953
|
return cache.get(
|
|
1570
1954
|
key,
|
|
1571
1955
|
() => timed("series", { scope, filter, measure, interval }, async () => {
|
|
1572
|
-
const m = /^(sum|avg):(.+)$/.exec(measure);
|
|
1573
|
-
const value = !m ? { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } } : m[1] === "sum" ? { $sum: `$metrics.${m[2]}` } : { $avg: `$metrics.${m[2]}` };
|
|
1574
1956
|
const buckets = await ctx.TelemetryModel.aggregate([
|
|
1575
1957
|
{ $match: buildMatch(scope, range, filter) },
|
|
1576
|
-
{
|
|
1577
|
-
$group: {
|
|
1578
|
-
_id: { $dateTrunc: { date: "$occurredAt", unit: interval, ...interval === "week" ? { startOfWeek: "monday" } : {} } },
|
|
1579
|
-
value
|
|
1580
|
-
}
|
|
1581
|
-
},
|
|
1958
|
+
{ $group: { _id: truncTo("$occurredAt", interval), value: measureAccumulator(measure) } },
|
|
1582
1959
|
{ $sort: { _id: 1 } },
|
|
1583
1960
|
{ $limit: limits.series }
|
|
1584
1961
|
]);
|
|
@@ -1586,6 +1963,114 @@ function createQueries(ctx) {
|
|
|
1586
1963
|
})
|
|
1587
1964
|
);
|
|
1588
1965
|
},
|
|
1966
|
+
/**
|
|
1967
|
+
* Top groups of a measure by one or two dimensions — "which models cost the
|
|
1968
|
+
* most", "errors by release", "events by platform per week". The primitive
|
|
1969
|
+
* that replaces a page's client-side grouping of whatever rows it happened
|
|
1970
|
+
* to have fetched, which answered "this page" while reading like it
|
|
1971
|
+
* answered the range (reports §6).
|
|
1972
|
+
*
|
|
1973
|
+
* THE CAP IS ON GROUPS RETURNED, NEVER ON ROWS SCANNED. Every `$limit`
|
|
1974
|
+
* below sits AFTER a `$group`, exactly as series() does: the scan is bounded
|
|
1975
|
+
* by buildMatch — tenant, range, filters, indexes — and nothing else, so a
|
|
1976
|
+
* quarter of a million records is one pass and 50 rows. Truncation
|
|
1977
|
+
* therefore keeps the TOP groups by measure, which is what a breakdown
|
|
1978
|
+
* table means; a cap on documents scanned would return an arbitrary prefix
|
|
1979
|
+
* and call it the top.
|
|
1980
|
+
*
|
|
1981
|
+
* With an `interval` this runs a SECOND aggregate restricted to the top
|
|
1982
|
+
* groups, rather than one pipeline that groups by (dims, bucket) and folds.
|
|
1983
|
+
* Two reasons: the ranking must be the measure over the WHOLE range (the
|
|
1984
|
+
* same number the no-interval call reports), and folding in one pass means
|
|
1985
|
+
* `$push`-ing every bucket of every group before the cap can apply — the
|
|
1986
|
+
* unbounded intermediate this primitive exists to avoid. The restriction is
|
|
1987
|
+
* an `$expr`/`$or` over the ≤ cap tuples because a dim can be a computed
|
|
1988
|
+
* expression (subjectType), which a plain `$in` on a path cannot address.
|
|
1989
|
+
*
|
|
1990
|
+
* Under PLATFORM_SCOPE it aggregates ACROSS tenants, like series() — one
|
|
1991
|
+
* set of groups with every tenant summed into it, which is the platform-wide
|
|
1992
|
+
* table a platform operator came for. Ask for a per-tenant split by scoping
|
|
1993
|
+
* to a tenant, or with rollups().
|
|
1994
|
+
*/
|
|
1995
|
+
breakdown(scope, range, filter, opts) {
|
|
1996
|
+
const groupBy = opts.groupBy ?? [];
|
|
1997
|
+
if (groupBy.length < 1 || groupBy.length > 2) {
|
|
1998
|
+
throw badRequest(`groupBy takes 1 or 2 dimensions, got ${groupBy.length}`);
|
|
1999
|
+
}
|
|
2000
|
+
const measure = opts.measure ?? "count";
|
|
2001
|
+
const interval = opts.interval;
|
|
2002
|
+
if (interval && !INTERVALS.includes(interval)) {
|
|
2003
|
+
throw badRequest(`interval must be one of ${INTERVALS.join(", ")}`);
|
|
2004
|
+
}
|
|
2005
|
+
const dims = groupBy.map(dimExpression);
|
|
2006
|
+
const cap = Math.min(Math.max(1, opts.limit ?? limits.breakdown), limits.breakdown);
|
|
2007
|
+
const key = JSON.stringify([
|
|
2008
|
+
"breakdown",
|
|
2009
|
+
scope,
|
|
2010
|
+
range.from,
|
|
2011
|
+
range.to,
|
|
2012
|
+
filter,
|
|
2013
|
+
groupBy,
|
|
2014
|
+
measure,
|
|
2015
|
+
interval ?? null,
|
|
2016
|
+
cap
|
|
2017
|
+
]);
|
|
2018
|
+
return cache.get(
|
|
2019
|
+
key,
|
|
2020
|
+
() => timed("breakdown", { scope, filter, groupBy, measure, interval }, async () => {
|
|
2021
|
+
const match = buildMatch(scope, range, filter);
|
|
2022
|
+
const dimId = Object.fromEntries(dims.map((expr, i) => [`d${i}`, expr]));
|
|
2023
|
+
const top = await ctx.TelemetryModel.aggregate([
|
|
2024
|
+
{ $match: match },
|
|
2025
|
+
{ $group: { _id: dimId, value: measureAccumulator(measure) } },
|
|
2026
|
+
{ $sort: { value: -1, _id: 1 } },
|
|
2027
|
+
{ $limit: cap + 1 }
|
|
2028
|
+
]);
|
|
2029
|
+
const truncated = top.length > cap;
|
|
2030
|
+
if (truncated) top.pop();
|
|
2031
|
+
const tuples = top.map(
|
|
2032
|
+
(g) => groupBy.map((_, i) => g._id?.[`d${i}`] ?? null)
|
|
2033
|
+
);
|
|
2034
|
+
if (!interval) {
|
|
2035
|
+
return {
|
|
2036
|
+
rows: top.map((g, i) => ({ dims: tuples[i], value: g.value })),
|
|
2037
|
+
groups: top.length,
|
|
2038
|
+
truncated,
|
|
2039
|
+
bucketsTruncated: false,
|
|
2040
|
+
dataSource: "raw"
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
if (!tuples.length) {
|
|
2044
|
+
return { rows: [], groups: 0, truncated, bucketsTruncated: false, dataSource: "raw" };
|
|
2045
|
+
}
|
|
2046
|
+
const inTop = {
|
|
2047
|
+
$or: tuples.map((t) => ({ $and: dims.map((expr, i) => ({ $eq: [expr, t[i] ?? null] })) }))
|
|
2048
|
+
};
|
|
2049
|
+
const bucketCap = limits.series * top.length;
|
|
2050
|
+
const perBucket = await ctx.TelemetryModel.aggregate([
|
|
2051
|
+
{ $match: { ...match, $expr: inTop } },
|
|
2052
|
+
{ $group: { _id: { at: truncTo("$occurredAt", interval), ...dimId }, value: measureAccumulator(measure) } },
|
|
2053
|
+
// `at` is the first key of `_id`, so one BSON sort orders by bucket
|
|
2054
|
+
// then by dims — deterministic without a second sort key
|
|
2055
|
+
{ $sort: { _id: 1 } },
|
|
2056
|
+
{ $limit: bucketCap + 1 }
|
|
2057
|
+
]);
|
|
2058
|
+
const bucketsTruncated = perBucket.length > bucketCap;
|
|
2059
|
+
if (bucketsTruncated) perBucket.pop();
|
|
2060
|
+
return {
|
|
2061
|
+
rows: perBucket.map((b) => ({
|
|
2062
|
+
dims: groupBy.map((_, i) => b._id?.[`d${i}`] ?? null),
|
|
2063
|
+
at: b._id.at,
|
|
2064
|
+
value: b.value
|
|
2065
|
+
})),
|
|
2066
|
+
groups: top.length,
|
|
2067
|
+
truncated,
|
|
2068
|
+
bucketsTruncated,
|
|
2069
|
+
dataSource: "raw"
|
|
2070
|
+
};
|
|
2071
|
+
})
|
|
2072
|
+
);
|
|
2073
|
+
},
|
|
1589
2074
|
/**
|
|
1590
2075
|
* Percentiles + histogram off raw. Keep-all makes the SAMPLE complete —
|
|
1591
2076
|
* no sampling stands between the match and the math (§5.3) — but the
|
|
@@ -1827,6 +2312,820 @@ function requireDistinctFamily(registry, as) {
|
|
|
1827
2312
|
}
|
|
1828
2313
|
return spec;
|
|
1829
2314
|
}
|
|
2315
|
+
|
|
2316
|
+
// src/server/values.ts
|
|
2317
|
+
var empty = (source) => ({
|
|
2318
|
+
values: [],
|
|
2319
|
+
source,
|
|
2320
|
+
truncated: false,
|
|
2321
|
+
dataSource: source
|
|
2322
|
+
});
|
|
2323
|
+
var SAMPLED_COUNT = { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } };
|
|
2324
|
+
function createValues(ctx) {
|
|
2325
|
+
const limits = { ...DEFAULT_LIMITS, ...ctx.limits };
|
|
2326
|
+
const slowMs = ctx.slowMs ?? 500;
|
|
2327
|
+
const cache = new QueryCache(ctx.cacheTtlMs ?? 10 * 6e4, ctx.cacheSize ?? 60);
|
|
2328
|
+
const { catalog } = ctx;
|
|
2329
|
+
const timed = async (op, params, run) => {
|
|
2330
|
+
const t0 = Date.now();
|
|
2331
|
+
try {
|
|
2332
|
+
return await run();
|
|
2333
|
+
} finally {
|
|
2334
|
+
const ms = Date.now() - t0;
|
|
2335
|
+
if (ms > slowMs) ctx.onSlowQuery?.({ op, ms, params });
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
const eventNames = (names) => names?.length ? names.filter((n) => catalog.events[n]) : Object.keys(catalog.events);
|
|
2339
|
+
function fromCatalog(dim2, names) {
|
|
2340
|
+
const facets = [];
|
|
2341
|
+
for (const d of catalog.envelope) if (d.key === dim2) facets.push(d);
|
|
2342
|
+
for (const name of eventNames(names)) {
|
|
2343
|
+
for (const d of catalog.events[name].dims) if (d.key === dim2) facets.push(d);
|
|
2344
|
+
}
|
|
2345
|
+
const out = [];
|
|
2346
|
+
for (const f of facets) for (const v of f.values ?? []) if (!out.includes(v)) out.push(v);
|
|
2347
|
+
return out;
|
|
2348
|
+
}
|
|
2349
|
+
function pickFamily(dim2, names) {
|
|
2350
|
+
if (dim2 === "subjectType" || dim2 === "actorType") return null;
|
|
2351
|
+
const matches = Object.values(catalog.families).map((f) => ({ f, index: f.by.indexOf(dim2) })).filter(({ f, index }) => index !== -1 && (!names?.length || f.feeders.some((n) => names.includes(n)))).sort((a, b) => a.f.by.length - b.f.by.length || a.f.as.localeCompare(b.f.as));
|
|
2352
|
+
const best = matches[0];
|
|
2353
|
+
return best ? { as: best.f.as, index: best.index, label: best.f.labels[best.index] } : null;
|
|
2354
|
+
}
|
|
2355
|
+
function rawReadable(dim2, names) {
|
|
2356
|
+
try {
|
|
2357
|
+
dimExpression(dim2);
|
|
2358
|
+
} catch {
|
|
2359
|
+
return false;
|
|
2360
|
+
}
|
|
2361
|
+
if (!dim2.startsWith("attr:")) return true;
|
|
2362
|
+
const key = dim2.slice(5);
|
|
2363
|
+
return eventNames(names).some((n) => catalog.events[n].indexedAttrs.includes(key));
|
|
2364
|
+
}
|
|
2365
|
+
return async function values(scope, params) {
|
|
2366
|
+
const { dim: dim2, names, range } = params;
|
|
2367
|
+
if (!dim2) return empty("none");
|
|
2368
|
+
const cap = Math.min(Math.max(1, params.limit ?? limits.values), limits.values);
|
|
2369
|
+
const key = JSON.stringify([
|
|
2370
|
+
"values",
|
|
2371
|
+
scope,
|
|
2372
|
+
dim2,
|
|
2373
|
+
names ?? null,
|
|
2374
|
+
range?.from ?? null,
|
|
2375
|
+
range?.to ?? null,
|
|
2376
|
+
cap
|
|
2377
|
+
]);
|
|
2378
|
+
return cache.get(
|
|
2379
|
+
key,
|
|
2380
|
+
() => timed("values", { scope, dim: dim2, names }, async () => {
|
|
2381
|
+
const declared = fromCatalog(dim2, names);
|
|
2382
|
+
if (declared.length) {
|
|
2383
|
+
return { values: declared, source: "catalog", truncated: false, dataSource: "catalog" };
|
|
2384
|
+
}
|
|
2385
|
+
const family = pickFamily(dim2, names);
|
|
2386
|
+
if (family) {
|
|
2387
|
+
const rows2 = await ctx.RollupModel.aggregate([
|
|
2388
|
+
{
|
|
2389
|
+
$match: {
|
|
2390
|
+
...isPlatformScope(scope) ? {} : { tenantId: scope },
|
|
2391
|
+
as: family.as
|
|
2392
|
+
}
|
|
2393
|
+
},
|
|
2394
|
+
{ $project: { v: { $arrayElemAt: ["$dims", family.index] }, count: 1 } },
|
|
2395
|
+
{ $match: { v: { $type: "string" } } },
|
|
2396
|
+
{ $group: { _id: "$v", count: { $sum: "$count" } } },
|
|
2397
|
+
{ $sort: { count: -1, _id: 1 } },
|
|
2398
|
+
{ $limit: cap + 1 }
|
|
2399
|
+
]);
|
|
2400
|
+
const truncated2 = rows2.length > cap;
|
|
2401
|
+
if (truncated2) rows2.pop();
|
|
2402
|
+
const prefix = `${family.label}=`;
|
|
2403
|
+
return {
|
|
2404
|
+
values: rows2.map(
|
|
2405
|
+
(r) => String(r._id).startsWith(prefix) ? String(r._id).slice(prefix.length) : String(r._id)
|
|
2406
|
+
),
|
|
2407
|
+
counts: rows2.map((r) => r.count),
|
|
2408
|
+
source: "rollups",
|
|
2409
|
+
via: family.as,
|
|
2410
|
+
truncated: truncated2,
|
|
2411
|
+
dataSource: "rollups"
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
if (!rawReadable(dim2, names)) return empty("none");
|
|
2415
|
+
if (!range) return empty("none");
|
|
2416
|
+
const rows = await ctx.TelemetryModel.aggregate([
|
|
2417
|
+
{ $match: buildMatch(scope, range, names?.length ? { name: names } : {}) },
|
|
2418
|
+
{ $group: { _id: dimExpression(dim2), count: SAMPLED_COUNT } },
|
|
2419
|
+
// a "no value" is not a value to pick — the null group is real
|
|
2420
|
+
// (breakdown reports it) but it is not something a filter can name
|
|
2421
|
+
{ $match: { _id: { $ne: null } } },
|
|
2422
|
+
{ $sort: { count: -1, _id: 1 } },
|
|
2423
|
+
{ $limit: cap + 1 }
|
|
2424
|
+
]);
|
|
2425
|
+
const truncated = rows.length > cap;
|
|
2426
|
+
if (truncated) rows.pop();
|
|
2427
|
+
return {
|
|
2428
|
+
values: rows.map((r) => String(r._id)),
|
|
2429
|
+
counts: rows.map((r) => r.count),
|
|
2430
|
+
source: "raw",
|
|
2431
|
+
truncated,
|
|
2432
|
+
dataSource: "raw"
|
|
2433
|
+
};
|
|
2434
|
+
})
|
|
2435
|
+
);
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
// src/server/report.ts
|
|
2440
|
+
var RANGE_MS = {
|
|
2441
|
+
"1h": 36e5,
|
|
2442
|
+
"24h": 864e5,
|
|
2443
|
+
"7d": 7 * 864e5,
|
|
2444
|
+
"30d": 30 * 864e5,
|
|
2445
|
+
"90d": 90 * 864e5
|
|
2446
|
+
};
|
|
2447
|
+
var badRequest2 = (message) => Object.assign(new Error(`telemetry: ${message}`), { status: 400 });
|
|
2448
|
+
function rangeOf(range, now = /* @__PURE__ */ new Date()) {
|
|
2449
|
+
if (typeof range === "string") {
|
|
2450
|
+
const ms = RANGE_MS[range] ?? spanOf(range);
|
|
2451
|
+
if (ms == null) {
|
|
2452
|
+
throw badRequest2(
|
|
2453
|
+
`range "${range}" is not a known shorthand \u2014 use one of ${Object.keys(RANGE_MS).join(", ")}, an \`<n>h\`/\`<n>d\` form, or an explicit { from, to } ISO pair`
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2456
|
+
return { from: new Date(now.getTime() - ms), to: now };
|
|
2457
|
+
}
|
|
2458
|
+
const from = new Date(range.from);
|
|
2459
|
+
const to = new Date(range.to);
|
|
2460
|
+
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()) || from >= to) {
|
|
2461
|
+
throw badRequest2(
|
|
2462
|
+
`range { from: "${range.from}", to: "${range.to}" } is not valid \u2014 \`from\` must be a valid ISO time strictly before \`to\` (the package is half-open everywhere)`
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
return { from, to };
|
|
2466
|
+
}
|
|
2467
|
+
function spanOf(range) {
|
|
2468
|
+
const m = /^(\d+)([hd])$/.exec(range);
|
|
2469
|
+
if (!m) return null;
|
|
2470
|
+
return Number(m[1]) * (m[2] === "h" ? 36e5 : 864e5);
|
|
2471
|
+
}
|
|
2472
|
+
function intervalForRange(range, now = /* @__PURE__ */ new Date()) {
|
|
2473
|
+
if (typeof range === "string" && RANGE_MS[range] != null) {
|
|
2474
|
+
return range === "1h" || range === "24h" ? "hour" : range === "90d" ? "week" : "day";
|
|
2475
|
+
}
|
|
2476
|
+
const { from, to } = rangeOf(range, now);
|
|
2477
|
+
const ms = to.getTime() - from.getTime();
|
|
2478
|
+
if (ms <= 864e5) return "hour";
|
|
2479
|
+
if (ms < 90 * 864e5) return "day";
|
|
2480
|
+
return "week";
|
|
2481
|
+
}
|
|
2482
|
+
var INTERVAL_RANK = { hour: 0, day: 1, week: 2, month: 3 };
|
|
2483
|
+
var shift = (range) => ({
|
|
2484
|
+
from: new Date(range.from.getTime() - (range.to.getTime() - range.from.getTime())),
|
|
2485
|
+
to: range.from
|
|
2486
|
+
});
|
|
2487
|
+
function expandSource(source, catalog) {
|
|
2488
|
+
if ("event" in source) {
|
|
2489
|
+
if (!catalog.events[source.event]) {
|
|
2490
|
+
return unavailable(
|
|
2491
|
+
`no event named "${source.event}" is registered \u2014 the catalog knows ${count(Object.keys(catalog.events).length, "event")}, so either the name is a typo or the registry never declared it`
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
return { form: "event", events: [source.event] };
|
|
2495
|
+
}
|
|
2496
|
+
if ("namespace" in source) {
|
|
2497
|
+
const events = catalog.namespaces[source.namespace];
|
|
2498
|
+
if (!events?.length) {
|
|
2499
|
+
return unavailable(
|
|
2500
|
+
`no event name starts with "${source.namespace}." \u2014 the registered namespaces are ${Object.keys(catalog.namespaces).join(", ")}`
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
return { form: "namespace", events: [...events] };
|
|
2504
|
+
}
|
|
2505
|
+
if ("kind" in source) {
|
|
2506
|
+
const events = Object.keys(catalog.events).filter((n) => catalog.events[n].kind === source.kind);
|
|
2507
|
+
if (!events.length) {
|
|
2508
|
+
return unavailable(
|
|
2509
|
+
`no event is registered with kind "${source.kind}" \u2014 declare one, or pick a kind the registry uses`
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
return { form: "kind", events, kind: source.kind };
|
|
2513
|
+
}
|
|
2514
|
+
const family = catalog.families[source.family];
|
|
2515
|
+
if (!family) {
|
|
2516
|
+
return unavailable(
|
|
2517
|
+
`no rollup family "${source.family}" is declared \u2014 add a \`rollups: [{ as: '${source.family}', by: [...] }]\` block to the event that should feed it`
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
return { form: "family", events: [...family.feeders], family };
|
|
2521
|
+
}
|
|
2522
|
+
var FILTER_ONLY = {
|
|
2523
|
+
"field:subject": "subject",
|
|
2524
|
+
"field:traceId": "traceId"
|
|
2525
|
+
};
|
|
2526
|
+
function dimsFor(catalog, events) {
|
|
2527
|
+
const out = /* @__PURE__ */ new Map();
|
|
2528
|
+
for (const d of catalog.envelope) out.set(d.key, d);
|
|
2529
|
+
for (const name of events) {
|
|
2530
|
+
for (const d of catalog.events[name]?.dims ?? []) {
|
|
2531
|
+
const seen = out.get(d.key);
|
|
2532
|
+
out.set(d.key, seen ? { ...seen, indexed: seen.indexed && d.indexed } : d);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
return out;
|
|
2536
|
+
}
|
|
2537
|
+
var measureDeclared = (catalog, events, key) => key === "count" || events.some((n) => catalog.events[n]?.measures.some((m) => m.key === key));
|
|
2538
|
+
var MEASURE_OP = /^(sum|avg|p50|p90|p95|p99):(.+)$/;
|
|
2539
|
+
var unavailable = (why) => ({ unavailable: true, why });
|
|
2540
|
+
var count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
2541
|
+
function resolveReport(report, catalog, opts = {}) {
|
|
2542
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
2543
|
+
const limits = opts.limits ?? {};
|
|
2544
|
+
const src = expandSource(report.source, catalog);
|
|
2545
|
+
if ("unavailable" in src) return src;
|
|
2546
|
+
const measure = report.measure ?? "count";
|
|
2547
|
+
const groupBy = report.groupBy ?? [];
|
|
2548
|
+
if (groupBy.length > 2) {
|
|
2549
|
+
return unavailable(
|
|
2550
|
+
`groupBy takes at most 2 dimensions, got ${groupBy.length} (${groupBy.join(", ")}) \u2014 three dims is a pivot table nobody can read and a group count that multiplies`
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
if (report.interval && INTERVAL_RANK[report.interval] == null) {
|
|
2554
|
+
return unavailable(`interval "${report.interval}" is not one of hour, day, week, month`);
|
|
2555
|
+
}
|
|
2556
|
+
if (measure === "funnel") return planFunnel(report, catalog, now, limits);
|
|
2557
|
+
if (measure.startsWith("distinct:")) return planDistinct(report, catalog, src, measure, now);
|
|
2558
|
+
const opMatch = MEASURE_OP.exec(measure);
|
|
2559
|
+
if (measure !== "count" && !opMatch) {
|
|
2560
|
+
return unavailable(
|
|
2561
|
+
`measure "${measure}" is not a measure \u2014 use 'count', 'sum:<metric>', 'avg:<metric>', 'p50|p95|p99:<metric>', 'distinct:<subjectType>' or 'funnel'`
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
if (opMatch && !measureDeclared(catalog, src.events, measure)) {
|
|
2565
|
+
return unavailable(
|
|
2566
|
+
`"${measure}" names a metric no source event declares \u2014 add \`${opMatch[2]}\` to the \`metrics\` object of ${src.events.join(", ")}, or pick one of ${metricList(catalog, src.events)}`
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
const exact = planRollups(report, catalog, src, measure, groupBy, now, limits);
|
|
2570
|
+
if (exact) return exact;
|
|
2571
|
+
const filter = toRecordFilter(report, catalog, src);
|
|
2572
|
+
if ("unavailable" in filter) return filter;
|
|
2573
|
+
const range = rangeOf(report.range, now);
|
|
2574
|
+
const dims = dimsFor(catalog, src.events);
|
|
2575
|
+
const touched = [...groupBy, ...(report.filters ?? []).map((f) => f.dim)];
|
|
2576
|
+
const unindexed = touched.find((k) => !(dims.get(k)?.indexed ?? FILTER_ONLY[k] != null));
|
|
2577
|
+
const scanWhy = unindexed ? ` \u2014 "${unindexed}" has no index behind it, so this is a collection scan bounded only by the range` + (unindexed.startsWith("attr:") ? `; add "${unindexed.slice(5)}" to \`indexedAttrs\` to make it a lookup` : "") : "";
|
|
2578
|
+
const exactness = unindexed != null ? "scan" : "raw";
|
|
2579
|
+
if (opMatch && opMatch[1] !== "sum" && opMatch[1] !== "avg") {
|
|
2580
|
+
if (groupBy.length) {
|
|
2581
|
+
return unavailable(
|
|
2582
|
+
`percentiles per group are not offered yet (reports \xA713) \u2014 "${measure}" with groupBy ${groupBy.join(", ")} would be one distribution() read per group. Drop the groupBy, or filter to one group and ask again`
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
return withCompare(report, {
|
|
2586
|
+
primitive: "distribution",
|
|
2587
|
+
args: [range, filter, { measure: opMatch[2] }],
|
|
2588
|
+
exactness,
|
|
2589
|
+
why: `${measure} is approximate by construction ($percentile t-digest over the matched records)${scanWhy}`
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
if (groupBy.length) {
|
|
2593
|
+
const bad = groupBy.find((k) => !dims.has(k));
|
|
2594
|
+
if (bad) {
|
|
2595
|
+
return unavailable(
|
|
2596
|
+
`"${bad}" is not a dimension of ${describe(src)} \u2014 group by one of ${[...dims.keys()].join(", ")}`
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
return withCompare(report, {
|
|
2600
|
+
primitive: "breakdown",
|
|
2601
|
+
args: [
|
|
2602
|
+
range,
|
|
2603
|
+
filter,
|
|
2604
|
+
{
|
|
2605
|
+
groupBy,
|
|
2606
|
+
measure,
|
|
2607
|
+
...report.interval ? { interval: report.interval } : {},
|
|
2608
|
+
...report.limit ? { limit: capped(report.limit, limits.breakdown) } : {}
|
|
2609
|
+
}
|
|
2610
|
+
],
|
|
2611
|
+
exactness,
|
|
2612
|
+
why: `raw ${measure} by ${groupBy.join(" \xD7 ")} over the range${scanWhy}`
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
if (!report.measure && !report.interval) {
|
|
2616
|
+
return withCompare(report, {
|
|
2617
|
+
primitive: "records",
|
|
2618
|
+
args: [range, filter, report.limit ? { limit: capped(report.limit, limits.records) } : {}],
|
|
2619
|
+
exactness,
|
|
2620
|
+
why: `the matching records themselves, newest first${scanWhy}`
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
const interval = report.interval ?? intervalForRange(report.range, now);
|
|
2624
|
+
return withCompare(report, {
|
|
2625
|
+
primitive: "series",
|
|
2626
|
+
args: [range, filter, { measure, interval }],
|
|
2627
|
+
exactness,
|
|
2628
|
+
why: `raw ${measure} per ${interval} over the range${scanWhy}`
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
var capped = (limit, cap) => cap == null ? limit : Math.max(1, Math.min(limit, cap));
|
|
2632
|
+
var describe = (src) => src.form === "family" ? `family "${src.family.as}"` : src.events.join(", ");
|
|
2633
|
+
var metricList = (catalog, events) => {
|
|
2634
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2635
|
+
for (const n of events) for (const m of catalog.events[n]?.measures ?? []) keys.add(m.key);
|
|
2636
|
+
return keys.size ? [...keys].join(", ") : "nothing but count";
|
|
2637
|
+
};
|
|
2638
|
+
function planFunnel(report, catalog, now, limits) {
|
|
2639
|
+
const stages = report.stages ?? [];
|
|
2640
|
+
if (!stages.length) {
|
|
2641
|
+
return unavailable(
|
|
2642
|
+
"`measure: 'funnel'` needs `stages` \u2014 one or more lifetime `by: ['subject']` rollup family names, in the order a subject reaches them"
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
const anchor = report.anchor ?? stages[0];
|
|
2646
|
+
const exits = report.exits ?? [];
|
|
2647
|
+
for (const as of [...stages, anchor, ...exits]) {
|
|
2648
|
+
const refusal = milestoneRefusal(catalog, as);
|
|
2649
|
+
if (refusal) return unavailable(refusal);
|
|
2650
|
+
}
|
|
2651
|
+
const first = catalog.families[stages[0]];
|
|
2652
|
+
for (const as of [...stages.slice(1), anchor]) {
|
|
2653
|
+
const f = catalog.families[as];
|
|
2654
|
+
if (!sameSet(f.subjectTypes, first.subjectTypes)) {
|
|
2655
|
+
return unavailable(
|
|
2656
|
+
`funnel stages must share one subject type: "${stages[0]}" is declared \`subjects: [${first.subjectTypes.join(", ")}]\` and "${as}" is \`subjects: [${f.subjectTypes.join(", ")}]\` \u2014 two populations cannot convert into each other`
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
if (report.subjectType && !first.subjectTypes.includes(report.subjectType)) {
|
|
2661
|
+
return unavailable(
|
|
2662
|
+
`subjectType "${report.subjectType}" is not one of the stages' subjects (${first.subjectTypes.join(", ")}) \u2014 the cohort would be empty`
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
if (report.interval === "hour") {
|
|
2666
|
+
return unavailable(
|
|
2667
|
+
"funnel slices are day, week or month \u2014 an hourly cohort slice is not offered, because a cohort is assembled from lifetime milestones with no hourly grain to slice on"
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
const params = {
|
|
2671
|
+
stages: stages.map((as) => ({ as })),
|
|
2672
|
+
anchor,
|
|
2673
|
+
...exits.length ? { exits: exits.map((as) => ({ as })) } : {},
|
|
2674
|
+
cohort: rangeOf(report.range, now),
|
|
2675
|
+
...report.subjectType ? { subjectType: report.subjectType } : {},
|
|
2676
|
+
...report.interval ? { interval: report.interval } : {},
|
|
2677
|
+
...report.limit ? { limit: capped(report.limit, limits.funnel) } : {}
|
|
2678
|
+
};
|
|
2679
|
+
return withCompare(report, {
|
|
2680
|
+
primitive: "funnel",
|
|
2681
|
+
args: [params],
|
|
2682
|
+
exactness: "exact",
|
|
2683
|
+
via: anchor,
|
|
2684
|
+
why: `cohort funnel over ${count(stages.length, "lifetime milestone family")}, anchored on "${anchor}" \u2014 rollups only, no raw scan`
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
function milestoneRefusal(catalog, as) {
|
|
2688
|
+
const f = catalog.families[as];
|
|
2689
|
+
if (!f) {
|
|
2690
|
+
return `no rollup family "${as}" is declared. Add a \`rollups: [{ as: '${as}', by: ['subject'], subjects: [...] }]\` block to the event that marks it`;
|
|
2691
|
+
}
|
|
2692
|
+
const shape = `by: [${f.by.map((d) => `'${d}'`).join(", ")}]${f.bucket ? `, bucket: '${f.bucket}'` : ""}`;
|
|
2693
|
+
if (!f.lifetime) {
|
|
2694
|
+
return `rollup family "${as}" (declared on "${f.feeders[0]}") is BUCKETED (${shape}). A milestone needs a lifetime family so \`firstAt\` is the one moment the subject reached it; a bucketed family has one doc per period and would count the same subject repeatedly`;
|
|
2695
|
+
}
|
|
2696
|
+
if (f.by.length !== 1 || f.by[0] !== "subject") {
|
|
2697
|
+
return `rollup family "${as}" (declared on "${f.feeders[0]}") is keyed ${shape}, but a milestone must be keyed by exactly one subject dim (\`by: ['subject']\`). Extra dims split one subject across several docs, which would over-count every stage`;
|
|
2698
|
+
}
|
|
2699
|
+
return null;
|
|
2700
|
+
}
|
|
2701
|
+
function planDistinct(report, catalog, src, measure, now) {
|
|
2702
|
+
const subjectType = measure.slice("distinct:".length);
|
|
2703
|
+
if (!subjectType) {
|
|
2704
|
+
return unavailable(
|
|
2705
|
+
`"${measure}" needs a subject type \u2014 'distinct:account', one of ${catalog.subjectTypes.join(", ")}`
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
if (!catalog.subjectTypes.includes(subjectType)) {
|
|
2709
|
+
return unavailable(
|
|
2710
|
+
`no event or rollup declares the subject type "${subjectType}" \u2014 the registry knows ${catalog.subjectTypes.join(", ") || "no subject types at all"}`
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
if (report.groupBy?.length) {
|
|
2714
|
+
return unavailable(
|
|
2715
|
+
`distinct counts take no groupBy: distinctCount() answers one series per family, and "${report.groupBy.join(", ")}" would need a family keyed by those dims AND \`by: ['subject']\`, which cannot count subjects exactly`
|
|
2716
|
+
);
|
|
2717
|
+
}
|
|
2718
|
+
const wanted = new Set(src.events);
|
|
2719
|
+
const fits = Object.values(catalog.families).filter(
|
|
2720
|
+
(f) => f.bucket != null && f.by.length === 1 && f.by[0] === "subject" && f.subjectTypes.includes(subjectType) && src.events.every((e) => f.feeders.includes(e))
|
|
2721
|
+
);
|
|
2722
|
+
const family = fits.find((f) => sameSet(f.feeders, [...wanted])) ?? fits[0];
|
|
2723
|
+
if (!family) {
|
|
2724
|
+
return unavailable(
|
|
2725
|
+
`no bucketed \`by: ['subject']\` family covers ${src.events.join(", ")} for subject type "${subjectType}" \u2014 declare \`rollups: [{ as: 'activity', by: ['subject'], subjects: ['${subjectType}'], bucket: 'day' }]\` on the events that count as activity`
|
|
2726
|
+
);
|
|
2727
|
+
}
|
|
2728
|
+
const superset = !sameSet(family.feeders, [...wanted]);
|
|
2729
|
+
return withCompare(report, {
|
|
2730
|
+
primitive: "distinctCount",
|
|
2731
|
+
args: [
|
|
2732
|
+
{
|
|
2733
|
+
as: family.as,
|
|
2734
|
+
subjectType,
|
|
2735
|
+
range: rangeOf(report.range, now),
|
|
2736
|
+
...report.interval ? { interval: report.interval } : {}
|
|
2737
|
+
}
|
|
2738
|
+
],
|
|
2739
|
+
exactness: "exact",
|
|
2740
|
+
via: family.as,
|
|
2741
|
+
why: `"${family.as}" writes exactly one doc per (subject, ${family.bucket}), so distinct subjects IS the doc count \u2014 exact, no sketch` + (superset ? `. Its feeders (${family.feeders.join(", ")}) are a SUPERSET of the source (${src.events.join(", ")}), so the count includes subjects who only did the others` : "")
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
var famDims = (f) => f.by.map((b) => b === "subject" ? "subjectType" : b);
|
|
2745
|
+
function planRollups(report, catalog, src, measure, groupBy, now, limits) {
|
|
2746
|
+
if (report.excludeActorTypes?.length) return null;
|
|
2747
|
+
const candidates = src.family ? [src.family] : Object.values(catalog.families).filter((f) => sameSet(f.feeders, src.events));
|
|
2748
|
+
const op = MEASURE_OP.exec(measure);
|
|
2749
|
+
const filters = report.filters ?? [];
|
|
2750
|
+
const fits = candidates.filter((f) => {
|
|
2751
|
+
const dims2 = famDims(f);
|
|
2752
|
+
if (!groupBy.every((k) => dims2.includes(k))) return false;
|
|
2753
|
+
if (report.interval && (!f.bucket || INTERVAL_RANK[f.bucket] > INTERVAL_RANK[report.interval])) return false;
|
|
2754
|
+
if (op) {
|
|
2755
|
+
if (op[1] !== "sum" && op[1] !== "avg" || !f.sums.includes(op[2])) return false;
|
|
2756
|
+
}
|
|
2757
|
+
return filters.every(
|
|
2758
|
+
(t) => dims2.includes(t.dim) && (t.op === "eq" || t.op === "in") || nameFilterCovers(t, f)
|
|
2759
|
+
);
|
|
2760
|
+
});
|
|
2761
|
+
const family = fits.sort((a, b) => a.by.length - b.by.length)[0];
|
|
2762
|
+
if (!family) return null;
|
|
2763
|
+
const dims = famDims(family);
|
|
2764
|
+
const range = rangeOf(report.range, now);
|
|
2765
|
+
const on = family.lifetime ? "firstAt" : "bucketAt";
|
|
2766
|
+
const fold = filters.filter((t) => dims.includes(t.dim));
|
|
2767
|
+
return withCompare(report, {
|
|
2768
|
+
primitive: "rollups",
|
|
2769
|
+
args: [
|
|
2770
|
+
{
|
|
2771
|
+
as: family.as,
|
|
2772
|
+
on,
|
|
2773
|
+
range,
|
|
2774
|
+
sort: family.lifetime ? "count" : "bucketAt",
|
|
2775
|
+
...report.limit ? { limit: capped(report.limit, limits.rollups) } : {}
|
|
2776
|
+
}
|
|
2777
|
+
],
|
|
2778
|
+
exactness: "exact",
|
|
2779
|
+
via: family.as,
|
|
2780
|
+
shape: {
|
|
2781
|
+
groupBy,
|
|
2782
|
+
labels: groupBy.map((k) => family.labels[dims.indexOf(k)]),
|
|
2783
|
+
measure,
|
|
2784
|
+
...report.interval ? { interval: report.interval } : {},
|
|
2785
|
+
...fold.length ? { filters: fold.map((t) => ({ ...t, label: family.labels[dims.indexOf(t.dim)] })) } : {}
|
|
2786
|
+
},
|
|
2787
|
+
why: `family "${family.as}" is keyed by ${dims.join(", ") || "nothing but its feeders"} and maintained on write, so this is one indexed rollup read over \`${on}\` instead of a raw scan` + (op?.[1] === "avg" ? `. avg:${op[2]} is exact off a rollup: the executor divides sums.${op[2]} by count` : "") + (report.interval && family.bucket !== report.interval ? `. Its ${family.bucket} buckets roll up into ${report.interval} without splitting one` : "")
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
function nameFilterCovers(t, f) {
|
|
2791
|
+
if (t.dim !== "field:name") return false;
|
|
2792
|
+
const admitted = t.op === "eq" ? [String(t.value)] : t.op === "in" ? [...t.value].map(String) : null;
|
|
2793
|
+
return admitted != null && f.feeders.every((n) => admitted.includes(n));
|
|
2794
|
+
}
|
|
2795
|
+
var FIELD_TERMS = {
|
|
2796
|
+
"field:kind": "kind",
|
|
2797
|
+
"field:name": "name",
|
|
2798
|
+
"field:severity": "severity",
|
|
2799
|
+
"field:env": "env",
|
|
2800
|
+
"field:service": "service",
|
|
2801
|
+
"field:release": "release",
|
|
2802
|
+
...FILTER_ONLY
|
|
2803
|
+
};
|
|
2804
|
+
function toRecordFilter(report, catalog, src) {
|
|
2805
|
+
const filter = {};
|
|
2806
|
+
if (src.events.length === 1) {
|
|
2807
|
+
filter.name = src.events[0];
|
|
2808
|
+
} else if (src.kind) {
|
|
2809
|
+
filter.kind = src.kind;
|
|
2810
|
+
} else {
|
|
2811
|
+
filter.name = [...src.events];
|
|
2812
|
+
}
|
|
2813
|
+
for (const t of report.filters ?? []) {
|
|
2814
|
+
const term = FIELD_TERMS[t.dim];
|
|
2815
|
+
if (term) {
|
|
2816
|
+
if (t.op !== "eq") {
|
|
2817
|
+
return unavailable(
|
|
2818
|
+
`"${t.dim}" supports equality only on the raw path \u2014 RecordFilter.${String(term)} is one string, and "${t.op}" would need a term query.ts does not build`
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
filter[term] = String(t.value);
|
|
2822
|
+
continue;
|
|
2823
|
+
}
|
|
2824
|
+
if (t.dim.startsWith("attr:")) {
|
|
2825
|
+
const key = t.dim.slice(5);
|
|
2826
|
+
if (t.op === "eq") {
|
|
2827
|
+
(filter.attrs ??= {})[key] = String(t.value);
|
|
2828
|
+
continue;
|
|
2829
|
+
}
|
|
2830
|
+
if (t.op === "gte" || t.op === "lte") {
|
|
2831
|
+
if (!measureDeclared(catalog, src.events, `sum:${key}`)) {
|
|
2832
|
+
return unavailable(
|
|
2833
|
+
`"${t.dim} ${t.op}" is a numeric bound, which only a declared metric can carry \u2014 add \`${key}\` to the \`metrics\` object of ${src.events.join(", ")}, or filter it as an equality`
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2836
|
+
const range = (filter.metrics ??= {})[key] ??= {};
|
|
2837
|
+
range[t.op] = Number(t.value);
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
return unavailable(
|
|
2841
|
+
`"${t.dim}" supports equality (or a gte/lte bound on a metric) \u2014 an \`in\` over attrs would need a \`$in\` term buildMatch does not write`
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
if (t.dim === "subjectType" || t.dim === "actorType") {
|
|
2845
|
+
return unavailable(
|
|
2846
|
+
`"${t.dim}" is derived at query time from ${t.dim === "subjectType" ? "`subjectKeys`" : "`actor`"} and RecordFilter has no term for it \u2014 group by it instead, pin one subject with \`field:subject\`` + (t.dim === "actorType" ? ", or use `excludeActorTypes`" : "")
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
2849
|
+
return unavailable(
|
|
2850
|
+
`"${t.dim}" is not filterable on the raw path \u2014 RecordFilter carries ${Object.keys(FIELD_TERMS).join(", ")}, \`attr:<key>\` and metric bounds`
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
if (report.excludeActorTypes?.length) filter.excludeActorTypes = [...report.excludeActorTypes];
|
|
2854
|
+
return filter;
|
|
2855
|
+
}
|
|
2856
|
+
function withCompare(report, plan) {
|
|
2857
|
+
if (report.compare !== "previous") return plan;
|
|
2858
|
+
const [first, ...rest] = plan.args;
|
|
2859
|
+
if (plan.primitive === "rollups" || plan.primitive === "distinctCount" || plan.primitive === "funnel") {
|
|
2860
|
+
const params = first;
|
|
2861
|
+
const key = plan.primitive === "funnel" ? "cohort" : "range";
|
|
2862
|
+
const window = params[key];
|
|
2863
|
+
if (!window) return plan;
|
|
2864
|
+
return { ...plan, previous: { args: [{ ...params, [key]: { ...window, ...shift(window) } }, ...rest] } };
|
|
2865
|
+
}
|
|
2866
|
+
return { ...plan, previous: { args: [shift(first), ...rest] } };
|
|
2867
|
+
}
|
|
2868
|
+
var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
2869
|
+
var FILTER_OPS = /* @__PURE__ */ new Set(["eq", "in", "gte", "lte"]);
|
|
2870
|
+
var SORTS = /* @__PURE__ */ new Set(["value", "label", "time"]);
|
|
2871
|
+
function parseReportQuery(q) {
|
|
2872
|
+
const str = (k) => {
|
|
2873
|
+
const v = Array.isArray(q[k]) ? q[k][0] : q[k];
|
|
2874
|
+
return typeof v === "string" && v ? v : void 0;
|
|
2875
|
+
};
|
|
2876
|
+
const list = (k) => (str(k) ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
2877
|
+
const raw = str("source");
|
|
2878
|
+
if (!raw) {
|
|
2879
|
+
throw badRequest2(
|
|
2880
|
+
"`source` is required \u2014 one of source=event:<name>, namespace:<ns>, kind:<kind>, family:<as>"
|
|
2881
|
+
);
|
|
2882
|
+
}
|
|
2883
|
+
const cut = raw.indexOf(":");
|
|
2884
|
+
const form = cut > 0 ? raw.slice(0, cut) : "";
|
|
2885
|
+
const named = cut > 0 ? raw.slice(cut + 1) : "";
|
|
2886
|
+
if (!named || !["event", "namespace", "kind", "family"].includes(form)) {
|
|
2887
|
+
throw badRequest2(
|
|
2888
|
+
`\`source\` must be "event:<name>", "namespace:<ns>", "kind:<kind>" or "family:<as>" \u2014 got "${raw}"`
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
const source = form === "event" ? { event: named } : form === "namespace" ? { namespace: named } : form === "kind" ? { kind: named } : { family: named };
|
|
2892
|
+
const shorthand = str("range");
|
|
2893
|
+
const from = str("from");
|
|
2894
|
+
const to = str("to");
|
|
2895
|
+
if (!shorthand && !(from && to)) {
|
|
2896
|
+
throw badRequest2("a range is required \u2014 either `range=7d` or both `from` and `to` as ISO times");
|
|
2897
|
+
}
|
|
2898
|
+
const range = shorthand ?? { from, to };
|
|
2899
|
+
const interval = str("interval");
|
|
2900
|
+
if (interval && INTERVAL_RANK[interval] == null) {
|
|
2901
|
+
throw badRequest2(`\`interval\` must be one of hour, day, week, month \u2014 got "${interval}"`);
|
|
2902
|
+
}
|
|
2903
|
+
const sort = str("sort");
|
|
2904
|
+
if (sort && !SORTS.has(sort)) {
|
|
2905
|
+
throw badRequest2(`\`sort\` must be one of value, label, time \u2014 got "${sort}"`);
|
|
2906
|
+
}
|
|
2907
|
+
const compare = str("compare");
|
|
2908
|
+
if (compare && compare !== "previous") {
|
|
2909
|
+
throw badRequest2(`\`compare\` takes only "previous" \u2014 got "${compare}"`);
|
|
2910
|
+
}
|
|
2911
|
+
const limitRaw = str("limit");
|
|
2912
|
+
const limit = limitRaw == null ? void 0 : Number(limitRaw);
|
|
2913
|
+
if (limit != null && (!Number.isInteger(limit) || limit < 1)) {
|
|
2914
|
+
throw badRequest2(`\`limit\` must be a positive integer \u2014 got "${limitRaw}"`);
|
|
2915
|
+
}
|
|
2916
|
+
const measure = str("measure");
|
|
2917
|
+
const groupBy = list("groupBy");
|
|
2918
|
+
const excludeActorTypes = list("excludeActors");
|
|
2919
|
+
const stages = list("stages");
|
|
2920
|
+
const exits = list("exits");
|
|
2921
|
+
const anchor = str("anchor");
|
|
2922
|
+
const subjectType = str("subjectType");
|
|
2923
|
+
const filters = (q.filter == null ? [] : Array.isArray(q.filter) ? q.filter.map(String) : [String(q.filter)]).map(parseFilterTerm);
|
|
2924
|
+
return {
|
|
2925
|
+
source,
|
|
2926
|
+
range,
|
|
2927
|
+
...interval ? { interval } : {},
|
|
2928
|
+
...measure ? { measure } : {},
|
|
2929
|
+
...groupBy.length ? { groupBy } : {},
|
|
2930
|
+
...filters.length ? { filters } : {},
|
|
2931
|
+
...excludeActorTypes.length ? { excludeActorTypes } : {},
|
|
2932
|
+
...sort ? { sort } : {},
|
|
2933
|
+
...limit != null ? { limit } : {},
|
|
2934
|
+
...compare ? { compare: "previous" } : {},
|
|
2935
|
+
...stages.length ? { stages } : {},
|
|
2936
|
+
...anchor ? { anchor } : {},
|
|
2937
|
+
...exits.length ? { exits } : {},
|
|
2938
|
+
...subjectType ? { subjectType } : {}
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
function parseFilterTerm(term) {
|
|
2942
|
+
const parts = term.split(":");
|
|
2943
|
+
const i = parts.findIndex((p) => FILTER_OPS.has(p));
|
|
2944
|
+
const rest = i < 0 ? "" : parts.slice(i + 1).join(":");
|
|
2945
|
+
if (i < 1 || !rest) {
|
|
2946
|
+
throw badRequest2(
|
|
2947
|
+
`\`filter\` must be "<dim>:<op>:<value>" with op one of eq, in, gte, lte \u2014 got "${term}"`
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2950
|
+
const dim2 = parts.slice(0, i).join(":");
|
|
2951
|
+
const op = parts[i];
|
|
2952
|
+
if (op === "in") {
|
|
2953
|
+
const values = rest.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2954
|
+
if (!values.length) throw badRequest2(`\`filter\` "${term}" has an empty \`in\` list`);
|
|
2955
|
+
return { dim: dim2, op, value: values };
|
|
2956
|
+
}
|
|
2957
|
+
if (op === "gte" || op === "lte") {
|
|
2958
|
+
const n = Number(rest);
|
|
2959
|
+
if (Number.isNaN(n)) throw badRequest2(`\`filter\` bound "${term}" is not a number`);
|
|
2960
|
+
return { dim: dim2, op, value: n };
|
|
2961
|
+
}
|
|
2962
|
+
return { dim: dim2, op, value: rest };
|
|
2963
|
+
}
|
|
2964
|
+
function reportToQuery(report) {
|
|
2965
|
+
const s = report.source;
|
|
2966
|
+
const q = {
|
|
2967
|
+
source: "event" in s ? `event:${s.event}` : "namespace" in s ? `namespace:${s.namespace}` : "kind" in s ? `kind:${s.kind}` : `family:${s.family}`
|
|
2968
|
+
};
|
|
2969
|
+
if (typeof report.range === "string") q.range = report.range;
|
|
2970
|
+
else {
|
|
2971
|
+
q.from = report.range.from;
|
|
2972
|
+
q.to = report.range.to;
|
|
2973
|
+
}
|
|
2974
|
+
if (report.interval) q.interval = report.interval;
|
|
2975
|
+
if (report.measure) q.measure = report.measure;
|
|
2976
|
+
if (report.groupBy?.length) q.groupBy = report.groupBy.join(",");
|
|
2977
|
+
if (report.filters?.length) {
|
|
2978
|
+
const terms = report.filters.map(
|
|
2979
|
+
(f) => `${f.dim}:${f.op}:${Array.isArray(f.value) ? f.value.join(",") : String(f.value)}`
|
|
2980
|
+
);
|
|
2981
|
+
q.filter = terms.length === 1 ? terms[0] : terms;
|
|
2982
|
+
}
|
|
2983
|
+
if (report.excludeActorTypes?.length) q.excludeActors = report.excludeActorTypes.join(",");
|
|
2984
|
+
if (report.sort) q.sort = report.sort;
|
|
2985
|
+
if (report.limit != null) q.limit = String(report.limit);
|
|
2986
|
+
if (report.compare) q.compare = report.compare;
|
|
2987
|
+
if (report.stages?.length) q.stages = report.stages.join(",");
|
|
2988
|
+
if (report.anchor) q.anchor = report.anchor;
|
|
2989
|
+
if (report.exits?.length) q.exits = report.exits.join(",");
|
|
2990
|
+
if (report.subjectType) q.subjectType = report.subjectType;
|
|
2991
|
+
return q;
|
|
2992
|
+
}
|
|
2993
|
+
var LEGACY_DIMS = {
|
|
2994
|
+
kind: "field:kind",
|
|
2995
|
+
name: "field:name",
|
|
2996
|
+
severity: "field:severity",
|
|
2997
|
+
env: "field:env",
|
|
2998
|
+
service: "field:service",
|
|
2999
|
+
release: "field:release",
|
|
3000
|
+
subject: "field:subject",
|
|
3001
|
+
traceId: "field:traceId"
|
|
3002
|
+
};
|
|
3003
|
+
function normalizeQuery(query) {
|
|
3004
|
+
if (!query || typeof query !== "object") return null;
|
|
3005
|
+
if ("source" in query && query.source) return query;
|
|
3006
|
+
const q = query;
|
|
3007
|
+
const filters = q.filters ?? {};
|
|
3008
|
+
const str = (v) => typeof v === "string" && v ? v : null;
|
|
3009
|
+
const name = str(filters.name);
|
|
3010
|
+
const family = str(filters.rollup);
|
|
3011
|
+
const kind = str(filters.kind);
|
|
3012
|
+
const source = name ? { event: name } : family ? { family } : kind ? { kind } : null;
|
|
3013
|
+
if (!source) return null;
|
|
3014
|
+
const consumed = name ? "name" : family ? "rollup" : "kind";
|
|
3015
|
+
const terms = [];
|
|
3016
|
+
for (const [k, v] of Object.entries(filters)) {
|
|
3017
|
+
if (k === consumed || k === "rollup") continue;
|
|
3018
|
+
if (k === "excludeActorTypes") continue;
|
|
3019
|
+
if (k === "attrs") {
|
|
3020
|
+
const entries = typeof v === "string" ? v.split(",").map((pair) => pair.split(":").map((s) => s.trim())) : Object.entries(v ?? {}).map(([a, b]) => [a, String(b)]);
|
|
3021
|
+
for (const [key, value2] of entries) {
|
|
3022
|
+
if (key && value2 != null) terms.push({ dim: `attr:${key}`, op: "eq", value: String(value2) });
|
|
3023
|
+
}
|
|
3024
|
+
continue;
|
|
3025
|
+
}
|
|
3026
|
+
const dim2 = LEGACY_DIMS[k];
|
|
3027
|
+
const value = str(v);
|
|
3028
|
+
if (dim2 && value) terms.push({ dim: dim2, op: "eq", value });
|
|
3029
|
+
}
|
|
3030
|
+
const groupBy = (q.groupBy ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3031
|
+
const sort = q.sort === "value" || q.sort === "label" || q.sort === "time" ? q.sort : void 0;
|
|
3032
|
+
const actors = filters.excludeActorTypes;
|
|
3033
|
+
return {
|
|
3034
|
+
source,
|
|
3035
|
+
range: q.range ?? "7d",
|
|
3036
|
+
...terms.length ? { filters: terms } : {},
|
|
3037
|
+
...groupBy.length ? { groupBy } : {},
|
|
3038
|
+
...sort ? { sort } : {},
|
|
3039
|
+
...Array.isArray(actors) && actors.length ? { excludeActorTypes: actors.map(String) } : {}
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
// src/server/execute.ts
|
|
3044
|
+
async function executeReport(q, scope, report, catalog, opts = {}) {
|
|
3045
|
+
const plan = resolveReport(report, catalog, { now: opts.now, limits: opts.limits });
|
|
3046
|
+
if ("unavailable" in plan) throw Object.assign(new Error(plan.why), { status: 400 });
|
|
3047
|
+
const run = async (args) => {
|
|
3048
|
+
const raw = await q[plan.primitive](scope, ...args);
|
|
3049
|
+
if (plan.primitive === "rollups" && plan.shape) {
|
|
3050
|
+
return foldRollups(raw?.rows ?? [], plan.shape, !!raw?.truncated);
|
|
3051
|
+
}
|
|
3052
|
+
if (plan.primitive === "records" && opts.redact) {
|
|
3053
|
+
return { ...raw, items: opts.redact(raw?.items ?? []) };
|
|
3054
|
+
}
|
|
3055
|
+
return raw;
|
|
3056
|
+
};
|
|
3057
|
+
const [result, previous] = await Promise.all([
|
|
3058
|
+
run(plan.args),
|
|
3059
|
+
plan.previous ? run(plan.previous.args) : Promise.resolve(void 0)
|
|
3060
|
+
]);
|
|
3061
|
+
return {
|
|
3062
|
+
report,
|
|
3063
|
+
plan,
|
|
3064
|
+
result,
|
|
3065
|
+
...plan.previous ? { previous } : {},
|
|
3066
|
+
dataSource: result?.dataSource ?? "raw"
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
var MEASURE_OP2 = /^(sum|avg):(.+)$/;
|
|
3070
|
+
function foldRollups(rows, shape, truncated = false) {
|
|
3071
|
+
const op = MEASURE_OP2.exec(shape.measure);
|
|
3072
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3073
|
+
for (const doc of rows) {
|
|
3074
|
+
const dims = doc?.dims ?? [];
|
|
3075
|
+
if (!(shape.filters ?? []).every((f) => admits(f, dimValue(dims, f.label)))) continue;
|
|
3076
|
+
const tuple = shape.labels.map((label3) => dimValue(dims, label3));
|
|
3077
|
+
const at = shape.interval && doc.bucketAt ? truncate(new Date(doc.bucketAt), shape.interval) : void 0;
|
|
3078
|
+
const key = `${JSON.stringify(tuple)}|${at ? at.getTime() : ""}`;
|
|
3079
|
+
let g = groups.get(key);
|
|
3080
|
+
if (!g) groups.set(key, g = { dims: tuple, ...at ? { at } : {}, sum: 0, count: 0 });
|
|
3081
|
+
g.count += typeof doc.count === "number" ? doc.count : 0;
|
|
3082
|
+
if (op) g.sum += sumOf(doc.sums, op[2]);
|
|
3083
|
+
}
|
|
3084
|
+
const rowsOut = [...groups.values()].map((g) => ({
|
|
3085
|
+
dims: g.dims,
|
|
3086
|
+
...g.at ? { at: g.at } : {},
|
|
3087
|
+
// avg is sums[k]/count off the SAME doc, which is exact — not an average of
|
|
3088
|
+
// averages, which is what folding a per-bucket mean would have produced
|
|
3089
|
+
value: !op ? g.count : op[1] === "sum" ? g.sum : g.count ? g.sum / g.count : 0
|
|
3090
|
+
}));
|
|
3091
|
+
rowsOut.sort(
|
|
3092
|
+
shape.interval ? (a, b) => (a.at?.getTime() ?? 0) - (b.at?.getTime() ?? 0) || byDims(a, b) : (a, b) => b.value - a.value || byDims(a, b)
|
|
3093
|
+
);
|
|
3094
|
+
return {
|
|
3095
|
+
rows: rowsOut,
|
|
3096
|
+
groups: new Set([...groups.values()].map((g) => JSON.stringify(g.dims))).size,
|
|
3097
|
+
truncated,
|
|
3098
|
+
dataSource: "rollups"
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
function dimValue(dims, label3) {
|
|
3102
|
+
const prefix = `${label3}=`;
|
|
3103
|
+
for (const d of dims) if (d.startsWith(prefix)) return d.slice(prefix.length);
|
|
3104
|
+
for (const d of dims) if (!d.includes("=")) return d;
|
|
3105
|
+
return null;
|
|
3106
|
+
}
|
|
3107
|
+
function admits(f, value) {
|
|
3108
|
+
if (f.op === "in") return f.value.map(String).includes(String(value));
|
|
3109
|
+
if (f.op === "gte" || f.op === "lte") {
|
|
3110
|
+
const n = Number(value);
|
|
3111
|
+
if (Number.isNaN(n)) return false;
|
|
3112
|
+
return f.op === "gte" ? n >= Number(f.value) : n <= Number(f.value);
|
|
3113
|
+
}
|
|
3114
|
+
return String(value) === String(f.value);
|
|
3115
|
+
}
|
|
3116
|
+
function sumOf(sums, key) {
|
|
3117
|
+
if (!sums) return 0;
|
|
3118
|
+
const v = sums instanceof Map ? sums.get(key) : sums[key];
|
|
3119
|
+
return typeof v === "number" ? v : 0;
|
|
3120
|
+
}
|
|
3121
|
+
function byDims(a, b) {
|
|
3122
|
+
for (let i = 0; i < a.dims.length; i++) {
|
|
3123
|
+
const x = a.dims[i] ?? "";
|
|
3124
|
+
const y = b.dims[i] ?? "";
|
|
3125
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
3126
|
+
}
|
|
3127
|
+
return 0;
|
|
3128
|
+
}
|
|
1830
3129
|
function buildViewModel(connection, modelName, collection) {
|
|
1831
3130
|
const existing = connection.models?.[modelName];
|
|
1832
3131
|
if (existing) return existing;
|
|
@@ -1853,31 +3152,58 @@ var KIND_PAGE = {
|
|
|
1853
3152
|
state: "journeys",
|
|
1854
3153
|
usage: "usage"
|
|
1855
3154
|
};
|
|
1856
|
-
function deriveViews(registry) {
|
|
3155
|
+
function deriveViews(registry, catalog = deriveCatalog(registry)) {
|
|
1857
3156
|
const views = [];
|
|
1858
|
-
const
|
|
1859
|
-
for (const [name,
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
query: { range: "7d", filters: { name }, display: spec.kind === "event" ? "series" : "table" }
|
|
3157
|
+
const derived = (name, page, query) => views.push({ origin: "derived", name, page, query });
|
|
3158
|
+
for (const [name, e] of Object.entries(catalog.events)) {
|
|
3159
|
+
derived(name, KIND_PAGE[e.kind] ?? "events", {
|
|
3160
|
+
source: { event: name },
|
|
3161
|
+
range: "7d",
|
|
3162
|
+
interval: intervalForRange("7d")
|
|
1865
3163
|
});
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
3164
|
+
}
|
|
3165
|
+
for (const as of Object.keys(catalog.families)) {
|
|
3166
|
+
derived(`rollup: ${as}`, "journeys", { source: { family: as }, range: "30d" });
|
|
3167
|
+
}
|
|
3168
|
+
for (const [ns, names] of Object.entries(catalog.namespaces)) {
|
|
3169
|
+
if (names.length < 2) continue;
|
|
3170
|
+
derived(`namespace: ${ns}`, "explore", {
|
|
3171
|
+
source: { namespace: ns },
|
|
3172
|
+
range: "30d",
|
|
3173
|
+
interval: "day",
|
|
3174
|
+
groupBy: ["field:name"]
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
for (const [name, e] of Object.entries(catalog.events)) {
|
|
3178
|
+
if (e.kind !== "usage") continue;
|
|
3179
|
+
const money = e.measures.find((m) => m.key.startsWith("sum:") && m.key.endsWith("_usd"));
|
|
3180
|
+
if (!money) continue;
|
|
3181
|
+
derived(`spend: ${name}`, "usage", {
|
|
3182
|
+
source: { event: name },
|
|
3183
|
+
range: "30d",
|
|
3184
|
+
interval: "day",
|
|
3185
|
+
measure: money.key
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
for (const subjectType of catalog.subjectTypes) {
|
|
3189
|
+
const stages = Object.values(catalog.families).filter((f) => f.lifetime && f.by.length === 1 && f.by[0] === "subject" && f.subjectTypes.includes(subjectType)).map((f) => f.as);
|
|
3190
|
+
if (stages.length < 2) continue;
|
|
3191
|
+
derived(`funnel: ${subjectType}`, "journeys", {
|
|
3192
|
+
// any source expands; the family the funnel is anchored on is the honest one
|
|
3193
|
+
source: { family: stages[0] },
|
|
3194
|
+
range: "30d",
|
|
3195
|
+
interval: "week",
|
|
3196
|
+
measure: "funnel",
|
|
3197
|
+
stages,
|
|
3198
|
+
anchor: stages[0],
|
|
3199
|
+
subjectType
|
|
1874
3200
|
});
|
|
1875
3201
|
}
|
|
1876
3202
|
return views;
|
|
1877
3203
|
}
|
|
1878
3204
|
async function resolveViews(opts) {
|
|
1879
3205
|
const byName = /* @__PURE__ */ new Map();
|
|
1880
|
-
for (const v of deriveViews(opts.registry)) byName.set(v.name, v);
|
|
3206
|
+
for (const v of deriveViews(opts.registry, opts.catalog)) byName.set(v.name, v);
|
|
1881
3207
|
for (const v of opts.configured) byName.set(v.name, { ...v, origin: "configured" });
|
|
1882
3208
|
const saved = await opts.ViewModel.find({
|
|
1883
3209
|
tenantId: opts.tenantId,
|
|
@@ -1936,6 +3262,10 @@ var parseFilter = (q) => {
|
|
|
1936
3262
|
for (const k of ["kind", "name", "severity", "env", "service", "release", "subject", "traceId"]) {
|
|
1937
3263
|
if (typeof q[k] === "string" && q[k]) f[k] = q[k];
|
|
1938
3264
|
}
|
|
3265
|
+
if (typeof q.name === "string" && q.name.includes(",")) {
|
|
3266
|
+
const names = q.name.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3267
|
+
if (names.length) f.name = names.length === 1 ? names[0] : names;
|
|
3268
|
+
}
|
|
1939
3269
|
if (typeof q.attrs === "string" && q.attrs) {
|
|
1940
3270
|
f.attrs = Object.fromEntries(
|
|
1941
3271
|
String(q.attrs).split(",").map((p) => p.split(":")).filter((p) => p.length >= 2).map(([k, ...v]) => [k, v.join(":")])
|
|
@@ -1967,30 +3297,6 @@ var parseDims = (v) => {
|
|
|
1967
3297
|
}
|
|
1968
3298
|
return v.length ? v : void 0;
|
|
1969
3299
|
};
|
|
1970
|
-
function registryProjection(t) {
|
|
1971
|
-
return Object.fromEntries(
|
|
1972
|
-
Object.entries(t.registry).map(([name, spec]) => [
|
|
1973
|
-
name,
|
|
1974
|
-
{
|
|
1975
|
-
kind: spec.kind,
|
|
1976
|
-
origin: spec.origin,
|
|
1977
|
-
subjects: spec.subjects,
|
|
1978
|
-
description: spec.description,
|
|
1979
|
-
attrKeys: spec.attrs ? Object.keys(spec.attrs.shape) : [],
|
|
1980
|
-
metricKeys: spec.metrics ? Object.keys(spec.metrics.shape) : [],
|
|
1981
|
-
indexedAttrs: spec.indexedAttrs ?? [],
|
|
1982
|
-
indexedMetrics: spec.indexedMetrics ?? [],
|
|
1983
|
-
rollups: (spec.rollups ?? []).map((r) => ({
|
|
1984
|
-
as: r.as ?? name,
|
|
1985
|
-
by: r.by,
|
|
1986
|
-
bucket: r.bucket ?? null,
|
|
1987
|
-
sum: r.sum ?? [],
|
|
1988
|
-
subjects: r.subjects ?? []
|
|
1989
|
-
}))
|
|
1990
|
-
}
|
|
1991
|
-
])
|
|
1992
|
-
);
|
|
1993
|
-
}
|
|
1994
3300
|
function createDashboard(opts) {
|
|
1995
3301
|
const { telemetry: t, viewerAdapter, subjectAdapter, views: configured = [] } = opts;
|
|
1996
3302
|
if (!viewerAdapter?.resolveViewer) {
|
|
@@ -2020,6 +3326,20 @@ function createDashboard(opts) {
|
|
|
2020
3326
|
cacheTtlMs: opts.cacheTtlMs,
|
|
2021
3327
|
cacheSize: opts.cacheSize
|
|
2022
3328
|
});
|
|
3329
|
+
const catalog = deriveCatalog(t.registry, {
|
|
3330
|
+
platforms: t.models.telemetry.schema.path("client")?.schema?.path("platform")?.enumValues
|
|
3331
|
+
});
|
|
3332
|
+
const registry = projectRegistry(catalog);
|
|
3333
|
+
const values = createValues({
|
|
3334
|
+
catalog,
|
|
3335
|
+
TelemetryModel: t.models.telemetry,
|
|
3336
|
+
RollupModel: t.models.rollups,
|
|
3337
|
+
limits: opts.queryLimits,
|
|
3338
|
+
onSlowQuery: opts.onSlowQuery,
|
|
3339
|
+
slowMs: opts.slowMs,
|
|
3340
|
+
cacheTtlMs: opts.cacheTtlMs,
|
|
3341
|
+
cacheSize: opts.cacheSize
|
|
3342
|
+
});
|
|
2023
3343
|
const api = express2.Router();
|
|
2024
3344
|
api.use(express2.json({ limit: "64kb" }));
|
|
2025
3345
|
api.use(async (req, res, next) => {
|
|
@@ -2038,7 +3358,8 @@ function createDashboard(opts) {
|
|
|
2038
3358
|
}, next);
|
|
2039
3359
|
};
|
|
2040
3360
|
api.get("/registry", h(async (req) => ({
|
|
2041
|
-
registry
|
|
3361
|
+
registry,
|
|
3362
|
+
catalog,
|
|
2042
3363
|
kinds: ["event", "error", "span", "state", "usage"],
|
|
2043
3364
|
role: req.viewer.role,
|
|
2044
3365
|
scope: req.viewer.tenantId,
|
|
@@ -2061,6 +3382,14 @@ function createDashboard(opts) {
|
|
|
2061
3382
|
measure: typeof req.query.measure === "string" ? req.query.measure : void 0
|
|
2062
3383
|
})
|
|
2063
3384
|
));
|
|
3385
|
+
api.get("/breakdown", h(
|
|
3386
|
+
async (req) => q.breakdown(req.viewer.tenantId, parseRange(req.query), parseFilter(req.query), {
|
|
3387
|
+
groupBy: String(req.query.groupBy ?? "").split(",").map((s) => s.trim()).filter(Boolean),
|
|
3388
|
+
measure: typeof req.query.measure === "string" ? req.query.measure : void 0,
|
|
3389
|
+
interval: req.query.interval || void 0,
|
|
3390
|
+
limit: req.query.limit ? Number(req.query.limit) : void 0
|
|
3391
|
+
})
|
|
3392
|
+
));
|
|
2064
3393
|
api.get("/rollups", h(async (req) => {
|
|
2065
3394
|
if (typeof req.query.as !== "string" || !req.query.as) {
|
|
2066
3395
|
throw Object.assign(new Error("rollup family required"), { status: 400 });
|
|
@@ -2091,7 +3420,7 @@ function createDashboard(opts) {
|
|
|
2091
3420
|
limit: req.query.limit ? Number(req.query.limit) : void 0
|
|
2092
3421
|
})
|
|
2093
3422
|
));
|
|
2094
|
-
const
|
|
3423
|
+
const badRequest3 = async (run) => {
|
|
2095
3424
|
try {
|
|
2096
3425
|
return await run();
|
|
2097
3426
|
} catch (e) {
|
|
@@ -2104,7 +3433,7 @@ function createDashboard(opts) {
|
|
|
2104
3433
|
if (!stages.length) {
|
|
2105
3434
|
throw Object.assign(new Error("funnel needs `stages` \u2014 a comma-separated list of rollup families"), { status: 400 });
|
|
2106
3435
|
}
|
|
2107
|
-
return
|
|
3436
|
+
return badRequest3(() => q.funnel(req.viewer.tenantId, {
|
|
2108
3437
|
stages,
|
|
2109
3438
|
exits: parseStages(req.query.exits),
|
|
2110
3439
|
anchor: typeof req.query.anchor === "string" ? req.query.anchor : void 0,
|
|
@@ -2119,22 +3448,46 @@ function createDashboard(opts) {
|
|
|
2119
3448
|
if (typeof req.query.as !== "string" || !req.query.as) {
|
|
2120
3449
|
throw Object.assign(new Error("rollup family required"), { status: 400 });
|
|
2121
3450
|
}
|
|
2122
|
-
return
|
|
3451
|
+
return badRequest3(() => q.distinctCount(req.viewer.tenantId, {
|
|
2123
3452
|
as: req.query.as,
|
|
2124
3453
|
subjectType: typeof req.query.subjectType === "string" ? req.query.subjectType : void 0,
|
|
2125
3454
|
range: parseRange(req.query),
|
|
2126
3455
|
interval: req.query.interval || void 0
|
|
2127
3456
|
}));
|
|
2128
3457
|
}));
|
|
3458
|
+
api.get("/values", h(async (req) => {
|
|
3459
|
+
const dim2 = typeof req.query.dim === "string" ? req.query.dim.trim() : "";
|
|
3460
|
+
if (!dim2) {
|
|
3461
|
+
throw Object.assign(new Error("dim required"), { status: 400 });
|
|
3462
|
+
}
|
|
3463
|
+
const names = String(req.query.names ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3464
|
+
return values(req.viewer.tenantId, {
|
|
3465
|
+
dim: dim2,
|
|
3466
|
+
names: names.length ? names : void 0,
|
|
3467
|
+
range: req.query.from || req.query.to ? parseRange(req.query) : void 0,
|
|
3468
|
+
limit: req.query.limit ? Number(req.query.limit) : void 0
|
|
3469
|
+
});
|
|
3470
|
+
}));
|
|
2129
3471
|
api.get("/subjects/describe", h(async (req) => {
|
|
2130
3472
|
const refs = String(req.query.refs ?? "").split(",").filter(Boolean).slice(0, 100);
|
|
2131
3473
|
if (!subjectAdapter) return { refs: {} };
|
|
2132
3474
|
return { refs: await subjectAdapter.describe(refs) };
|
|
2133
3475
|
}));
|
|
3476
|
+
api.get("/report", h(
|
|
3477
|
+
async (req) => badRequest3(
|
|
3478
|
+
() => executeReport(q, req.viewer.tenantId, parseReportQuery(req.query), catalog)
|
|
3479
|
+
)
|
|
3480
|
+
));
|
|
3481
|
+
api.get("/report/plan", h(
|
|
3482
|
+
async (req) => badRequest3(
|
|
3483
|
+
async () => resolveReport(parseReportQuery(req.query), catalog)
|
|
3484
|
+
)
|
|
3485
|
+
));
|
|
2134
3486
|
api.get("/views", h(async (req) => ({
|
|
2135
3487
|
views: await resolveViews({
|
|
2136
3488
|
ViewModel,
|
|
2137
3489
|
registry: t.registry,
|
|
3490
|
+
catalog,
|
|
2138
3491
|
configured,
|
|
2139
3492
|
tenantId: req.viewer.tenantId,
|
|
2140
3493
|
viewerRef: req.viewer.viewerRef
|
|
@@ -2177,7 +3530,13 @@ function createDashboard(opts) {
|
|
|
2177
3530
|
indexCount: indexes.length,
|
|
2178
3531
|
indexBudget: INDEX_BUDGET,
|
|
2179
3532
|
keys,
|
|
2180
|
-
role: req.viewer.role
|
|
3533
|
+
role: req.viewer.role,
|
|
3534
|
+
// The same three sources, read the other way round: what the data says
|
|
3535
|
+
// the registry is missing, each with the line that would fix it. Derived
|
|
3536
|
+
// from the counters and the quarantine ALREADY fetched above, so the
|
|
3537
|
+
// page costs no extra read. Nothing is written — the host still edits
|
|
3538
|
+
// the registry by hand (reports §9).
|
|
3539
|
+
suggestions: deriveSuggestions({ counters: t.counters, catalog, quarantine })
|
|
2181
3540
|
};
|
|
2182
3541
|
}));
|
|
2183
3542
|
api.post("/system/keys/:id/revoke", h(async (req, res) => {
|
|
@@ -2345,6 +3704,6 @@ function createTelemetry(config) {
|
|
|
2345
3704
|
};
|
|
2346
3705
|
}
|
|
2347
3706
|
|
|
2348
|
-
export { BODY_MAX_CHARS, DEFAULT_LIMITS, Env, INDEX_BUDGET, KeyKind, LogLevel, Origin, PLATFORM_SCOPE, RETENTION_DAYS, SAMPLE_RATE, SCHEMA_VERSION, TelemetryKind, TenantMode, boundedMeta, createDashboard, createIngest, createKey, createQueries, createTelemetry, defaultSpaDir, defineRegistry, deriveViews, findFamily, hashSecret, isPlatformScope, median, newId, parseKeyString, plain, requireMilestoneFamily, resolveDim, summarizeStages, traceKeep, truncate, validateRegistry, verifySecret };
|
|
3707
|
+
export { BODY_MAX_CHARS, COUNTER_MAP_MAX, COUNTER_OVERFLOW_KEY, DEFAULT_LIMITS, Env, INDEX_BUDGET, KeyKind, LogLevel, MAX_SUGGESTIONS, Origin, PLATFORM_SCOPE, RETENTION_DAYS, SAMPLE_RATE, SCHEMA_VERSION, TelemetryKind, TenantMode, boundedMeta, createDashboard, createIngest, createKey, createQueries, createTelemetry, createValues, defaultSpaDir, defineRegistry, deriveCatalog, deriveSuggestions, deriveViews, executeReport, findFamily, foldRollups, hashSecret, intervalForRange, isPlatformScope, median, newId, normalizeQuery, parseKeyString, parseReportQuery, plain, projectRegistry, rangeOf, reportToQuery, requireMilestoneFamily, resolveDim, resolveReport, summarizeStages, traceKeep, truncate, validateRegistry, verifySecret };
|
|
2349
3708
|
//# sourceMappingURL=index.js.map
|
|
2350
3709
|
//# sourceMappingURL=index.js.map
|