@jeffjassky/telemetry 0.3.0 → 0.5.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/index.cjs +1583 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1567 -66
- 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/ui/_assets/{index-CXiDp_v6.css → index-COswHpSX.css} +1 -1
- package/dist/ui/_assets/index-kvwB9_3A.js +41 -0
- package/dist/ui/_assets/index-kvwB9_3A.js.map +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/types/index.d.ts +572 -11
- package/types/test-d.ts +193 -2
- 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,30 @@ var newCounters = () => ({
|
|
|
59
59
|
capped: 0,
|
|
60
60
|
rollupSkipped: 0,
|
|
61
61
|
deduped: 0,
|
|
62
|
-
truncated: 0
|
|
62
|
+
truncated: 0,
|
|
63
|
+
rollupSkippedBy: {},
|
|
64
|
+
undeclaredAttrs: {},
|
|
65
|
+
subjectsLinked: 0,
|
|
66
|
+
subjectLinkMisses: 0,
|
|
67
|
+
subjectLinkErrors: 0,
|
|
68
|
+
subjectLinkTimeouts: 0,
|
|
69
|
+
subjectLinkUndeclared: 0,
|
|
70
|
+
subjectLinkCapped: 0
|
|
63
71
|
});
|
|
72
|
+
var COUNTER_MAP_MAX = 1e3;
|
|
73
|
+
var COUNTER_OVERFLOW_KEY = "(other)|(other)";
|
|
74
|
+
var bumpCounterMap = (map, key) => {
|
|
75
|
+
const seen = map[key];
|
|
76
|
+
if (seen !== void 0) {
|
|
77
|
+
map[key] = seen + 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (map[COUNTER_OVERFLOW_KEY] !== void 0 || Object.keys(map).length >= COUNTER_MAP_MAX) {
|
|
81
|
+
map[COUNTER_OVERFLOW_KEY] = (map[COUNTER_OVERFLOW_KEY] ?? 0) + 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
map[key] = 1;
|
|
85
|
+
};
|
|
64
86
|
var traceKeep = (traceId, rate) => {
|
|
65
87
|
if (rate >= 1) return true;
|
|
66
88
|
if (!traceId) return Math.random() < rate;
|
|
@@ -388,15 +410,15 @@ function buildBaseSchema(collection, registry, counters, opts) {
|
|
|
388
410
|
if (this.kind === TelemetryKind.State && !this.state?.to) {
|
|
389
411
|
throw new Error("telemetry: state requires state.to");
|
|
390
412
|
}
|
|
391
|
-
const check = (
|
|
413
|
+
const check = (label3, m, zschema) => {
|
|
392
414
|
const obj = Object.fromEntries(m ?? []);
|
|
393
415
|
if (!zschema) {
|
|
394
|
-
if (Object.keys(obj).length) throw new Error(`telemetry: "${this.name}" declares no ${
|
|
416
|
+
if (Object.keys(obj).length) throw new Error(`telemetry: "${this.name}" declares no ${label3}`);
|
|
395
417
|
return;
|
|
396
418
|
}
|
|
397
419
|
const s = zschema.strict?.() ?? zschema;
|
|
398
420
|
const r = s.safeParse(obj);
|
|
399
|
-
if (!r.success) throw new Error(`telemetry: ${
|
|
421
|
+
if (!r.success) throw new Error(`telemetry: ${label3} invalid for "${this.name}": ${r.error.message}`);
|
|
400
422
|
};
|
|
401
423
|
check("attrs", this.attrs, spec.attrs);
|
|
402
424
|
check("metrics", this.metrics, spec.metrics);
|
|
@@ -545,6 +567,7 @@ async function recordRollup(RollupModel, doc, name, spec, counters) {
|
|
|
545
567
|
if (v == null || v === "") {
|
|
546
568
|
if (spec.dimDefault === void 0) {
|
|
547
569
|
counters.rollupSkipped++;
|
|
570
|
+
bumpCounterMap(counters.rollupSkippedBy, `${as}|${label(src)}`);
|
|
548
571
|
return;
|
|
549
572
|
}
|
|
550
573
|
v = spec.dimDefault;
|
|
@@ -636,6 +659,120 @@ function createCheckpointFactory(CheckpointModel, logger) {
|
|
|
636
659
|
}
|
|
637
660
|
|
|
638
661
|
// src/server/emit.ts
|
|
662
|
+
function noteUndeclaredAttrs(counters, name, spec, attrs) {
|
|
663
|
+
if (!attrs || typeof attrs !== "object") return;
|
|
664
|
+
const keys = attrs instanceof Map ? [...attrs.keys()] : Object.keys(attrs);
|
|
665
|
+
const shape = spec.attrs?.shape;
|
|
666
|
+
for (const raw of keys) {
|
|
667
|
+
const key = String(raw).replace(/\./g, "_");
|
|
668
|
+
if (shape && Object.prototype.hasOwnProperty.call(shape, key)) continue;
|
|
669
|
+
bumpCounterMap(counters.undeclaredAttrs, `${name}|${key}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
var SUBJECT_MAX = 8;
|
|
673
|
+
var SUBJECT_LINK_TIMEOUT_MS = 50;
|
|
674
|
+
var LINK_TIMEOUT = /* @__PURE__ */ Symbol("telemetry.subjectLink.timeout");
|
|
675
|
+
function createSubjectLinking(opts) {
|
|
676
|
+
const { linker, counters, logger } = opts;
|
|
677
|
+
if (!linker) return null;
|
|
678
|
+
const timeoutMs = opts.timeoutMs ?? SUBJECT_LINK_TIMEOUT_MS;
|
|
679
|
+
const warned = /* @__PURE__ */ new Set();
|
|
680
|
+
const warnOnce = (key, msg) => {
|
|
681
|
+
if (warned.has(key) || warned.size >= COUNTER_MAP_MAX) return;
|
|
682
|
+
warned.add(key);
|
|
683
|
+
logger.warn(msg);
|
|
684
|
+
};
|
|
685
|
+
return async function linkSubjects(name, spec, tenantId, declared) {
|
|
686
|
+
const have = Array.isArray(declared) ? declared : [];
|
|
687
|
+
const seen = /* @__PURE__ */ new Set();
|
|
688
|
+
const view = [];
|
|
689
|
+
for (const s of have) {
|
|
690
|
+
const ref = wellFormed(s);
|
|
691
|
+
if (!ref) continue;
|
|
692
|
+
seen.add(`${ref.type}:${ref.id}`);
|
|
693
|
+
view.push(ref);
|
|
694
|
+
}
|
|
695
|
+
let out;
|
|
696
|
+
let timer;
|
|
697
|
+
try {
|
|
698
|
+
out = await Promise.race([
|
|
699
|
+
// the async wrapper turns a SYNCHRONOUS throw into a rejection, so a
|
|
700
|
+
// linker that dies on its first line lands in the same catch as one
|
|
701
|
+
// whose promise rejects
|
|
702
|
+
(async () => linker.link(view, { name, tenantId }))(),
|
|
703
|
+
new Promise((_, reject) => {
|
|
704
|
+
timer = setTimeout(() => reject(LINK_TIMEOUT), timeoutMs);
|
|
705
|
+
})
|
|
706
|
+
]);
|
|
707
|
+
} catch (e) {
|
|
708
|
+
if (e === LINK_TIMEOUT) {
|
|
709
|
+
counters.subjectLinkTimeouts++;
|
|
710
|
+
warnOnce(
|
|
711
|
+
"timeout",
|
|
712
|
+
`[telemetry] subjectLinker.link() exceeded ${timeoutMs}ms \u2014 records are being written UNLINKED rather than waiting. The hook is expected to answer from a cache; a resolver that queries per record cannot keep up with ingest. Warned once \u2014 the count is counters.subjectLinkTimeouts.`
|
|
713
|
+
);
|
|
714
|
+
} else {
|
|
715
|
+
counters.subjectLinkErrors++;
|
|
716
|
+
warnOnce(
|
|
717
|
+
"threw",
|
|
718
|
+
`[telemetry] subjectLinker.link() threw \u2014 records are being written unlinked: ${e}. Warned once \u2014 the count is counters.subjectLinkErrors.`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
return null;
|
|
722
|
+
} finally {
|
|
723
|
+
clearTimeout(timer);
|
|
724
|
+
}
|
|
725
|
+
if (!Array.isArray(out)) {
|
|
726
|
+
counters.subjectLinkErrors++;
|
|
727
|
+
warnOnce(
|
|
728
|
+
"shape",
|
|
729
|
+
`[telemetry] subjectLinker.link() resolved to ${typeof out}, not an array \u2014 records are being written unlinked. Return [] when nothing links. Warned once \u2014 the count is counters.subjectLinkErrors.`
|
|
730
|
+
);
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
if (!out.length) {
|
|
734
|
+
counters.subjectLinkMisses++;
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
let room = Math.max(0, SUBJECT_MAX - have.length);
|
|
738
|
+
let capped2 = 0;
|
|
739
|
+
const add = [];
|
|
740
|
+
for (const s of out) {
|
|
741
|
+
const ref = wellFormed(s);
|
|
742
|
+
if (!ref) {
|
|
743
|
+
counters.subjectLinkErrors++;
|
|
744
|
+
warnOnce(
|
|
745
|
+
"entry",
|
|
746
|
+
"[telemetry] subjectLinker returned an entry that is not { type, id } \u2014 dropped. Warned once \u2014 the count is counters.subjectLinkErrors."
|
|
747
|
+
);
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const key = `${ref.type}:${ref.id}`;
|
|
751
|
+
if (seen.has(key)) continue;
|
|
752
|
+
if (!spec.subjects.includes(ref.type)) {
|
|
753
|
+
counters.subjectLinkUndeclared++;
|
|
754
|
+
}
|
|
755
|
+
if (room <= 0) {
|
|
756
|
+
capped2++;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
seen.add(key);
|
|
760
|
+
room--;
|
|
761
|
+
add.push(ref);
|
|
762
|
+
}
|
|
763
|
+
counters.subjectLinkCapped += capped2;
|
|
764
|
+
if (!add.length) return null;
|
|
765
|
+
counters.subjectsLinked += add.length;
|
|
766
|
+
return [...have, ...add];
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function wellFormed(s) {
|
|
770
|
+
if (!s || typeof s !== "object") return null;
|
|
771
|
+
const { type, id, role } = s;
|
|
772
|
+
if (typeof type !== "string" || !type) return null;
|
|
773
|
+
if (typeof id !== "string" || !id) return null;
|
|
774
|
+
return typeof role === "string" && role ? { type, id, role } : { type, id };
|
|
775
|
+
}
|
|
639
776
|
function createEmitter(ctx) {
|
|
640
777
|
const { registry, byKind, RollupModel, rejects, counters } = ctx;
|
|
641
778
|
const burstBuckets = /* @__PURE__ */ new Map();
|
|
@@ -672,16 +809,21 @@ function createEmitter(ctx) {
|
|
|
672
809
|
const durable = kind === TelemetryKind.Usage || (doc.durable ?? spec.durable ?? false);
|
|
673
810
|
const Model = byKind[kind];
|
|
674
811
|
const { forceKeep: _drop, durable: _durable, ...rest } = doc;
|
|
812
|
+
const linked = ctx.linkSubjects ? await ctx.linkSubjects(name, spec, doc.tenantId, doc.subjects) : null;
|
|
675
813
|
const safe = (o) => new Map(Object.entries(o ?? {}).map(([k, v]) => [k.replace(/\./g, "_"), v]));
|
|
676
814
|
const payload = {
|
|
677
815
|
...rest,
|
|
678
816
|
_id: id,
|
|
679
817
|
name,
|
|
818
|
+
// computed like everything below it, and absent when nothing linked, so a
|
|
819
|
+
// host with no linker hands the model the exact object 0.4.0 did
|
|
820
|
+
...linked ? { subjects: linked } : {},
|
|
680
821
|
sampleRate: forced ? 1 : baseRate,
|
|
681
822
|
forced,
|
|
682
823
|
attrs: safe(doc.attrs),
|
|
683
824
|
metrics: safe(doc.metrics)
|
|
684
825
|
};
|
|
826
|
+
noteUndeclaredAttrs(counters, name, spec, doc.attrs);
|
|
685
827
|
const onFail = async (e) => {
|
|
686
828
|
counters.rejected++;
|
|
687
829
|
await rejects().insertOne({ at: /* @__PURE__ */ new Date(), name, reason: String(e), raw: plain(doc) }).catch(() => {
|
|
@@ -887,10 +1029,10 @@ function parseKeyString(raw) {
|
|
|
887
1029
|
if (!raw) return null;
|
|
888
1030
|
const m = KEY_RE.exec(raw.trim());
|
|
889
1031
|
if (!m) return null;
|
|
890
|
-
const [, prefix,
|
|
1032
|
+
const [, prefix, label3, id, secret] = m;
|
|
891
1033
|
if (prefix === "sk" && !secret) return null;
|
|
892
1034
|
if (prefix === "pk" && secret) return null;
|
|
893
|
-
return { kind: prefix === "pk" ? KeyKind.Publishable : KeyKind.Secret, label:
|
|
1035
|
+
return { kind: prefix === "pk" ? KeyKind.Publishable : KeyKind.Secret, label: label3, id, secret };
|
|
894
1036
|
}
|
|
895
1037
|
var SCRYPT_VERSION = "scrypt1";
|
|
896
1038
|
var SCRYPT = { N: 16384, r: 8, p: 1, keylen: 32 };
|
|
@@ -948,7 +1090,7 @@ async function createKey(KeyModel, input) {
|
|
|
948
1090
|
tenantId,
|
|
949
1091
|
service,
|
|
950
1092
|
env,
|
|
951
|
-
label:
|
|
1093
|
+
label: label3 = "live",
|
|
952
1094
|
origins = [],
|
|
953
1095
|
allowedNames,
|
|
954
1096
|
maxPerMinute = 600
|
|
@@ -981,7 +1123,7 @@ async function createKey(KeyModel, input) {
|
|
|
981
1123
|
createdAt: /* @__PURE__ */ new Date()
|
|
982
1124
|
});
|
|
983
1125
|
const prefix = kind === KeyKind.Publishable ? "pk" : "sk";
|
|
984
|
-
return { key: secret ? `${prefix}_${
|
|
1126
|
+
return { key: secret ? `${prefix}_${label3}_${id}_${secret}` : `${prefix}_${label3}_${id}`, id };
|
|
985
1127
|
}
|
|
986
1128
|
var BATCH_MAX = 100;
|
|
987
1129
|
function createIngest(opts) {
|
|
@@ -1183,6 +1325,9 @@ function createIngest(opts) {
|
|
|
1183
1325
|
const occurredRaw = rec.occurredAt ? Date.parse(rec.occurredAt) : NaN;
|
|
1184
1326
|
const occurredAt = Number.isFinite(occurredRaw) ? new Date(occurredRaw - clockSkewMs) : receivedAt;
|
|
1185
1327
|
const safeMap = (o) => o && typeof o === "object" ? new Map(Object.entries(o).map(([k, v]) => [k.replace(/\./g, "_"), v])) : /* @__PURE__ */ new Map();
|
|
1328
|
+
noteUndeclaredAttrs(t.counters, name, spec, rec.attrs);
|
|
1329
|
+
const subjects = mergeSubjects(rec.subjects);
|
|
1330
|
+
const linked = t.linkSubjects ? await t.linkSubjects(name, spec, tenantId, subjects) : null;
|
|
1186
1331
|
const Model = t.models.byKind[spec.kind];
|
|
1187
1332
|
const d = new Model({
|
|
1188
1333
|
// facts the wire may not assert: tenant, service, env, origin, plane
|
|
@@ -1193,7 +1338,7 @@ function createIngest(opts) {
|
|
|
1193
1338
|
tenantId,
|
|
1194
1339
|
occurredAt,
|
|
1195
1340
|
severity: typeof rec.severity === "string" ? rec.severity : void 0,
|
|
1196
|
-
subjects:
|
|
1341
|
+
subjects: linked ?? subjects,
|
|
1197
1342
|
actor: ctx.actor ?? (typeof rec.actor === "string" ? rec.actor : batchActor),
|
|
1198
1343
|
onBehalfOf: typeof rec.onBehalfOf === "string" ? rec.onBehalfOf : void 0,
|
|
1199
1344
|
service: key.service,
|
|
@@ -1253,6 +1398,292 @@ function createIngest(opts) {
|
|
|
1253
1398
|
return router;
|
|
1254
1399
|
}
|
|
1255
1400
|
|
|
1401
|
+
// src/server/catalog.ts
|
|
1402
|
+
var label2 = (src) => src.slice(src.indexOf(":") + 1);
|
|
1403
|
+
var LEAF_TYPES = {
|
|
1404
|
+
string: "string",
|
|
1405
|
+
number: "number",
|
|
1406
|
+
int: "number",
|
|
1407
|
+
bigint: "number",
|
|
1408
|
+
boolean: "boolean",
|
|
1409
|
+
date: "date"
|
|
1410
|
+
};
|
|
1411
|
+
function walkAttr(schema) {
|
|
1412
|
+
let node = schema;
|
|
1413
|
+
let optional = false;
|
|
1414
|
+
for (let depth = 0; node && depth < 20; depth++) {
|
|
1415
|
+
const def = node._zod?.def ?? node.def;
|
|
1416
|
+
if (!def?.type) break;
|
|
1417
|
+
switch (def.type) {
|
|
1418
|
+
// these three all mean "the value may be absent from a stored record",
|
|
1419
|
+
// which is the only thing `optional` claims
|
|
1420
|
+
case "optional":
|
|
1421
|
+
case "nullable":
|
|
1422
|
+
case "default":
|
|
1423
|
+
optional = true;
|
|
1424
|
+
node = def.innerType;
|
|
1425
|
+
continue;
|
|
1426
|
+
case "catch":
|
|
1427
|
+
case "readonly":
|
|
1428
|
+
node = def.innerType;
|
|
1429
|
+
continue;
|
|
1430
|
+
// a pipe is `in -> out`; the INPUT side is what a caller may send and so
|
|
1431
|
+
// what a stored value was validated as. The output of a transform is
|
|
1432
|
+
// frequently a shape no filter could ever be written against.
|
|
1433
|
+
case "pipe":
|
|
1434
|
+
node = def.in;
|
|
1435
|
+
continue;
|
|
1436
|
+
case "enum": {
|
|
1437
|
+
const options = Array.isArray(node.options) ? node.options : Object.values(def.entries ?? {});
|
|
1438
|
+
return { type: "enum", values: options.map(String), optional };
|
|
1439
|
+
}
|
|
1440
|
+
case "literal":
|
|
1441
|
+
return { type: "enum", values: [...def.values ?? []].map(String), optional };
|
|
1442
|
+
default:
|
|
1443
|
+
return { type: LEAF_TYPES[def.type] ?? "string", optional };
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return { type: "string", optional };
|
|
1447
|
+
}
|
|
1448
|
+
var dim = (key, type, o = {}) => ({
|
|
1449
|
+
key,
|
|
1450
|
+
// the two pseudo-dims are derived at query time from subjectKeys / actor, so
|
|
1451
|
+
// they carry no `field:` prefix and label to themselves
|
|
1452
|
+
label: key.startsWith("field:") ? key.slice(6) : key,
|
|
1453
|
+
type,
|
|
1454
|
+
...o.values ? { values: [...o.values] } : {},
|
|
1455
|
+
optional: o.optional ?? false,
|
|
1456
|
+
indexed: o.indexed ?? false
|
|
1457
|
+
});
|
|
1458
|
+
var envelopeDims = (platforms) => [
|
|
1459
|
+
dim("field:kind", "enum", { values: TELEMETRY_KINDS, indexed: true }),
|
|
1460
|
+
dim("field:name", "string", { indexed: true }),
|
|
1461
|
+
dim("field:severity", "enum", { values: Object.values(LogLevel) }),
|
|
1462
|
+
dim("field:env", "enum", { values: Object.values(Env) }),
|
|
1463
|
+
dim("field:service", "string"),
|
|
1464
|
+
dim("field:release", "string"),
|
|
1465
|
+
dim("field:origin", "enum", { values: Object.values(Origin) }),
|
|
1466
|
+
// client context is absent on server-origin records, so both of its dims are optional
|
|
1467
|
+
dim("field:client.platform", "enum", { values: platforms, optional: true }),
|
|
1468
|
+
dim("field:client.appVersion", "string", { optional: true }),
|
|
1469
|
+
dim("subjectType", "string", { optional: true, indexed: true }),
|
|
1470
|
+
dim("actorType", "string", { optional: true })
|
|
1471
|
+
];
|
|
1472
|
+
var kindDims = (kind) => {
|
|
1473
|
+
switch (kind) {
|
|
1474
|
+
case TelemetryKind.Usage:
|
|
1475
|
+
return [
|
|
1476
|
+
dim("field:usage.meter", "string", { indexed: true }),
|
|
1477
|
+
dim("field:usage.billedTo", "string"),
|
|
1478
|
+
dim("field:usage.unit", "string")
|
|
1479
|
+
];
|
|
1480
|
+
case TelemetryKind.State:
|
|
1481
|
+
return [
|
|
1482
|
+
dim("field:state.key", "string", { indexed: true }),
|
|
1483
|
+
dim("field:state.to", "string", { indexed: true })
|
|
1484
|
+
];
|
|
1485
|
+
case TelemetryKind.Error:
|
|
1486
|
+
return [dim("field:error.type", "string"), dim("field:error.handled", "boolean")];
|
|
1487
|
+
default:
|
|
1488
|
+
return [];
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
var RAW_OPS = ["avg", "p50", "p95", "p99"];
|
|
1492
|
+
function deriveCatalog(registry, opts = {}) {
|
|
1493
|
+
const platforms = [.../* @__PURE__ */ new Set([...BUILTIN_PLATFORMS, ...opts.platforms ?? []])];
|
|
1494
|
+
const families = {};
|
|
1495
|
+
for (const [name, spec] of Object.entries(registry)) {
|
|
1496
|
+
for (const r of spec.rollups ?? []) {
|
|
1497
|
+
const as = r.as ?? name;
|
|
1498
|
+
const seen = families[as];
|
|
1499
|
+
if (!seen) {
|
|
1500
|
+
families[as] = {
|
|
1501
|
+
as,
|
|
1502
|
+
by: [...r.by],
|
|
1503
|
+
labels: r.by.map(label2),
|
|
1504
|
+
bucket: r.bucket ?? null,
|
|
1505
|
+
lifetime: !r.bucket,
|
|
1506
|
+
// `subjects` only means anything when there is a subject dim to
|
|
1507
|
+
// restrict; without one it selects nothing and claiming it would
|
|
1508
|
+
// offer a subject filter the family cannot answer
|
|
1509
|
+
subjectTypes: r.by.includes("subject") ? [...r.subjects ?? []] : [],
|
|
1510
|
+
sums: [...r.sum ?? []],
|
|
1511
|
+
capture: (r.capture ?? []).map(label2),
|
|
1512
|
+
feeders: [name],
|
|
1513
|
+
retentionDays: r.retentionDays ?? null
|
|
1514
|
+
};
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
for (const k of r.sum ?? []) if (!seen.sums.includes(k)) seen.sums.push(k);
|
|
1518
|
+
for (const c of r.capture ?? []) {
|
|
1519
|
+
const l = label2(c);
|
|
1520
|
+
if (!seen.capture.includes(l)) seen.capture.push(l);
|
|
1521
|
+
}
|
|
1522
|
+
if (!seen.feeders.includes(name)) seen.feeders.push(name);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
const events = {};
|
|
1526
|
+
const namespaces = {};
|
|
1527
|
+
const subjectTypes = [];
|
|
1528
|
+
const noteSubject = (t) => {
|
|
1529
|
+
if (!subjectTypes.includes(t)) subjectTypes.push(t);
|
|
1530
|
+
};
|
|
1531
|
+
for (const [name, spec] of Object.entries(registry)) {
|
|
1532
|
+
const dot = name.indexOf(".");
|
|
1533
|
+
const namespace = dot === -1 ? name : name.slice(0, dot);
|
|
1534
|
+
(namespaces[namespace] ??= []).push(name);
|
|
1535
|
+
for (const s of spec.subjects) noteSubject(s);
|
|
1536
|
+
const indexedAttrs = [...spec.indexedAttrs ?? []];
|
|
1537
|
+
const dims = Object.entries(spec.attrs?.shape ?? {}).map(
|
|
1538
|
+
([key, schema]) => {
|
|
1539
|
+
const walked = walkAttr(schema);
|
|
1540
|
+
return {
|
|
1541
|
+
key: `attr:${key}`,
|
|
1542
|
+
label: key,
|
|
1543
|
+
type: walked.type,
|
|
1544
|
+
...walked.values ? { values: walked.values } : {},
|
|
1545
|
+
optional: walked.optional,
|
|
1546
|
+
indexed: indexedAttrs.includes(key)
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
);
|
|
1550
|
+
dims.push(...kindDims(spec.kind));
|
|
1551
|
+
const eventFamilies = [];
|
|
1552
|
+
const ownSums = /* @__PURE__ */ new Map();
|
|
1553
|
+
for (const r of spec.rollups ?? []) {
|
|
1554
|
+
const as = r.as ?? name;
|
|
1555
|
+
if (!eventFamilies.includes(as)) eventFamilies.push(as);
|
|
1556
|
+
const set = ownSums.get(as) ?? /* @__PURE__ */ new Set();
|
|
1557
|
+
for (const k of r.sum ?? []) set.add(k);
|
|
1558
|
+
ownSums.set(as, set);
|
|
1559
|
+
for (const s of r.subjects ?? []) noteSubject(s);
|
|
1560
|
+
}
|
|
1561
|
+
const measures = [{ key: "count", exactVia: [] }];
|
|
1562
|
+
for (const k of Object.keys(spec.metrics?.shape ?? {})) {
|
|
1563
|
+
measures.push({
|
|
1564
|
+
key: `sum:${k}`,
|
|
1565
|
+
metric: k,
|
|
1566
|
+
exactVia: eventFamilies.filter((as) => ownSums.get(as)?.has(k))
|
|
1567
|
+
});
|
|
1568
|
+
for (const op of RAW_OPS) measures.push({ key: `${op}:${k}`, metric: k, exactVia: [] });
|
|
1569
|
+
}
|
|
1570
|
+
if (spec.kind === TelemetryKind.Span) {
|
|
1571
|
+
for (const op of RAW_OPS) {
|
|
1572
|
+
measures.push({ key: `${op}:durationMs`, metric: "durationMs", exactVia: [] });
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
events[name] = {
|
|
1576
|
+
kind: spec.kind,
|
|
1577
|
+
origin: spec.origin,
|
|
1578
|
+
subjects: [...spec.subjects],
|
|
1579
|
+
description: spec.description,
|
|
1580
|
+
namespace,
|
|
1581
|
+
dims,
|
|
1582
|
+
measures,
|
|
1583
|
+
families: eventFamilies,
|
|
1584
|
+
indexedAttrs,
|
|
1585
|
+
indexedMetrics: [...spec.indexedMetrics ?? []],
|
|
1586
|
+
// `hasOwnProperty` rather than `??`, exactly as model.ts stamps expiresAt:
|
|
1587
|
+
// an explicit `retentionDays: null` means immortal and must not fall
|
|
1588
|
+
// through to the per-kind default
|
|
1589
|
+
retentionDays: Object.prototype.hasOwnProperty.call(spec, "retentionDays") ? spec.retentionDays ?? null : RETENTION_DAYS[spec.kind]
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
return { events, families, namespaces, envelope: envelopeDims(platforms), subjectTypes };
|
|
1593
|
+
}
|
|
1594
|
+
function projectRegistry(catalog) {
|
|
1595
|
+
return Object.fromEntries(
|
|
1596
|
+
Object.entries(catalog.events).map(([name, e]) => [
|
|
1597
|
+
name,
|
|
1598
|
+
{
|
|
1599
|
+
kind: e.kind,
|
|
1600
|
+
origin: e.origin,
|
|
1601
|
+
subjects: e.subjects,
|
|
1602
|
+
description: e.description,
|
|
1603
|
+
attrKeys: e.dims.filter((d) => d.key.startsWith("attr:")).map((d) => d.label),
|
|
1604
|
+
// every metric key gets exactly one `sum:` measure and nothing else does
|
|
1605
|
+
metricKeys: e.measures.filter((m) => m.key.startsWith("sum:")).map((m) => m.metric),
|
|
1606
|
+
indexedAttrs: e.indexedAttrs,
|
|
1607
|
+
indexedMetrics: e.indexedMetrics,
|
|
1608
|
+
rollups: e.families.map((as) => {
|
|
1609
|
+
const f = catalog.families[as];
|
|
1610
|
+
return { as: f.as, by: f.by, bucket: f.bucket, sum: f.sums, subjects: f.subjectTypes };
|
|
1611
|
+
})
|
|
1612
|
+
}
|
|
1613
|
+
])
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// src/server/suggest.ts
|
|
1618
|
+
var UNREGISTERED_REASON = "unregistered event";
|
|
1619
|
+
var MAX_SUGGESTIONS = 50;
|
|
1620
|
+
var NAME_MAX = 120;
|
|
1621
|
+
var quote = (s) => /^[A-Za-z0-9_.:$-]+$/.test(s) ? `'${s}'` : JSON.stringify(s);
|
|
1622
|
+
var prop = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s) ? s : quote(s);
|
|
1623
|
+
var times = (n) => `${n} time${n === 1 ? "" : "s"}`;
|
|
1624
|
+
var split = (k) => {
|
|
1625
|
+
const i = k.indexOf("|");
|
|
1626
|
+
return i === -1 ? [k, ""] : [k.slice(0, i), k.slice(i + 1)];
|
|
1627
|
+
};
|
|
1628
|
+
function deriveSuggestions(input) {
|
|
1629
|
+
const { counters, catalog, quarantine = [] } = input;
|
|
1630
|
+
const out = [];
|
|
1631
|
+
for (const [k, count2] of Object.entries(counters.undeclaredAttrs ?? {})) {
|
|
1632
|
+
if (k === COUNTER_OVERFLOW_KEY || !count2) continue;
|
|
1633
|
+
const [name, key] = split(k);
|
|
1634
|
+
if (!name || !key) continue;
|
|
1635
|
+
const facet = catalog.events[name];
|
|
1636
|
+
const line = `${prop(key)}: z.string().max(64),`;
|
|
1637
|
+
const hasAttrs = !!facet?.dims.some((d) => d.key.startsWith("attr:"));
|
|
1638
|
+
out.push({
|
|
1639
|
+
kind: "undeclared_attr",
|
|
1640
|
+
target: name,
|
|
1641
|
+
key,
|
|
1642
|
+
count: count2,
|
|
1643
|
+
message: `\`${name}\` has been sent with attr \`${key}\` ${times(count2)} \u2014 not declared`,
|
|
1644
|
+
fix: hasAttrs ? line : `attrs: z.object({ ${prop(key)}: z.string().max(64) }),`
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
for (const [k, count2] of Object.entries(counters.rollupSkippedBy ?? {})) {
|
|
1648
|
+
if (k === COUNTER_OVERFLOW_KEY || !count2) continue;
|
|
1649
|
+
const [as, dim2] = split(k);
|
|
1650
|
+
if (!as || !dim2) continue;
|
|
1651
|
+
const feeders = catalog.families[as]?.feeders ?? [];
|
|
1652
|
+
const where = feeders.length ? `// on the \`${as}\` rollup of ${feeders.map((f) => `\`${f}\``).join(", ")}
|
|
1653
|
+
` : "";
|
|
1654
|
+
out.push({
|
|
1655
|
+
kind: "missing_dim_default",
|
|
1656
|
+
target: as,
|
|
1657
|
+
key: dim2,
|
|
1658
|
+
count: count2,
|
|
1659
|
+
message: `\`${as}\` skipped ${count2} record${count2 === 1 ? "" : "s"} with no \`${dim2}\` \u2014 declare \`dimDefault\``,
|
|
1660
|
+
fix: `${where}dimDefault: 'unknown',`
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
const unregistered = /* @__PURE__ */ new Map();
|
|
1664
|
+
for (const row of quarantine) {
|
|
1665
|
+
if (typeof row?.reason !== "string" || !row.reason.includes(UNREGISTERED_REASON)) continue;
|
|
1666
|
+
const name = typeof row.name === "string" ? row.name.slice(0, NAME_MAX) : "";
|
|
1667
|
+
if (!name || name === "(unnamed)") continue;
|
|
1668
|
+
unregistered.set(name, (unregistered.get(name) ?? 0) + 1);
|
|
1669
|
+
}
|
|
1670
|
+
for (const [name, count2] of unregistered) {
|
|
1671
|
+
out.push({
|
|
1672
|
+
kind: "unregistered_event",
|
|
1673
|
+
target: name,
|
|
1674
|
+
count: count2,
|
|
1675
|
+
message: `\`${name}\` was rejected ${times(count2)} \u2014 not in the registry`,
|
|
1676
|
+
// the minimum that boots: validateRegistry wants a kind, an origin, and
|
|
1677
|
+
// a subjects array, and nothing here can guess the rest
|
|
1678
|
+
fix: `${quote(name)}: { kind: 'event', origin: 'client', subjects: [], description: '' },`
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
out.sort(
|
|
1682
|
+
(a, b) => b.count - a.count || a.target.localeCompare(b.target) || (a.key ?? "").localeCompare(b.key ?? "")
|
|
1683
|
+
);
|
|
1684
|
+
return out.slice(0, MAX_SUGGESTIONS);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1256
1687
|
// src/server/funnel.ts
|
|
1257
1688
|
var DAY_MS = 864e5;
|
|
1258
1689
|
function median(values) {
|
|
@@ -1460,6 +1891,10 @@ var DEFAULT_LIMITS = {
|
|
|
1460
1891
|
rollups: 500,
|
|
1461
1892
|
trace: 500,
|
|
1462
1893
|
journey: 500,
|
|
1894
|
+
breakdown: 50,
|
|
1895
|
+
// top groups — a starting point, to be measured on real hosts
|
|
1896
|
+
values: 200,
|
|
1897
|
+
// top values of one dimension — a picker, not a table
|
|
1463
1898
|
distribution: 1e5,
|
|
1464
1899
|
distinct: 1e5,
|
|
1465
1900
|
funnel: 5e3
|
|
@@ -1473,7 +1908,10 @@ function buildMatch(scope, range, f) {
|
|
|
1473
1908
|
occurredAt: { $gte: range.from, $lt: range.to }
|
|
1474
1909
|
};
|
|
1475
1910
|
for (const k of ["kind", "name", "severity", "env", "service", "release", "traceId"]) {
|
|
1476
|
-
|
|
1911
|
+
const v = f[k];
|
|
1912
|
+
if (Array.isArray(v)) {
|
|
1913
|
+
if (v.length) match[k] = { $in: v };
|
|
1914
|
+
} else if (v) match[k] = v;
|
|
1477
1915
|
}
|
|
1478
1916
|
if (f.subject) match.subjectKeys = f.subject;
|
|
1479
1917
|
for (const [k, v] of Object.entries(f.attrs ?? {})) match[`attrs.${k}`] = v;
|
|
@@ -1496,6 +1934,68 @@ function buildMatch(scope, range, f) {
|
|
|
1496
1934
|
}
|
|
1497
1935
|
return match;
|
|
1498
1936
|
}
|
|
1937
|
+
var INTERVALS = ["hour", "day", "week", "month"];
|
|
1938
|
+
var truncTo = (path3, unit) => ({
|
|
1939
|
+
$dateTrunc: { date: path3, unit, ...unit === "week" ? { startOfWeek: "monday" } : {} }
|
|
1940
|
+
});
|
|
1941
|
+
function measureAccumulator(measure) {
|
|
1942
|
+
const m = /^(sum|avg):(.+)$/.exec(measure);
|
|
1943
|
+
if (!m) return { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } };
|
|
1944
|
+
const path3 = m[2] === "durationMs" ? "$durationMs" : `$metrics.${m[2]}`;
|
|
1945
|
+
return m[1] === "sum" ? { $sum: path3 } : { $avg: path3 };
|
|
1946
|
+
}
|
|
1947
|
+
var badRequest = (message) => Object.assign(new Error(`telemetry: breakdown() \u2014 ${message}`), { status: 400 });
|
|
1948
|
+
var BREAKDOWN_FIELDS = [
|
|
1949
|
+
"kind",
|
|
1950
|
+
"name",
|
|
1951
|
+
"severity",
|
|
1952
|
+
"env",
|
|
1953
|
+
"service",
|
|
1954
|
+
"release",
|
|
1955
|
+
"origin",
|
|
1956
|
+
"client.platform",
|
|
1957
|
+
"client.appVersion",
|
|
1958
|
+
"usage.meter",
|
|
1959
|
+
"usage.billedTo",
|
|
1960
|
+
"usage.unit",
|
|
1961
|
+
"state.key",
|
|
1962
|
+
"state.to",
|
|
1963
|
+
"error.type",
|
|
1964
|
+
"error.handled"
|
|
1965
|
+
];
|
|
1966
|
+
var typePrefix = (ref) => ({
|
|
1967
|
+
$let: {
|
|
1968
|
+
vars: { ref },
|
|
1969
|
+
in: {
|
|
1970
|
+
$cond: [
|
|
1971
|
+
{ $eq: [{ $type: "$$ref" }, "string"] },
|
|
1972
|
+
{ $arrayElemAt: [{ $split: ["$$ref", ":"] }, 0] },
|
|
1973
|
+
null
|
|
1974
|
+
]
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
function dimExpression(dim2) {
|
|
1979
|
+
if (dim2.startsWith("attr:")) {
|
|
1980
|
+
const key = dim2.slice(5);
|
|
1981
|
+
if (!key) throw badRequest('`attr:` needs a key, e.g. "attr:plan"');
|
|
1982
|
+
return { $ifNull: [`$attrs.${key}`, null] };
|
|
1983
|
+
}
|
|
1984
|
+
if (dim2.startsWith("field:")) {
|
|
1985
|
+
const path3 = dim2.slice(6);
|
|
1986
|
+
if (!BREAKDOWN_FIELDS.includes(path3)) {
|
|
1987
|
+
throw badRequest(
|
|
1988
|
+
`"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.`
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
return { $ifNull: [`$${path3}`, null] };
|
|
1992
|
+
}
|
|
1993
|
+
if (dim2 === "subjectType") return typePrefix({ $arrayElemAt: ["$subjectKeys", 0] });
|
|
1994
|
+
if (dim2 === "actorType") return typePrefix("$actor");
|
|
1995
|
+
throw badRequest(
|
|
1996
|
+
`"${dim2}" is not a dimension. Use "attr:<key>", "field:<path>", "subjectType" or "actorType".`
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1499
1999
|
var QueryCache = class {
|
|
1500
2000
|
constructor(ttlMs, cap) {
|
|
1501
2001
|
this.ttlMs = ttlMs;
|
|
@@ -1569,16 +2069,9 @@ function createQueries(ctx) {
|
|
|
1569
2069
|
return cache.get(
|
|
1570
2070
|
key,
|
|
1571
2071
|
() => 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
2072
|
const buckets = await ctx.TelemetryModel.aggregate([
|
|
1575
2073
|
{ $match: buildMatch(scope, range, filter) },
|
|
1576
|
-
{
|
|
1577
|
-
$group: {
|
|
1578
|
-
_id: { $dateTrunc: { date: "$occurredAt", unit: interval, ...interval === "week" ? { startOfWeek: "monday" } : {} } },
|
|
1579
|
-
value
|
|
1580
|
-
}
|
|
1581
|
-
},
|
|
2074
|
+
{ $group: { _id: truncTo("$occurredAt", interval), value: measureAccumulator(measure) } },
|
|
1582
2075
|
{ $sort: { _id: 1 } },
|
|
1583
2076
|
{ $limit: limits.series }
|
|
1584
2077
|
]);
|
|
@@ -1586,6 +2079,114 @@ function createQueries(ctx) {
|
|
|
1586
2079
|
})
|
|
1587
2080
|
);
|
|
1588
2081
|
},
|
|
2082
|
+
/**
|
|
2083
|
+
* Top groups of a measure by one or two dimensions — "which models cost the
|
|
2084
|
+
* most", "errors by release", "events by platform per week". The primitive
|
|
2085
|
+
* that replaces a page's client-side grouping of whatever rows it happened
|
|
2086
|
+
* to have fetched, which answered "this page" while reading like it
|
|
2087
|
+
* answered the range (reports §6).
|
|
2088
|
+
*
|
|
2089
|
+
* THE CAP IS ON GROUPS RETURNED, NEVER ON ROWS SCANNED. Every `$limit`
|
|
2090
|
+
* below sits AFTER a `$group`, exactly as series() does: the scan is bounded
|
|
2091
|
+
* by buildMatch — tenant, range, filters, indexes — and nothing else, so a
|
|
2092
|
+
* quarter of a million records is one pass and 50 rows. Truncation
|
|
2093
|
+
* therefore keeps the TOP groups by measure, which is what a breakdown
|
|
2094
|
+
* table means; a cap on documents scanned would return an arbitrary prefix
|
|
2095
|
+
* and call it the top.
|
|
2096
|
+
*
|
|
2097
|
+
* With an `interval` this runs a SECOND aggregate restricted to the top
|
|
2098
|
+
* groups, rather than one pipeline that groups by (dims, bucket) and folds.
|
|
2099
|
+
* Two reasons: the ranking must be the measure over the WHOLE range (the
|
|
2100
|
+
* same number the no-interval call reports), and folding in one pass means
|
|
2101
|
+
* `$push`-ing every bucket of every group before the cap can apply — the
|
|
2102
|
+
* unbounded intermediate this primitive exists to avoid. The restriction is
|
|
2103
|
+
* an `$expr`/`$or` over the ≤ cap tuples because a dim can be a computed
|
|
2104
|
+
* expression (subjectType), which a plain `$in` on a path cannot address.
|
|
2105
|
+
*
|
|
2106
|
+
* Under PLATFORM_SCOPE it aggregates ACROSS tenants, like series() — one
|
|
2107
|
+
* set of groups with every tenant summed into it, which is the platform-wide
|
|
2108
|
+
* table a platform operator came for. Ask for a per-tenant split by scoping
|
|
2109
|
+
* to a tenant, or with rollups().
|
|
2110
|
+
*/
|
|
2111
|
+
breakdown(scope, range, filter, opts) {
|
|
2112
|
+
const groupBy = opts.groupBy ?? [];
|
|
2113
|
+
if (groupBy.length < 1 || groupBy.length > 2) {
|
|
2114
|
+
throw badRequest(`groupBy takes 1 or 2 dimensions, got ${groupBy.length}`);
|
|
2115
|
+
}
|
|
2116
|
+
const measure = opts.measure ?? "count";
|
|
2117
|
+
const interval = opts.interval;
|
|
2118
|
+
if (interval && !INTERVALS.includes(interval)) {
|
|
2119
|
+
throw badRequest(`interval must be one of ${INTERVALS.join(", ")}`);
|
|
2120
|
+
}
|
|
2121
|
+
const dims = groupBy.map(dimExpression);
|
|
2122
|
+
const cap = Math.min(Math.max(1, opts.limit ?? limits.breakdown), limits.breakdown);
|
|
2123
|
+
const key = JSON.stringify([
|
|
2124
|
+
"breakdown",
|
|
2125
|
+
scope,
|
|
2126
|
+
range.from,
|
|
2127
|
+
range.to,
|
|
2128
|
+
filter,
|
|
2129
|
+
groupBy,
|
|
2130
|
+
measure,
|
|
2131
|
+
interval ?? null,
|
|
2132
|
+
cap
|
|
2133
|
+
]);
|
|
2134
|
+
return cache.get(
|
|
2135
|
+
key,
|
|
2136
|
+
() => timed("breakdown", { scope, filter, groupBy, measure, interval }, async () => {
|
|
2137
|
+
const match = buildMatch(scope, range, filter);
|
|
2138
|
+
const dimId = Object.fromEntries(dims.map((expr, i) => [`d${i}`, expr]));
|
|
2139
|
+
const top = await ctx.TelemetryModel.aggregate([
|
|
2140
|
+
{ $match: match },
|
|
2141
|
+
{ $group: { _id: dimId, value: measureAccumulator(measure) } },
|
|
2142
|
+
{ $sort: { value: -1, _id: 1 } },
|
|
2143
|
+
{ $limit: cap + 1 }
|
|
2144
|
+
]);
|
|
2145
|
+
const truncated = top.length > cap;
|
|
2146
|
+
if (truncated) top.pop();
|
|
2147
|
+
const tuples = top.map(
|
|
2148
|
+
(g) => groupBy.map((_, i) => g._id?.[`d${i}`] ?? null)
|
|
2149
|
+
);
|
|
2150
|
+
if (!interval) {
|
|
2151
|
+
return {
|
|
2152
|
+
rows: top.map((g, i) => ({ dims: tuples[i], value: g.value })),
|
|
2153
|
+
groups: top.length,
|
|
2154
|
+
truncated,
|
|
2155
|
+
bucketsTruncated: false,
|
|
2156
|
+
dataSource: "raw"
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
if (!tuples.length) {
|
|
2160
|
+
return { rows: [], groups: 0, truncated, bucketsTruncated: false, dataSource: "raw" };
|
|
2161
|
+
}
|
|
2162
|
+
const inTop = {
|
|
2163
|
+
$or: tuples.map((t) => ({ $and: dims.map((expr, i) => ({ $eq: [expr, t[i] ?? null] })) }))
|
|
2164
|
+
};
|
|
2165
|
+
const bucketCap = limits.series * top.length;
|
|
2166
|
+
const perBucket = await ctx.TelemetryModel.aggregate([
|
|
2167
|
+
{ $match: { ...match, $expr: inTop } },
|
|
2168
|
+
{ $group: { _id: { at: truncTo("$occurredAt", interval), ...dimId }, value: measureAccumulator(measure) } },
|
|
2169
|
+
// `at` is the first key of `_id`, so one BSON sort orders by bucket
|
|
2170
|
+
// then by dims — deterministic without a second sort key
|
|
2171
|
+
{ $sort: { _id: 1 } },
|
|
2172
|
+
{ $limit: bucketCap + 1 }
|
|
2173
|
+
]);
|
|
2174
|
+
const bucketsTruncated = perBucket.length > bucketCap;
|
|
2175
|
+
if (bucketsTruncated) perBucket.pop();
|
|
2176
|
+
return {
|
|
2177
|
+
rows: perBucket.map((b) => ({
|
|
2178
|
+
dims: groupBy.map((_, i) => b._id?.[`d${i}`] ?? null),
|
|
2179
|
+
at: b._id.at,
|
|
2180
|
+
value: b.value
|
|
2181
|
+
})),
|
|
2182
|
+
groups: top.length,
|
|
2183
|
+
truncated,
|
|
2184
|
+
bucketsTruncated,
|
|
2185
|
+
dataSource: "raw"
|
|
2186
|
+
};
|
|
2187
|
+
})
|
|
2188
|
+
);
|
|
2189
|
+
},
|
|
1589
2190
|
/**
|
|
1590
2191
|
* Percentiles + histogram off raw. Keep-all makes the SAMPLE complete —
|
|
1591
2192
|
* no sampling stands between the match and the math (§5.3) — but the
|
|
@@ -1827,6 +2428,820 @@ function requireDistinctFamily(registry, as) {
|
|
|
1827
2428
|
}
|
|
1828
2429
|
return spec;
|
|
1829
2430
|
}
|
|
2431
|
+
|
|
2432
|
+
// src/server/values.ts
|
|
2433
|
+
var empty = (source) => ({
|
|
2434
|
+
values: [],
|
|
2435
|
+
source,
|
|
2436
|
+
truncated: false,
|
|
2437
|
+
dataSource: source
|
|
2438
|
+
});
|
|
2439
|
+
var SAMPLED_COUNT = { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } };
|
|
2440
|
+
function createValues(ctx) {
|
|
2441
|
+
const limits = { ...DEFAULT_LIMITS, ...ctx.limits };
|
|
2442
|
+
const slowMs = ctx.slowMs ?? 500;
|
|
2443
|
+
const cache = new QueryCache(ctx.cacheTtlMs ?? 10 * 6e4, ctx.cacheSize ?? 60);
|
|
2444
|
+
const { catalog } = ctx;
|
|
2445
|
+
const timed = async (op, params, run) => {
|
|
2446
|
+
const t0 = Date.now();
|
|
2447
|
+
try {
|
|
2448
|
+
return await run();
|
|
2449
|
+
} finally {
|
|
2450
|
+
const ms = Date.now() - t0;
|
|
2451
|
+
if (ms > slowMs) ctx.onSlowQuery?.({ op, ms, params });
|
|
2452
|
+
}
|
|
2453
|
+
};
|
|
2454
|
+
const eventNames = (names) => names?.length ? names.filter((n) => catalog.events[n]) : Object.keys(catalog.events);
|
|
2455
|
+
function fromCatalog(dim2, names) {
|
|
2456
|
+
const facets = [];
|
|
2457
|
+
for (const d of catalog.envelope) if (d.key === dim2) facets.push(d);
|
|
2458
|
+
for (const name of eventNames(names)) {
|
|
2459
|
+
for (const d of catalog.events[name].dims) if (d.key === dim2) facets.push(d);
|
|
2460
|
+
}
|
|
2461
|
+
const out = [];
|
|
2462
|
+
for (const f of facets) for (const v of f.values ?? []) if (!out.includes(v)) out.push(v);
|
|
2463
|
+
return out;
|
|
2464
|
+
}
|
|
2465
|
+
function pickFamily(dim2, names) {
|
|
2466
|
+
if (dim2 === "subjectType" || dim2 === "actorType") return null;
|
|
2467
|
+
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));
|
|
2468
|
+
const best = matches[0];
|
|
2469
|
+
return best ? { as: best.f.as, index: best.index, label: best.f.labels[best.index] } : null;
|
|
2470
|
+
}
|
|
2471
|
+
function rawReadable(dim2, names) {
|
|
2472
|
+
try {
|
|
2473
|
+
dimExpression(dim2);
|
|
2474
|
+
} catch {
|
|
2475
|
+
return false;
|
|
2476
|
+
}
|
|
2477
|
+
if (!dim2.startsWith("attr:")) return true;
|
|
2478
|
+
const key = dim2.slice(5);
|
|
2479
|
+
return eventNames(names).some((n) => catalog.events[n].indexedAttrs.includes(key));
|
|
2480
|
+
}
|
|
2481
|
+
return async function values(scope, params) {
|
|
2482
|
+
const { dim: dim2, names, range } = params;
|
|
2483
|
+
if (!dim2) return empty("none");
|
|
2484
|
+
const cap = Math.min(Math.max(1, params.limit ?? limits.values), limits.values);
|
|
2485
|
+
const key = JSON.stringify([
|
|
2486
|
+
"values",
|
|
2487
|
+
scope,
|
|
2488
|
+
dim2,
|
|
2489
|
+
names ?? null,
|
|
2490
|
+
range?.from ?? null,
|
|
2491
|
+
range?.to ?? null,
|
|
2492
|
+
cap
|
|
2493
|
+
]);
|
|
2494
|
+
return cache.get(
|
|
2495
|
+
key,
|
|
2496
|
+
() => timed("values", { scope, dim: dim2, names }, async () => {
|
|
2497
|
+
const declared = fromCatalog(dim2, names);
|
|
2498
|
+
if (declared.length) {
|
|
2499
|
+
return { values: declared, source: "catalog", truncated: false, dataSource: "catalog" };
|
|
2500
|
+
}
|
|
2501
|
+
const family = pickFamily(dim2, names);
|
|
2502
|
+
if (family) {
|
|
2503
|
+
const rows2 = await ctx.RollupModel.aggregate([
|
|
2504
|
+
{
|
|
2505
|
+
$match: {
|
|
2506
|
+
...isPlatformScope(scope) ? {} : { tenantId: scope },
|
|
2507
|
+
as: family.as
|
|
2508
|
+
}
|
|
2509
|
+
},
|
|
2510
|
+
{ $project: { v: { $arrayElemAt: ["$dims", family.index] }, count: 1 } },
|
|
2511
|
+
{ $match: { v: { $type: "string" } } },
|
|
2512
|
+
{ $group: { _id: "$v", count: { $sum: "$count" } } },
|
|
2513
|
+
{ $sort: { count: -1, _id: 1 } },
|
|
2514
|
+
{ $limit: cap + 1 }
|
|
2515
|
+
]);
|
|
2516
|
+
const truncated2 = rows2.length > cap;
|
|
2517
|
+
if (truncated2) rows2.pop();
|
|
2518
|
+
const prefix = `${family.label}=`;
|
|
2519
|
+
return {
|
|
2520
|
+
values: rows2.map(
|
|
2521
|
+
(r) => String(r._id).startsWith(prefix) ? String(r._id).slice(prefix.length) : String(r._id)
|
|
2522
|
+
),
|
|
2523
|
+
counts: rows2.map((r) => r.count),
|
|
2524
|
+
source: "rollups",
|
|
2525
|
+
via: family.as,
|
|
2526
|
+
truncated: truncated2,
|
|
2527
|
+
dataSource: "rollups"
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
if (!rawReadable(dim2, names)) return empty("none");
|
|
2531
|
+
if (!range) return empty("none");
|
|
2532
|
+
const rows = await ctx.TelemetryModel.aggregate([
|
|
2533
|
+
{ $match: buildMatch(scope, range, names?.length ? { name: names } : {}) },
|
|
2534
|
+
{ $group: { _id: dimExpression(dim2), count: SAMPLED_COUNT } },
|
|
2535
|
+
// a "no value" is not a value to pick — the null group is real
|
|
2536
|
+
// (breakdown reports it) but it is not something a filter can name
|
|
2537
|
+
{ $match: { _id: { $ne: null } } },
|
|
2538
|
+
{ $sort: { count: -1, _id: 1 } },
|
|
2539
|
+
{ $limit: cap + 1 }
|
|
2540
|
+
]);
|
|
2541
|
+
const truncated = rows.length > cap;
|
|
2542
|
+
if (truncated) rows.pop();
|
|
2543
|
+
return {
|
|
2544
|
+
values: rows.map((r) => String(r._id)),
|
|
2545
|
+
counts: rows.map((r) => r.count),
|
|
2546
|
+
source: "raw",
|
|
2547
|
+
truncated,
|
|
2548
|
+
dataSource: "raw"
|
|
2549
|
+
};
|
|
2550
|
+
})
|
|
2551
|
+
);
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
// src/server/report.ts
|
|
2556
|
+
var RANGE_MS = {
|
|
2557
|
+
"1h": 36e5,
|
|
2558
|
+
"24h": 864e5,
|
|
2559
|
+
"7d": 7 * 864e5,
|
|
2560
|
+
"30d": 30 * 864e5,
|
|
2561
|
+
"90d": 90 * 864e5
|
|
2562
|
+
};
|
|
2563
|
+
var badRequest2 = (message) => Object.assign(new Error(`telemetry: ${message}`), { status: 400 });
|
|
2564
|
+
function rangeOf(range, now = /* @__PURE__ */ new Date()) {
|
|
2565
|
+
if (typeof range === "string") {
|
|
2566
|
+
const ms = RANGE_MS[range] ?? spanOf(range);
|
|
2567
|
+
if (ms == null) {
|
|
2568
|
+
throw badRequest2(
|
|
2569
|
+
`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`
|
|
2570
|
+
);
|
|
2571
|
+
}
|
|
2572
|
+
return { from: new Date(now.getTime() - ms), to: now };
|
|
2573
|
+
}
|
|
2574
|
+
const from = new Date(range.from);
|
|
2575
|
+
const to = new Date(range.to);
|
|
2576
|
+
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()) || from >= to) {
|
|
2577
|
+
throw badRequest2(
|
|
2578
|
+
`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)`
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
return { from, to };
|
|
2582
|
+
}
|
|
2583
|
+
function spanOf(range) {
|
|
2584
|
+
const m = /^(\d+)([hd])$/.exec(range);
|
|
2585
|
+
if (!m) return null;
|
|
2586
|
+
return Number(m[1]) * (m[2] === "h" ? 36e5 : 864e5);
|
|
2587
|
+
}
|
|
2588
|
+
function intervalForRange(range, now = /* @__PURE__ */ new Date()) {
|
|
2589
|
+
if (typeof range === "string" && RANGE_MS[range] != null) {
|
|
2590
|
+
return range === "1h" || range === "24h" ? "hour" : range === "90d" ? "week" : "day";
|
|
2591
|
+
}
|
|
2592
|
+
const { from, to } = rangeOf(range, now);
|
|
2593
|
+
const ms = to.getTime() - from.getTime();
|
|
2594
|
+
if (ms <= 864e5) return "hour";
|
|
2595
|
+
if (ms < 90 * 864e5) return "day";
|
|
2596
|
+
return "week";
|
|
2597
|
+
}
|
|
2598
|
+
var INTERVAL_RANK = { hour: 0, day: 1, week: 2, month: 3 };
|
|
2599
|
+
var shift = (range) => ({
|
|
2600
|
+
from: new Date(range.from.getTime() - (range.to.getTime() - range.from.getTime())),
|
|
2601
|
+
to: range.from
|
|
2602
|
+
});
|
|
2603
|
+
function expandSource(source, catalog) {
|
|
2604
|
+
if ("event" in source) {
|
|
2605
|
+
if (!catalog.events[source.event]) {
|
|
2606
|
+
return unavailable(
|
|
2607
|
+
`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`
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
return { form: "event", events: [source.event] };
|
|
2611
|
+
}
|
|
2612
|
+
if ("namespace" in source) {
|
|
2613
|
+
const events = catalog.namespaces[source.namespace];
|
|
2614
|
+
if (!events?.length) {
|
|
2615
|
+
return unavailable(
|
|
2616
|
+
`no event name starts with "${source.namespace}." \u2014 the registered namespaces are ${Object.keys(catalog.namespaces).join(", ")}`
|
|
2617
|
+
);
|
|
2618
|
+
}
|
|
2619
|
+
return { form: "namespace", events: [...events] };
|
|
2620
|
+
}
|
|
2621
|
+
if ("kind" in source) {
|
|
2622
|
+
const events = Object.keys(catalog.events).filter((n) => catalog.events[n].kind === source.kind);
|
|
2623
|
+
if (!events.length) {
|
|
2624
|
+
return unavailable(
|
|
2625
|
+
`no event is registered with kind "${source.kind}" \u2014 declare one, or pick a kind the registry uses`
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2628
|
+
return { form: "kind", events, kind: source.kind };
|
|
2629
|
+
}
|
|
2630
|
+
const family = catalog.families[source.family];
|
|
2631
|
+
if (!family) {
|
|
2632
|
+
return unavailable(
|
|
2633
|
+
`no rollup family "${source.family}" is declared \u2014 add a \`rollups: [{ as: '${source.family}', by: [...] }]\` block to the event that should feed it`
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
return { form: "family", events: [...family.feeders], family };
|
|
2637
|
+
}
|
|
2638
|
+
var FILTER_ONLY = {
|
|
2639
|
+
"field:subject": "subject",
|
|
2640
|
+
"field:traceId": "traceId"
|
|
2641
|
+
};
|
|
2642
|
+
function dimsFor(catalog, events) {
|
|
2643
|
+
const out = /* @__PURE__ */ new Map();
|
|
2644
|
+
for (const d of catalog.envelope) out.set(d.key, d);
|
|
2645
|
+
for (const name of events) {
|
|
2646
|
+
for (const d of catalog.events[name]?.dims ?? []) {
|
|
2647
|
+
const seen = out.get(d.key);
|
|
2648
|
+
out.set(d.key, seen ? { ...seen, indexed: seen.indexed && d.indexed } : d);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
return out;
|
|
2652
|
+
}
|
|
2653
|
+
var measureDeclared = (catalog, events, key) => key === "count" || events.some((n) => catalog.events[n]?.measures.some((m) => m.key === key));
|
|
2654
|
+
var MEASURE_OP = /^(sum|avg|p50|p90|p95|p99):(.+)$/;
|
|
2655
|
+
var unavailable = (why) => ({ unavailable: true, why });
|
|
2656
|
+
var count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
2657
|
+
function resolveReport(report, catalog, opts = {}) {
|
|
2658
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
2659
|
+
const limits = opts.limits ?? {};
|
|
2660
|
+
const src = expandSource(report.source, catalog);
|
|
2661
|
+
if ("unavailable" in src) return src;
|
|
2662
|
+
const measure = report.measure ?? "count";
|
|
2663
|
+
const groupBy = report.groupBy ?? [];
|
|
2664
|
+
if (groupBy.length > 2) {
|
|
2665
|
+
return unavailable(
|
|
2666
|
+
`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`
|
|
2667
|
+
);
|
|
2668
|
+
}
|
|
2669
|
+
if (report.interval && INTERVAL_RANK[report.interval] == null) {
|
|
2670
|
+
return unavailable(`interval "${report.interval}" is not one of hour, day, week, month`);
|
|
2671
|
+
}
|
|
2672
|
+
if (measure === "funnel") return planFunnel(report, catalog, now, limits);
|
|
2673
|
+
if (measure.startsWith("distinct:")) return planDistinct(report, catalog, src, measure, now);
|
|
2674
|
+
const opMatch = MEASURE_OP.exec(measure);
|
|
2675
|
+
if (measure !== "count" && !opMatch) {
|
|
2676
|
+
return unavailable(
|
|
2677
|
+
`measure "${measure}" is not a measure \u2014 use 'count', 'sum:<metric>', 'avg:<metric>', 'p50|p95|p99:<metric>', 'distinct:<subjectType>' or 'funnel'`
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
if (opMatch && !measureDeclared(catalog, src.events, measure)) {
|
|
2681
|
+
return unavailable(
|
|
2682
|
+
`"${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)}`
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
const exact = planRollups(report, catalog, src, measure, groupBy, now, limits);
|
|
2686
|
+
if (exact) return exact;
|
|
2687
|
+
const filter = toRecordFilter(report, catalog, src);
|
|
2688
|
+
if ("unavailable" in filter) return filter;
|
|
2689
|
+
const range = rangeOf(report.range, now);
|
|
2690
|
+
const dims = dimsFor(catalog, src.events);
|
|
2691
|
+
const touched = [...groupBy, ...(report.filters ?? []).map((f) => f.dim)];
|
|
2692
|
+
const unindexed = touched.find((k) => !(dims.get(k)?.indexed ?? FILTER_ONLY[k] != null));
|
|
2693
|
+
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` : "") : "";
|
|
2694
|
+
const exactness = unindexed != null ? "scan" : "raw";
|
|
2695
|
+
if (opMatch && opMatch[1] !== "sum" && opMatch[1] !== "avg") {
|
|
2696
|
+
if (groupBy.length) {
|
|
2697
|
+
return unavailable(
|
|
2698
|
+
`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`
|
|
2699
|
+
);
|
|
2700
|
+
}
|
|
2701
|
+
return withCompare(report, {
|
|
2702
|
+
primitive: "distribution",
|
|
2703
|
+
args: [range, filter, { measure: opMatch[2] }],
|
|
2704
|
+
exactness,
|
|
2705
|
+
why: `${measure} is approximate by construction ($percentile t-digest over the matched records)${scanWhy}`
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
if (groupBy.length) {
|
|
2709
|
+
const bad = groupBy.find((k) => !dims.has(k));
|
|
2710
|
+
if (bad) {
|
|
2711
|
+
return unavailable(
|
|
2712
|
+
`"${bad}" is not a dimension of ${describe(src)} \u2014 group by one of ${[...dims.keys()].join(", ")}`
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
2715
|
+
return withCompare(report, {
|
|
2716
|
+
primitive: "breakdown",
|
|
2717
|
+
args: [
|
|
2718
|
+
range,
|
|
2719
|
+
filter,
|
|
2720
|
+
{
|
|
2721
|
+
groupBy,
|
|
2722
|
+
measure,
|
|
2723
|
+
...report.interval ? { interval: report.interval } : {},
|
|
2724
|
+
...report.limit ? { limit: capped(report.limit, limits.breakdown) } : {}
|
|
2725
|
+
}
|
|
2726
|
+
],
|
|
2727
|
+
exactness,
|
|
2728
|
+
why: `raw ${measure} by ${groupBy.join(" \xD7 ")} over the range${scanWhy}`
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
if (!report.measure && !report.interval) {
|
|
2732
|
+
return withCompare(report, {
|
|
2733
|
+
primitive: "records",
|
|
2734
|
+
args: [range, filter, report.limit ? { limit: capped(report.limit, limits.records) } : {}],
|
|
2735
|
+
exactness,
|
|
2736
|
+
why: `the matching records themselves, newest first${scanWhy}`
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
const interval = report.interval ?? intervalForRange(report.range, now);
|
|
2740
|
+
return withCompare(report, {
|
|
2741
|
+
primitive: "series",
|
|
2742
|
+
args: [range, filter, { measure, interval }],
|
|
2743
|
+
exactness,
|
|
2744
|
+
why: `raw ${measure} per ${interval} over the range${scanWhy}`
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
var capped = (limit, cap) => cap == null ? limit : Math.max(1, Math.min(limit, cap));
|
|
2748
|
+
var describe = (src) => src.form === "family" ? `family "${src.family.as}"` : src.events.join(", ");
|
|
2749
|
+
var metricList = (catalog, events) => {
|
|
2750
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2751
|
+
for (const n of events) for (const m of catalog.events[n]?.measures ?? []) keys.add(m.key);
|
|
2752
|
+
return keys.size ? [...keys].join(", ") : "nothing but count";
|
|
2753
|
+
};
|
|
2754
|
+
function planFunnel(report, catalog, now, limits) {
|
|
2755
|
+
const stages = report.stages ?? [];
|
|
2756
|
+
if (!stages.length) {
|
|
2757
|
+
return unavailable(
|
|
2758
|
+
"`measure: 'funnel'` needs `stages` \u2014 one or more lifetime `by: ['subject']` rollup family names, in the order a subject reaches them"
|
|
2759
|
+
);
|
|
2760
|
+
}
|
|
2761
|
+
const anchor = report.anchor ?? stages[0];
|
|
2762
|
+
const exits = report.exits ?? [];
|
|
2763
|
+
for (const as of [...stages, anchor, ...exits]) {
|
|
2764
|
+
const refusal = milestoneRefusal(catalog, as);
|
|
2765
|
+
if (refusal) return unavailable(refusal);
|
|
2766
|
+
}
|
|
2767
|
+
const first = catalog.families[stages[0]];
|
|
2768
|
+
for (const as of [...stages.slice(1), anchor]) {
|
|
2769
|
+
const f = catalog.families[as];
|
|
2770
|
+
if (!sameSet(f.subjectTypes, first.subjectTypes)) {
|
|
2771
|
+
return unavailable(
|
|
2772
|
+
`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`
|
|
2773
|
+
);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
if (report.subjectType && !first.subjectTypes.includes(report.subjectType)) {
|
|
2777
|
+
return unavailable(
|
|
2778
|
+
`subjectType "${report.subjectType}" is not one of the stages' subjects (${first.subjectTypes.join(", ")}) \u2014 the cohort would be empty`
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
if (report.interval === "hour") {
|
|
2782
|
+
return unavailable(
|
|
2783
|
+
"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"
|
|
2784
|
+
);
|
|
2785
|
+
}
|
|
2786
|
+
const params = {
|
|
2787
|
+
stages: stages.map((as) => ({ as })),
|
|
2788
|
+
anchor,
|
|
2789
|
+
...exits.length ? { exits: exits.map((as) => ({ as })) } : {},
|
|
2790
|
+
cohort: rangeOf(report.range, now),
|
|
2791
|
+
...report.subjectType ? { subjectType: report.subjectType } : {},
|
|
2792
|
+
...report.interval ? { interval: report.interval } : {},
|
|
2793
|
+
...report.limit ? { limit: capped(report.limit, limits.funnel) } : {}
|
|
2794
|
+
};
|
|
2795
|
+
return withCompare(report, {
|
|
2796
|
+
primitive: "funnel",
|
|
2797
|
+
args: [params],
|
|
2798
|
+
exactness: "exact",
|
|
2799
|
+
via: anchor,
|
|
2800
|
+
why: `cohort funnel over ${count(stages.length, "lifetime milestone family")}, anchored on "${anchor}" \u2014 rollups only, no raw scan`
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
function milestoneRefusal(catalog, as) {
|
|
2804
|
+
const f = catalog.families[as];
|
|
2805
|
+
if (!f) {
|
|
2806
|
+
return `no rollup family "${as}" is declared. Add a \`rollups: [{ as: '${as}', by: ['subject'], subjects: [...] }]\` block to the event that marks it`;
|
|
2807
|
+
}
|
|
2808
|
+
const shape = `by: [${f.by.map((d) => `'${d}'`).join(", ")}]${f.bucket ? `, bucket: '${f.bucket}'` : ""}`;
|
|
2809
|
+
if (!f.lifetime) {
|
|
2810
|
+
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`;
|
|
2811
|
+
}
|
|
2812
|
+
if (f.by.length !== 1 || f.by[0] !== "subject") {
|
|
2813
|
+
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`;
|
|
2814
|
+
}
|
|
2815
|
+
return null;
|
|
2816
|
+
}
|
|
2817
|
+
function planDistinct(report, catalog, src, measure, now) {
|
|
2818
|
+
const subjectType = measure.slice("distinct:".length);
|
|
2819
|
+
if (!subjectType) {
|
|
2820
|
+
return unavailable(
|
|
2821
|
+
`"${measure}" needs a subject type \u2014 'distinct:account', one of ${catalog.subjectTypes.join(", ")}`
|
|
2822
|
+
);
|
|
2823
|
+
}
|
|
2824
|
+
if (!catalog.subjectTypes.includes(subjectType)) {
|
|
2825
|
+
return unavailable(
|
|
2826
|
+
`no event or rollup declares the subject type "${subjectType}" \u2014 the registry knows ${catalog.subjectTypes.join(", ") || "no subject types at all"}`
|
|
2827
|
+
);
|
|
2828
|
+
}
|
|
2829
|
+
if (report.groupBy?.length) {
|
|
2830
|
+
return unavailable(
|
|
2831
|
+
`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`
|
|
2832
|
+
);
|
|
2833
|
+
}
|
|
2834
|
+
const wanted = new Set(src.events);
|
|
2835
|
+
const fits = Object.values(catalog.families).filter(
|
|
2836
|
+
(f) => f.bucket != null && f.by.length === 1 && f.by[0] === "subject" && f.subjectTypes.includes(subjectType) && src.events.every((e) => f.feeders.includes(e))
|
|
2837
|
+
);
|
|
2838
|
+
const family = fits.find((f) => sameSet(f.feeders, [...wanted])) ?? fits[0];
|
|
2839
|
+
if (!family) {
|
|
2840
|
+
return unavailable(
|
|
2841
|
+
`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`
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
const superset = !sameSet(family.feeders, [...wanted]);
|
|
2845
|
+
return withCompare(report, {
|
|
2846
|
+
primitive: "distinctCount",
|
|
2847
|
+
args: [
|
|
2848
|
+
{
|
|
2849
|
+
as: family.as,
|
|
2850
|
+
subjectType,
|
|
2851
|
+
range: rangeOf(report.range, now),
|
|
2852
|
+
...report.interval ? { interval: report.interval } : {}
|
|
2853
|
+
}
|
|
2854
|
+
],
|
|
2855
|
+
exactness: "exact",
|
|
2856
|
+
via: family.as,
|
|
2857
|
+
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` : "")
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
var famDims = (f) => f.by.map((b) => b === "subject" ? "subjectType" : b);
|
|
2861
|
+
function planRollups(report, catalog, src, measure, groupBy, now, limits) {
|
|
2862
|
+
if (report.excludeActorTypes?.length) return null;
|
|
2863
|
+
const candidates = src.family ? [src.family] : Object.values(catalog.families).filter((f) => sameSet(f.feeders, src.events));
|
|
2864
|
+
const op = MEASURE_OP.exec(measure);
|
|
2865
|
+
const filters = report.filters ?? [];
|
|
2866
|
+
const fits = candidates.filter((f) => {
|
|
2867
|
+
const dims2 = famDims(f);
|
|
2868
|
+
if (!groupBy.every((k) => dims2.includes(k))) return false;
|
|
2869
|
+
if (report.interval && (!f.bucket || INTERVAL_RANK[f.bucket] > INTERVAL_RANK[report.interval])) return false;
|
|
2870
|
+
if (op) {
|
|
2871
|
+
if (op[1] !== "sum" && op[1] !== "avg" || !f.sums.includes(op[2])) return false;
|
|
2872
|
+
}
|
|
2873
|
+
return filters.every(
|
|
2874
|
+
(t) => dims2.includes(t.dim) && (t.op === "eq" || t.op === "in") || nameFilterCovers(t, f)
|
|
2875
|
+
);
|
|
2876
|
+
});
|
|
2877
|
+
const family = fits.sort((a, b) => a.by.length - b.by.length)[0];
|
|
2878
|
+
if (!family) return null;
|
|
2879
|
+
const dims = famDims(family);
|
|
2880
|
+
const range = rangeOf(report.range, now);
|
|
2881
|
+
const on = family.lifetime ? "firstAt" : "bucketAt";
|
|
2882
|
+
const fold = filters.filter((t) => dims.includes(t.dim));
|
|
2883
|
+
return withCompare(report, {
|
|
2884
|
+
primitive: "rollups",
|
|
2885
|
+
args: [
|
|
2886
|
+
{
|
|
2887
|
+
as: family.as,
|
|
2888
|
+
on,
|
|
2889
|
+
range,
|
|
2890
|
+
sort: family.lifetime ? "count" : "bucketAt",
|
|
2891
|
+
...report.limit ? { limit: capped(report.limit, limits.rollups) } : {}
|
|
2892
|
+
}
|
|
2893
|
+
],
|
|
2894
|
+
exactness: "exact",
|
|
2895
|
+
via: family.as,
|
|
2896
|
+
shape: {
|
|
2897
|
+
groupBy,
|
|
2898
|
+
labels: groupBy.map((k) => family.labels[dims.indexOf(k)]),
|
|
2899
|
+
measure,
|
|
2900
|
+
...report.interval ? { interval: report.interval } : {},
|
|
2901
|
+
...fold.length ? { filters: fold.map((t) => ({ ...t, label: family.labels[dims.indexOf(t.dim)] })) } : {}
|
|
2902
|
+
},
|
|
2903
|
+
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` : "")
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
function nameFilterCovers(t, f) {
|
|
2907
|
+
if (t.dim !== "field:name") return false;
|
|
2908
|
+
const admitted = t.op === "eq" ? [String(t.value)] : t.op === "in" ? [...t.value].map(String) : null;
|
|
2909
|
+
return admitted != null && f.feeders.every((n) => admitted.includes(n));
|
|
2910
|
+
}
|
|
2911
|
+
var FIELD_TERMS = {
|
|
2912
|
+
"field:kind": "kind",
|
|
2913
|
+
"field:name": "name",
|
|
2914
|
+
"field:severity": "severity",
|
|
2915
|
+
"field:env": "env",
|
|
2916
|
+
"field:service": "service",
|
|
2917
|
+
"field:release": "release",
|
|
2918
|
+
...FILTER_ONLY
|
|
2919
|
+
};
|
|
2920
|
+
function toRecordFilter(report, catalog, src) {
|
|
2921
|
+
const filter = {};
|
|
2922
|
+
if (src.events.length === 1) {
|
|
2923
|
+
filter.name = src.events[0];
|
|
2924
|
+
} else if (src.kind) {
|
|
2925
|
+
filter.kind = src.kind;
|
|
2926
|
+
} else {
|
|
2927
|
+
filter.name = [...src.events];
|
|
2928
|
+
}
|
|
2929
|
+
for (const t of report.filters ?? []) {
|
|
2930
|
+
const term = FIELD_TERMS[t.dim];
|
|
2931
|
+
if (term) {
|
|
2932
|
+
if (t.op !== "eq") {
|
|
2933
|
+
return unavailable(
|
|
2934
|
+
`"${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`
|
|
2935
|
+
);
|
|
2936
|
+
}
|
|
2937
|
+
filter[term] = String(t.value);
|
|
2938
|
+
continue;
|
|
2939
|
+
}
|
|
2940
|
+
if (t.dim.startsWith("attr:")) {
|
|
2941
|
+
const key = t.dim.slice(5);
|
|
2942
|
+
if (t.op === "eq") {
|
|
2943
|
+
(filter.attrs ??= {})[key] = String(t.value);
|
|
2944
|
+
continue;
|
|
2945
|
+
}
|
|
2946
|
+
if (t.op === "gte" || t.op === "lte") {
|
|
2947
|
+
if (!measureDeclared(catalog, src.events, `sum:${key}`)) {
|
|
2948
|
+
return unavailable(
|
|
2949
|
+
`"${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`
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
const range = (filter.metrics ??= {})[key] ??= {};
|
|
2953
|
+
range[t.op] = Number(t.value);
|
|
2954
|
+
continue;
|
|
2955
|
+
}
|
|
2956
|
+
return unavailable(
|
|
2957
|
+
`"${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`
|
|
2958
|
+
);
|
|
2959
|
+
}
|
|
2960
|
+
if (t.dim === "subjectType" || t.dim === "actorType") {
|
|
2961
|
+
return unavailable(
|
|
2962
|
+
`"${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`" : "")
|
|
2963
|
+
);
|
|
2964
|
+
}
|
|
2965
|
+
return unavailable(
|
|
2966
|
+
`"${t.dim}" is not filterable on the raw path \u2014 RecordFilter carries ${Object.keys(FIELD_TERMS).join(", ")}, \`attr:<key>\` and metric bounds`
|
|
2967
|
+
);
|
|
2968
|
+
}
|
|
2969
|
+
if (report.excludeActorTypes?.length) filter.excludeActorTypes = [...report.excludeActorTypes];
|
|
2970
|
+
return filter;
|
|
2971
|
+
}
|
|
2972
|
+
function withCompare(report, plan) {
|
|
2973
|
+
if (report.compare !== "previous") return plan;
|
|
2974
|
+
const [first, ...rest] = plan.args;
|
|
2975
|
+
if (plan.primitive === "rollups" || plan.primitive === "distinctCount" || plan.primitive === "funnel") {
|
|
2976
|
+
const params = first;
|
|
2977
|
+
const key = plan.primitive === "funnel" ? "cohort" : "range";
|
|
2978
|
+
const window = params[key];
|
|
2979
|
+
if (!window) return plan;
|
|
2980
|
+
return { ...plan, previous: { args: [{ ...params, [key]: { ...window, ...shift(window) } }, ...rest] } };
|
|
2981
|
+
}
|
|
2982
|
+
return { ...plan, previous: { args: [shift(first), ...rest] } };
|
|
2983
|
+
}
|
|
2984
|
+
var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
2985
|
+
var FILTER_OPS = /* @__PURE__ */ new Set(["eq", "in", "gte", "lte"]);
|
|
2986
|
+
var SORTS = /* @__PURE__ */ new Set(["value", "label", "time"]);
|
|
2987
|
+
function parseReportQuery(q) {
|
|
2988
|
+
const str = (k) => {
|
|
2989
|
+
const v = Array.isArray(q[k]) ? q[k][0] : q[k];
|
|
2990
|
+
return typeof v === "string" && v ? v : void 0;
|
|
2991
|
+
};
|
|
2992
|
+
const list = (k) => (str(k) ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
2993
|
+
const raw = str("source");
|
|
2994
|
+
if (!raw) {
|
|
2995
|
+
throw badRequest2(
|
|
2996
|
+
"`source` is required \u2014 one of source=event:<name>, namespace:<ns>, kind:<kind>, family:<as>"
|
|
2997
|
+
);
|
|
2998
|
+
}
|
|
2999
|
+
const cut = raw.indexOf(":");
|
|
3000
|
+
const form = cut > 0 ? raw.slice(0, cut) : "";
|
|
3001
|
+
const named = cut > 0 ? raw.slice(cut + 1) : "";
|
|
3002
|
+
if (!named || !["event", "namespace", "kind", "family"].includes(form)) {
|
|
3003
|
+
throw badRequest2(
|
|
3004
|
+
`\`source\` must be "event:<name>", "namespace:<ns>", "kind:<kind>" or "family:<as>" \u2014 got "${raw}"`
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
const source = form === "event" ? { event: named } : form === "namespace" ? { namespace: named } : form === "kind" ? { kind: named } : { family: named };
|
|
3008
|
+
const shorthand = str("range");
|
|
3009
|
+
const from = str("from");
|
|
3010
|
+
const to = str("to");
|
|
3011
|
+
if (!shorthand && !(from && to)) {
|
|
3012
|
+
throw badRequest2("a range is required \u2014 either `range=7d` or both `from` and `to` as ISO times");
|
|
3013
|
+
}
|
|
3014
|
+
const range = shorthand ?? { from, to };
|
|
3015
|
+
const interval = str("interval");
|
|
3016
|
+
if (interval && INTERVAL_RANK[interval] == null) {
|
|
3017
|
+
throw badRequest2(`\`interval\` must be one of hour, day, week, month \u2014 got "${interval}"`);
|
|
3018
|
+
}
|
|
3019
|
+
const sort = str("sort");
|
|
3020
|
+
if (sort && !SORTS.has(sort)) {
|
|
3021
|
+
throw badRequest2(`\`sort\` must be one of value, label, time \u2014 got "${sort}"`);
|
|
3022
|
+
}
|
|
3023
|
+
const compare = str("compare");
|
|
3024
|
+
if (compare && compare !== "previous") {
|
|
3025
|
+
throw badRequest2(`\`compare\` takes only "previous" \u2014 got "${compare}"`);
|
|
3026
|
+
}
|
|
3027
|
+
const limitRaw = str("limit");
|
|
3028
|
+
const limit = limitRaw == null ? void 0 : Number(limitRaw);
|
|
3029
|
+
if (limit != null && (!Number.isInteger(limit) || limit < 1)) {
|
|
3030
|
+
throw badRequest2(`\`limit\` must be a positive integer \u2014 got "${limitRaw}"`);
|
|
3031
|
+
}
|
|
3032
|
+
const measure = str("measure");
|
|
3033
|
+
const groupBy = list("groupBy");
|
|
3034
|
+
const excludeActorTypes = list("excludeActors");
|
|
3035
|
+
const stages = list("stages");
|
|
3036
|
+
const exits = list("exits");
|
|
3037
|
+
const anchor = str("anchor");
|
|
3038
|
+
const subjectType = str("subjectType");
|
|
3039
|
+
const filters = (q.filter == null ? [] : Array.isArray(q.filter) ? q.filter.map(String) : [String(q.filter)]).map(parseFilterTerm);
|
|
3040
|
+
return {
|
|
3041
|
+
source,
|
|
3042
|
+
range,
|
|
3043
|
+
...interval ? { interval } : {},
|
|
3044
|
+
...measure ? { measure } : {},
|
|
3045
|
+
...groupBy.length ? { groupBy } : {},
|
|
3046
|
+
...filters.length ? { filters } : {},
|
|
3047
|
+
...excludeActorTypes.length ? { excludeActorTypes } : {},
|
|
3048
|
+
...sort ? { sort } : {},
|
|
3049
|
+
...limit != null ? { limit } : {},
|
|
3050
|
+
...compare ? { compare: "previous" } : {},
|
|
3051
|
+
...stages.length ? { stages } : {},
|
|
3052
|
+
...anchor ? { anchor } : {},
|
|
3053
|
+
...exits.length ? { exits } : {},
|
|
3054
|
+
...subjectType ? { subjectType } : {}
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
function parseFilterTerm(term) {
|
|
3058
|
+
const parts = term.split(":");
|
|
3059
|
+
const i = parts.findIndex((p) => FILTER_OPS.has(p));
|
|
3060
|
+
const rest = i < 0 ? "" : parts.slice(i + 1).join(":");
|
|
3061
|
+
if (i < 1 || !rest) {
|
|
3062
|
+
throw badRequest2(
|
|
3063
|
+
`\`filter\` must be "<dim>:<op>:<value>" with op one of eq, in, gte, lte \u2014 got "${term}"`
|
|
3064
|
+
);
|
|
3065
|
+
}
|
|
3066
|
+
const dim2 = parts.slice(0, i).join(":");
|
|
3067
|
+
const op = parts[i];
|
|
3068
|
+
if (op === "in") {
|
|
3069
|
+
const values = rest.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3070
|
+
if (!values.length) throw badRequest2(`\`filter\` "${term}" has an empty \`in\` list`);
|
|
3071
|
+
return { dim: dim2, op, value: values };
|
|
3072
|
+
}
|
|
3073
|
+
if (op === "gte" || op === "lte") {
|
|
3074
|
+
const n = Number(rest);
|
|
3075
|
+
if (Number.isNaN(n)) throw badRequest2(`\`filter\` bound "${term}" is not a number`);
|
|
3076
|
+
return { dim: dim2, op, value: n };
|
|
3077
|
+
}
|
|
3078
|
+
return { dim: dim2, op, value: rest };
|
|
3079
|
+
}
|
|
3080
|
+
function reportToQuery(report) {
|
|
3081
|
+
const s = report.source;
|
|
3082
|
+
const q = {
|
|
3083
|
+
source: "event" in s ? `event:${s.event}` : "namespace" in s ? `namespace:${s.namespace}` : "kind" in s ? `kind:${s.kind}` : `family:${s.family}`
|
|
3084
|
+
};
|
|
3085
|
+
if (typeof report.range === "string") q.range = report.range;
|
|
3086
|
+
else {
|
|
3087
|
+
q.from = report.range.from;
|
|
3088
|
+
q.to = report.range.to;
|
|
3089
|
+
}
|
|
3090
|
+
if (report.interval) q.interval = report.interval;
|
|
3091
|
+
if (report.measure) q.measure = report.measure;
|
|
3092
|
+
if (report.groupBy?.length) q.groupBy = report.groupBy.join(",");
|
|
3093
|
+
if (report.filters?.length) {
|
|
3094
|
+
const terms = report.filters.map(
|
|
3095
|
+
(f) => `${f.dim}:${f.op}:${Array.isArray(f.value) ? f.value.join(",") : String(f.value)}`
|
|
3096
|
+
);
|
|
3097
|
+
q.filter = terms.length === 1 ? terms[0] : terms;
|
|
3098
|
+
}
|
|
3099
|
+
if (report.excludeActorTypes?.length) q.excludeActors = report.excludeActorTypes.join(",");
|
|
3100
|
+
if (report.sort) q.sort = report.sort;
|
|
3101
|
+
if (report.limit != null) q.limit = String(report.limit);
|
|
3102
|
+
if (report.compare) q.compare = report.compare;
|
|
3103
|
+
if (report.stages?.length) q.stages = report.stages.join(",");
|
|
3104
|
+
if (report.anchor) q.anchor = report.anchor;
|
|
3105
|
+
if (report.exits?.length) q.exits = report.exits.join(",");
|
|
3106
|
+
if (report.subjectType) q.subjectType = report.subjectType;
|
|
3107
|
+
return q;
|
|
3108
|
+
}
|
|
3109
|
+
var LEGACY_DIMS = {
|
|
3110
|
+
kind: "field:kind",
|
|
3111
|
+
name: "field:name",
|
|
3112
|
+
severity: "field:severity",
|
|
3113
|
+
env: "field:env",
|
|
3114
|
+
service: "field:service",
|
|
3115
|
+
release: "field:release",
|
|
3116
|
+
subject: "field:subject",
|
|
3117
|
+
traceId: "field:traceId"
|
|
3118
|
+
};
|
|
3119
|
+
function normalizeQuery(query) {
|
|
3120
|
+
if (!query || typeof query !== "object") return null;
|
|
3121
|
+
if ("source" in query && query.source) return query;
|
|
3122
|
+
const q = query;
|
|
3123
|
+
const filters = q.filters ?? {};
|
|
3124
|
+
const str = (v) => typeof v === "string" && v ? v : null;
|
|
3125
|
+
const name = str(filters.name);
|
|
3126
|
+
const family = str(filters.rollup);
|
|
3127
|
+
const kind = str(filters.kind);
|
|
3128
|
+
const source = name ? { event: name } : family ? { family } : kind ? { kind } : null;
|
|
3129
|
+
if (!source) return null;
|
|
3130
|
+
const consumed = name ? "name" : family ? "rollup" : "kind";
|
|
3131
|
+
const terms = [];
|
|
3132
|
+
for (const [k, v] of Object.entries(filters)) {
|
|
3133
|
+
if (k === consumed || k === "rollup") continue;
|
|
3134
|
+
if (k === "excludeActorTypes") continue;
|
|
3135
|
+
if (k === "attrs") {
|
|
3136
|
+
const entries = typeof v === "string" ? v.split(",").map((pair) => pair.split(":").map((s) => s.trim())) : Object.entries(v ?? {}).map(([a, b]) => [a, String(b)]);
|
|
3137
|
+
for (const [key, value2] of entries) {
|
|
3138
|
+
if (key && value2 != null) terms.push({ dim: `attr:${key}`, op: "eq", value: String(value2) });
|
|
3139
|
+
}
|
|
3140
|
+
continue;
|
|
3141
|
+
}
|
|
3142
|
+
const dim2 = LEGACY_DIMS[k];
|
|
3143
|
+
const value = str(v);
|
|
3144
|
+
if (dim2 && value) terms.push({ dim: dim2, op: "eq", value });
|
|
3145
|
+
}
|
|
3146
|
+
const groupBy = (q.groupBy ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3147
|
+
const sort = q.sort === "value" || q.sort === "label" || q.sort === "time" ? q.sort : void 0;
|
|
3148
|
+
const actors = filters.excludeActorTypes;
|
|
3149
|
+
return {
|
|
3150
|
+
source,
|
|
3151
|
+
range: q.range ?? "7d",
|
|
3152
|
+
...terms.length ? { filters: terms } : {},
|
|
3153
|
+
...groupBy.length ? { groupBy } : {},
|
|
3154
|
+
...sort ? { sort } : {},
|
|
3155
|
+
...Array.isArray(actors) && actors.length ? { excludeActorTypes: actors.map(String) } : {}
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
// src/server/execute.ts
|
|
3160
|
+
async function executeReport(q, scope, report, catalog, opts = {}) {
|
|
3161
|
+
const plan = resolveReport(report, catalog, { now: opts.now, limits: opts.limits });
|
|
3162
|
+
if ("unavailable" in plan) throw Object.assign(new Error(plan.why), { status: 400 });
|
|
3163
|
+
const run = async (args) => {
|
|
3164
|
+
const raw = await q[plan.primitive](scope, ...args);
|
|
3165
|
+
if (plan.primitive === "rollups" && plan.shape) {
|
|
3166
|
+
return foldRollups(raw?.rows ?? [], plan.shape, !!raw?.truncated);
|
|
3167
|
+
}
|
|
3168
|
+
if (plan.primitive === "records" && opts.redact) {
|
|
3169
|
+
return { ...raw, items: opts.redact(raw?.items ?? []) };
|
|
3170
|
+
}
|
|
3171
|
+
return raw;
|
|
3172
|
+
};
|
|
3173
|
+
const [result, previous] = await Promise.all([
|
|
3174
|
+
run(plan.args),
|
|
3175
|
+
plan.previous ? run(plan.previous.args) : Promise.resolve(void 0)
|
|
3176
|
+
]);
|
|
3177
|
+
return {
|
|
3178
|
+
report,
|
|
3179
|
+
plan,
|
|
3180
|
+
result,
|
|
3181
|
+
...plan.previous ? { previous } : {},
|
|
3182
|
+
dataSource: result?.dataSource ?? "raw"
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
var MEASURE_OP2 = /^(sum|avg):(.+)$/;
|
|
3186
|
+
function foldRollups(rows, shape, truncated = false) {
|
|
3187
|
+
const op = MEASURE_OP2.exec(shape.measure);
|
|
3188
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3189
|
+
for (const doc of rows) {
|
|
3190
|
+
const dims = doc?.dims ?? [];
|
|
3191
|
+
if (!(shape.filters ?? []).every((f) => admits(f, dimValue(dims, f.label)))) continue;
|
|
3192
|
+
const tuple = shape.labels.map((label3) => dimValue(dims, label3));
|
|
3193
|
+
const at = shape.interval && doc.bucketAt ? truncate(new Date(doc.bucketAt), shape.interval) : void 0;
|
|
3194
|
+
const key = `${JSON.stringify(tuple)}|${at ? at.getTime() : ""}`;
|
|
3195
|
+
let g = groups.get(key);
|
|
3196
|
+
if (!g) groups.set(key, g = { dims: tuple, ...at ? { at } : {}, sum: 0, count: 0 });
|
|
3197
|
+
g.count += typeof doc.count === "number" ? doc.count : 0;
|
|
3198
|
+
if (op) g.sum += sumOf(doc.sums, op[2]);
|
|
3199
|
+
}
|
|
3200
|
+
const rowsOut = [...groups.values()].map((g) => ({
|
|
3201
|
+
dims: g.dims,
|
|
3202
|
+
...g.at ? { at: g.at } : {},
|
|
3203
|
+
// avg is sums[k]/count off the SAME doc, which is exact — not an average of
|
|
3204
|
+
// averages, which is what folding a per-bucket mean would have produced
|
|
3205
|
+
value: !op ? g.count : op[1] === "sum" ? g.sum : g.count ? g.sum / g.count : 0
|
|
3206
|
+
}));
|
|
3207
|
+
rowsOut.sort(
|
|
3208
|
+
shape.interval ? (a, b) => (a.at?.getTime() ?? 0) - (b.at?.getTime() ?? 0) || byDims(a, b) : (a, b) => b.value - a.value || byDims(a, b)
|
|
3209
|
+
);
|
|
3210
|
+
return {
|
|
3211
|
+
rows: rowsOut,
|
|
3212
|
+
groups: new Set([...groups.values()].map((g) => JSON.stringify(g.dims))).size,
|
|
3213
|
+
truncated,
|
|
3214
|
+
dataSource: "rollups"
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
function dimValue(dims, label3) {
|
|
3218
|
+
const prefix = `${label3}=`;
|
|
3219
|
+
for (const d of dims) if (d.startsWith(prefix)) return d.slice(prefix.length);
|
|
3220
|
+
for (const d of dims) if (!d.includes("=")) return d;
|
|
3221
|
+
return null;
|
|
3222
|
+
}
|
|
3223
|
+
function admits(f, value) {
|
|
3224
|
+
if (f.op === "in") return f.value.map(String).includes(String(value));
|
|
3225
|
+
if (f.op === "gte" || f.op === "lte") {
|
|
3226
|
+
const n = Number(value);
|
|
3227
|
+
if (Number.isNaN(n)) return false;
|
|
3228
|
+
return f.op === "gte" ? n >= Number(f.value) : n <= Number(f.value);
|
|
3229
|
+
}
|
|
3230
|
+
return String(value) === String(f.value);
|
|
3231
|
+
}
|
|
3232
|
+
function sumOf(sums, key) {
|
|
3233
|
+
if (!sums) return 0;
|
|
3234
|
+
const v = sums instanceof Map ? sums.get(key) : sums[key];
|
|
3235
|
+
return typeof v === "number" ? v : 0;
|
|
3236
|
+
}
|
|
3237
|
+
function byDims(a, b) {
|
|
3238
|
+
for (let i = 0; i < a.dims.length; i++) {
|
|
3239
|
+
const x = a.dims[i] ?? "";
|
|
3240
|
+
const y = b.dims[i] ?? "";
|
|
3241
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
3242
|
+
}
|
|
3243
|
+
return 0;
|
|
3244
|
+
}
|
|
1830
3245
|
function buildViewModel(connection, modelName, collection) {
|
|
1831
3246
|
const existing = connection.models?.[modelName];
|
|
1832
3247
|
if (existing) return existing;
|
|
@@ -1853,31 +3268,58 @@ var KIND_PAGE = {
|
|
|
1853
3268
|
state: "journeys",
|
|
1854
3269
|
usage: "usage"
|
|
1855
3270
|
};
|
|
1856
|
-
function deriveViews(registry) {
|
|
3271
|
+
function deriveViews(registry, catalog = deriveCatalog(registry)) {
|
|
1857
3272
|
const views = [];
|
|
1858
|
-
const
|
|
1859
|
-
for (const [name,
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
query: { range: "7d", filters: { name }, display: spec.kind === "event" ? "series" : "table" }
|
|
3273
|
+
const derived = (name, page, query) => views.push({ origin: "derived", name, page, query });
|
|
3274
|
+
for (const [name, e] of Object.entries(catalog.events)) {
|
|
3275
|
+
derived(name, KIND_PAGE[e.kind] ?? "events", {
|
|
3276
|
+
source: { event: name },
|
|
3277
|
+
range: "7d",
|
|
3278
|
+
interval: intervalForRange("7d")
|
|
1865
3279
|
});
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
3280
|
+
}
|
|
3281
|
+
for (const as of Object.keys(catalog.families)) {
|
|
3282
|
+
derived(`rollup: ${as}`, "journeys", { source: { family: as }, range: "30d" });
|
|
3283
|
+
}
|
|
3284
|
+
for (const [ns, names] of Object.entries(catalog.namespaces)) {
|
|
3285
|
+
if (names.length < 2) continue;
|
|
3286
|
+
derived(`namespace: ${ns}`, "explore", {
|
|
3287
|
+
source: { namespace: ns },
|
|
3288
|
+
range: "30d",
|
|
3289
|
+
interval: "day",
|
|
3290
|
+
groupBy: ["field:name"]
|
|
3291
|
+
});
|
|
3292
|
+
}
|
|
3293
|
+
for (const [name, e] of Object.entries(catalog.events)) {
|
|
3294
|
+
if (e.kind !== "usage") continue;
|
|
3295
|
+
const money = e.measures.find((m) => m.key.startsWith("sum:") && m.key.endsWith("_usd"));
|
|
3296
|
+
if (!money) continue;
|
|
3297
|
+
derived(`spend: ${name}`, "usage", {
|
|
3298
|
+
source: { event: name },
|
|
3299
|
+
range: "30d",
|
|
3300
|
+
interval: "day",
|
|
3301
|
+
measure: money.key
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
for (const subjectType of catalog.subjectTypes) {
|
|
3305
|
+
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);
|
|
3306
|
+
if (stages.length < 2) continue;
|
|
3307
|
+
derived(`funnel: ${subjectType}`, "journeys", {
|
|
3308
|
+
// any source expands; the family the funnel is anchored on is the honest one
|
|
3309
|
+
source: { family: stages[0] },
|
|
3310
|
+
range: "30d",
|
|
3311
|
+
interval: "week",
|
|
3312
|
+
measure: "funnel",
|
|
3313
|
+
stages,
|
|
3314
|
+
anchor: stages[0],
|
|
3315
|
+
subjectType
|
|
1874
3316
|
});
|
|
1875
3317
|
}
|
|
1876
3318
|
return views;
|
|
1877
3319
|
}
|
|
1878
3320
|
async function resolveViews(opts) {
|
|
1879
3321
|
const byName = /* @__PURE__ */ new Map();
|
|
1880
|
-
for (const v of deriveViews(opts.registry)) byName.set(v.name, v);
|
|
3322
|
+
for (const v of deriveViews(opts.registry, opts.catalog)) byName.set(v.name, v);
|
|
1881
3323
|
for (const v of opts.configured) byName.set(v.name, { ...v, origin: "configured" });
|
|
1882
3324
|
const saved = await opts.ViewModel.find({
|
|
1883
3325
|
tenantId: opts.tenantId,
|
|
@@ -1936,6 +3378,10 @@ var parseFilter = (q) => {
|
|
|
1936
3378
|
for (const k of ["kind", "name", "severity", "env", "service", "release", "subject", "traceId"]) {
|
|
1937
3379
|
if (typeof q[k] === "string" && q[k]) f[k] = q[k];
|
|
1938
3380
|
}
|
|
3381
|
+
if (typeof q.name === "string" && q.name.includes(",")) {
|
|
3382
|
+
const names = q.name.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3383
|
+
if (names.length) f.name = names.length === 1 ? names[0] : names;
|
|
3384
|
+
}
|
|
1939
3385
|
if (typeof q.attrs === "string" && q.attrs) {
|
|
1940
3386
|
f.attrs = Object.fromEntries(
|
|
1941
3387
|
String(q.attrs).split(",").map((p) => p.split(":")).filter((p) => p.length >= 2).map(([k, ...v]) => [k, v.join(":")])
|
|
@@ -1967,30 +3413,6 @@ var parseDims = (v) => {
|
|
|
1967
3413
|
}
|
|
1968
3414
|
return v.length ? v : void 0;
|
|
1969
3415
|
};
|
|
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
3416
|
function createDashboard(opts) {
|
|
1995
3417
|
const { telemetry: t, viewerAdapter, subjectAdapter, views: configured = [] } = opts;
|
|
1996
3418
|
if (!viewerAdapter?.resolveViewer) {
|
|
@@ -2020,6 +3442,20 @@ function createDashboard(opts) {
|
|
|
2020
3442
|
cacheTtlMs: opts.cacheTtlMs,
|
|
2021
3443
|
cacheSize: opts.cacheSize
|
|
2022
3444
|
});
|
|
3445
|
+
const catalog = deriveCatalog(t.registry, {
|
|
3446
|
+
platforms: t.models.telemetry.schema.path("client")?.schema?.path("platform")?.enumValues
|
|
3447
|
+
});
|
|
3448
|
+
const registry = projectRegistry(catalog);
|
|
3449
|
+
const values = createValues({
|
|
3450
|
+
catalog,
|
|
3451
|
+
TelemetryModel: t.models.telemetry,
|
|
3452
|
+
RollupModel: t.models.rollups,
|
|
3453
|
+
limits: opts.queryLimits,
|
|
3454
|
+
onSlowQuery: opts.onSlowQuery,
|
|
3455
|
+
slowMs: opts.slowMs,
|
|
3456
|
+
cacheTtlMs: opts.cacheTtlMs,
|
|
3457
|
+
cacheSize: opts.cacheSize
|
|
3458
|
+
});
|
|
2023
3459
|
const api = express2.Router();
|
|
2024
3460
|
api.use(express2.json({ limit: "64kb" }));
|
|
2025
3461
|
api.use(async (req, res, next) => {
|
|
@@ -2038,7 +3474,8 @@ function createDashboard(opts) {
|
|
|
2038
3474
|
}, next);
|
|
2039
3475
|
};
|
|
2040
3476
|
api.get("/registry", h(async (req) => ({
|
|
2041
|
-
registry
|
|
3477
|
+
registry,
|
|
3478
|
+
catalog,
|
|
2042
3479
|
kinds: ["event", "error", "span", "state", "usage"],
|
|
2043
3480
|
role: req.viewer.role,
|
|
2044
3481
|
scope: req.viewer.tenantId,
|
|
@@ -2061,6 +3498,14 @@ function createDashboard(opts) {
|
|
|
2061
3498
|
measure: typeof req.query.measure === "string" ? req.query.measure : void 0
|
|
2062
3499
|
})
|
|
2063
3500
|
));
|
|
3501
|
+
api.get("/breakdown", h(
|
|
3502
|
+
async (req) => q.breakdown(req.viewer.tenantId, parseRange(req.query), parseFilter(req.query), {
|
|
3503
|
+
groupBy: String(req.query.groupBy ?? "").split(",").map((s) => s.trim()).filter(Boolean),
|
|
3504
|
+
measure: typeof req.query.measure === "string" ? req.query.measure : void 0,
|
|
3505
|
+
interval: req.query.interval || void 0,
|
|
3506
|
+
limit: req.query.limit ? Number(req.query.limit) : void 0
|
|
3507
|
+
})
|
|
3508
|
+
));
|
|
2064
3509
|
api.get("/rollups", h(async (req) => {
|
|
2065
3510
|
if (typeof req.query.as !== "string" || !req.query.as) {
|
|
2066
3511
|
throw Object.assign(new Error("rollup family required"), { status: 400 });
|
|
@@ -2091,7 +3536,7 @@ function createDashboard(opts) {
|
|
|
2091
3536
|
limit: req.query.limit ? Number(req.query.limit) : void 0
|
|
2092
3537
|
})
|
|
2093
3538
|
));
|
|
2094
|
-
const
|
|
3539
|
+
const badRequest3 = async (run) => {
|
|
2095
3540
|
try {
|
|
2096
3541
|
return await run();
|
|
2097
3542
|
} catch (e) {
|
|
@@ -2104,7 +3549,7 @@ function createDashboard(opts) {
|
|
|
2104
3549
|
if (!stages.length) {
|
|
2105
3550
|
throw Object.assign(new Error("funnel needs `stages` \u2014 a comma-separated list of rollup families"), { status: 400 });
|
|
2106
3551
|
}
|
|
2107
|
-
return
|
|
3552
|
+
return badRequest3(() => q.funnel(req.viewer.tenantId, {
|
|
2108
3553
|
stages,
|
|
2109
3554
|
exits: parseStages(req.query.exits),
|
|
2110
3555
|
anchor: typeof req.query.anchor === "string" ? req.query.anchor : void 0,
|
|
@@ -2119,22 +3564,46 @@ function createDashboard(opts) {
|
|
|
2119
3564
|
if (typeof req.query.as !== "string" || !req.query.as) {
|
|
2120
3565
|
throw Object.assign(new Error("rollup family required"), { status: 400 });
|
|
2121
3566
|
}
|
|
2122
|
-
return
|
|
3567
|
+
return badRequest3(() => q.distinctCount(req.viewer.tenantId, {
|
|
2123
3568
|
as: req.query.as,
|
|
2124
3569
|
subjectType: typeof req.query.subjectType === "string" ? req.query.subjectType : void 0,
|
|
2125
3570
|
range: parseRange(req.query),
|
|
2126
3571
|
interval: req.query.interval || void 0
|
|
2127
3572
|
}));
|
|
2128
3573
|
}));
|
|
3574
|
+
api.get("/values", h(async (req) => {
|
|
3575
|
+
const dim2 = typeof req.query.dim === "string" ? req.query.dim.trim() : "";
|
|
3576
|
+
if (!dim2) {
|
|
3577
|
+
throw Object.assign(new Error("dim required"), { status: 400 });
|
|
3578
|
+
}
|
|
3579
|
+
const names = String(req.query.names ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3580
|
+
return values(req.viewer.tenantId, {
|
|
3581
|
+
dim: dim2,
|
|
3582
|
+
names: names.length ? names : void 0,
|
|
3583
|
+
range: req.query.from || req.query.to ? parseRange(req.query) : void 0,
|
|
3584
|
+
limit: req.query.limit ? Number(req.query.limit) : void 0
|
|
3585
|
+
});
|
|
3586
|
+
}));
|
|
2129
3587
|
api.get("/subjects/describe", h(async (req) => {
|
|
2130
3588
|
const refs = String(req.query.refs ?? "").split(",").filter(Boolean).slice(0, 100);
|
|
2131
3589
|
if (!subjectAdapter) return { refs: {} };
|
|
2132
3590
|
return { refs: await subjectAdapter.describe(refs) };
|
|
2133
3591
|
}));
|
|
3592
|
+
api.get("/report", h(
|
|
3593
|
+
async (req) => badRequest3(
|
|
3594
|
+
() => executeReport(q, req.viewer.tenantId, parseReportQuery(req.query), catalog)
|
|
3595
|
+
)
|
|
3596
|
+
));
|
|
3597
|
+
api.get("/report/plan", h(
|
|
3598
|
+
async (req) => badRequest3(
|
|
3599
|
+
async () => resolveReport(parseReportQuery(req.query), catalog)
|
|
3600
|
+
)
|
|
3601
|
+
));
|
|
2134
3602
|
api.get("/views", h(async (req) => ({
|
|
2135
3603
|
views: await resolveViews({
|
|
2136
3604
|
ViewModel,
|
|
2137
3605
|
registry: t.registry,
|
|
3606
|
+
catalog,
|
|
2138
3607
|
configured,
|
|
2139
3608
|
tenantId: req.viewer.tenantId,
|
|
2140
3609
|
viewerRef: req.viewer.viewerRef
|
|
@@ -2177,7 +3646,13 @@ function createDashboard(opts) {
|
|
|
2177
3646
|
indexCount: indexes.length,
|
|
2178
3647
|
indexBudget: INDEX_BUDGET,
|
|
2179
3648
|
keys,
|
|
2180
|
-
role: req.viewer.role
|
|
3649
|
+
role: req.viewer.role,
|
|
3650
|
+
// The same three sources, read the other way round: what the data says
|
|
3651
|
+
// the registry is missing, each with the line that would fix it. Derived
|
|
3652
|
+
// from the counters and the quarantine ALREADY fetched above, so the
|
|
3653
|
+
// page costs no extra read. Nothing is written — the host still edits
|
|
3654
|
+
// the registry by hand (reports §9).
|
|
3655
|
+
suggestions: deriveSuggestions({ counters: t.counters, catalog, quarantine })
|
|
2181
3656
|
};
|
|
2182
3657
|
}));
|
|
2183
3658
|
api.post("/system/keys/:id/revoke", h(async (req, res) => {
|
|
@@ -2266,7 +3741,22 @@ function createTelemetry(config) {
|
|
|
2266
3741
|
inFlight.add(p);
|
|
2267
3742
|
void p.finally(() => inFlight.delete(p));
|
|
2268
3743
|
};
|
|
2269
|
-
const
|
|
3744
|
+
const linkSubjects = createSubjectLinking({
|
|
3745
|
+
linker: config.subjectLinker,
|
|
3746
|
+
timeoutMs: config.subjectLinkTimeoutMs,
|
|
3747
|
+
counters,
|
|
3748
|
+
logger
|
|
3749
|
+
});
|
|
3750
|
+
const emit = createEmitter({
|
|
3751
|
+
registry,
|
|
3752
|
+
byKind,
|
|
3753
|
+
RollupModel,
|
|
3754
|
+
rejects,
|
|
3755
|
+
counters,
|
|
3756
|
+
logger,
|
|
3757
|
+
track,
|
|
3758
|
+
linkSubjects
|
|
3759
|
+
});
|
|
2270
3760
|
const forget = createForget({
|
|
2271
3761
|
TelemetryModel,
|
|
2272
3762
|
RollupModel,
|
|
@@ -2329,6 +3819,17 @@ function createTelemetry(config) {
|
|
|
2329
3819
|
counters,
|
|
2330
3820
|
/** the registry, exposed for the router factories — hosts should import their own */
|
|
2331
3821
|
registry,
|
|
3822
|
+
/**
|
|
3823
|
+
* Write-time subject linking, exposed for the router factories. `null` when
|
|
3824
|
+
* no `subjectLinker` is configured.
|
|
3825
|
+
*
|
|
3826
|
+
* The wire path does not go through emit() — createIngest() builds its
|
|
3827
|
+
* record itself, because at-least-once delivery inverts the plane order
|
|
3828
|
+
* (insert first, THEN aggregate). So it reaches the linker the same way it
|
|
3829
|
+
* reaches the registry and the models: off the instance, running the one
|
|
3830
|
+
* implementation, rather than growing a second copy of the rules.
|
|
3831
|
+
*/
|
|
3832
|
+
linkSubjects,
|
|
2332
3833
|
logger,
|
|
2333
3834
|
/** mint an ingest key; the full key string is returned once, never again */
|
|
2334
3835
|
createKey: (input) => createKey(KeyModel, input),
|
|
@@ -2345,6 +3846,6 @@ function createTelemetry(config) {
|
|
|
2345
3846
|
};
|
|
2346
3847
|
}
|
|
2347
3848
|
|
|
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 };
|
|
3849
|
+
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, SUBJECT_LINK_TIMEOUT_MS, SUBJECT_MAX, 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
3850
|
//# sourceMappingURL=index.js.map
|
|
2350
3851
|
//# sourceMappingURL=index.js.map
|