@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.cjs
CHANGED
|
@@ -68,8 +68,30 @@ var newCounters = () => ({
|
|
|
68
68
|
capped: 0,
|
|
69
69
|
rollupSkipped: 0,
|
|
70
70
|
deduped: 0,
|
|
71
|
-
truncated: 0
|
|
71
|
+
truncated: 0,
|
|
72
|
+
rollupSkippedBy: {},
|
|
73
|
+
undeclaredAttrs: {},
|
|
74
|
+
subjectsLinked: 0,
|
|
75
|
+
subjectLinkMisses: 0,
|
|
76
|
+
subjectLinkErrors: 0,
|
|
77
|
+
subjectLinkTimeouts: 0,
|
|
78
|
+
subjectLinkUndeclared: 0,
|
|
79
|
+
subjectLinkCapped: 0
|
|
72
80
|
});
|
|
81
|
+
var COUNTER_MAP_MAX = 1e3;
|
|
82
|
+
var COUNTER_OVERFLOW_KEY = "(other)|(other)";
|
|
83
|
+
var bumpCounterMap = (map, key) => {
|
|
84
|
+
const seen = map[key];
|
|
85
|
+
if (seen !== void 0) {
|
|
86
|
+
map[key] = seen + 1;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (map[COUNTER_OVERFLOW_KEY] !== void 0 || Object.keys(map).length >= COUNTER_MAP_MAX) {
|
|
90
|
+
map[COUNTER_OVERFLOW_KEY] = (map[COUNTER_OVERFLOW_KEY] ?? 0) + 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
map[key] = 1;
|
|
94
|
+
};
|
|
73
95
|
var traceKeep = (traceId, rate) => {
|
|
74
96
|
if (rate >= 1) return true;
|
|
75
97
|
if (!traceId) return Math.random() < rate;
|
|
@@ -397,15 +419,15 @@ function buildBaseSchema(collection, registry, counters, opts) {
|
|
|
397
419
|
if (this.kind === TelemetryKind.State && !this.state?.to) {
|
|
398
420
|
throw new Error("telemetry: state requires state.to");
|
|
399
421
|
}
|
|
400
|
-
const check = (
|
|
422
|
+
const check = (label3, m, zschema) => {
|
|
401
423
|
const obj = Object.fromEntries(m ?? []);
|
|
402
424
|
if (!zschema) {
|
|
403
|
-
if (Object.keys(obj).length) throw new Error(`telemetry: "${this.name}" declares no ${
|
|
425
|
+
if (Object.keys(obj).length) throw new Error(`telemetry: "${this.name}" declares no ${label3}`);
|
|
404
426
|
return;
|
|
405
427
|
}
|
|
406
428
|
const s = zschema.strict?.() ?? zschema;
|
|
407
429
|
const r = s.safeParse(obj);
|
|
408
|
-
if (!r.success) throw new Error(`telemetry: ${
|
|
430
|
+
if (!r.success) throw new Error(`telemetry: ${label3} invalid for "${this.name}": ${r.error.message}`);
|
|
409
431
|
};
|
|
410
432
|
check("attrs", this.attrs, spec.attrs);
|
|
411
433
|
check("metrics", this.metrics, spec.metrics);
|
|
@@ -554,6 +576,7 @@ async function recordRollup(RollupModel, doc, name, spec, counters) {
|
|
|
554
576
|
if (v == null || v === "") {
|
|
555
577
|
if (spec.dimDefault === void 0) {
|
|
556
578
|
counters.rollupSkipped++;
|
|
579
|
+
bumpCounterMap(counters.rollupSkippedBy, `${as}|${label(src)}`);
|
|
557
580
|
return;
|
|
558
581
|
}
|
|
559
582
|
v = spec.dimDefault;
|
|
@@ -645,6 +668,120 @@ function createCheckpointFactory(CheckpointModel, logger) {
|
|
|
645
668
|
}
|
|
646
669
|
|
|
647
670
|
// src/server/emit.ts
|
|
671
|
+
function noteUndeclaredAttrs(counters, name, spec, attrs) {
|
|
672
|
+
if (!attrs || typeof attrs !== "object") return;
|
|
673
|
+
const keys = attrs instanceof Map ? [...attrs.keys()] : Object.keys(attrs);
|
|
674
|
+
const shape = spec.attrs?.shape;
|
|
675
|
+
for (const raw of keys) {
|
|
676
|
+
const key = String(raw).replace(/\./g, "_");
|
|
677
|
+
if (shape && Object.prototype.hasOwnProperty.call(shape, key)) continue;
|
|
678
|
+
bumpCounterMap(counters.undeclaredAttrs, `${name}|${key}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
var SUBJECT_MAX = 8;
|
|
682
|
+
var SUBJECT_LINK_TIMEOUT_MS = 50;
|
|
683
|
+
var LINK_TIMEOUT = /* @__PURE__ */ Symbol("telemetry.subjectLink.timeout");
|
|
684
|
+
function createSubjectLinking(opts) {
|
|
685
|
+
const { linker, counters, logger } = opts;
|
|
686
|
+
if (!linker) return null;
|
|
687
|
+
const timeoutMs = opts.timeoutMs ?? SUBJECT_LINK_TIMEOUT_MS;
|
|
688
|
+
const warned = /* @__PURE__ */ new Set();
|
|
689
|
+
const warnOnce = (key, msg) => {
|
|
690
|
+
if (warned.has(key) || warned.size >= COUNTER_MAP_MAX) return;
|
|
691
|
+
warned.add(key);
|
|
692
|
+
logger.warn(msg);
|
|
693
|
+
};
|
|
694
|
+
return async function linkSubjects(name, spec, tenantId, declared) {
|
|
695
|
+
const have = Array.isArray(declared) ? declared : [];
|
|
696
|
+
const seen = /* @__PURE__ */ new Set();
|
|
697
|
+
const view = [];
|
|
698
|
+
for (const s of have) {
|
|
699
|
+
const ref = wellFormed(s);
|
|
700
|
+
if (!ref) continue;
|
|
701
|
+
seen.add(`${ref.type}:${ref.id}`);
|
|
702
|
+
view.push(ref);
|
|
703
|
+
}
|
|
704
|
+
let out;
|
|
705
|
+
let timer;
|
|
706
|
+
try {
|
|
707
|
+
out = await Promise.race([
|
|
708
|
+
// the async wrapper turns a SYNCHRONOUS throw into a rejection, so a
|
|
709
|
+
// linker that dies on its first line lands in the same catch as one
|
|
710
|
+
// whose promise rejects
|
|
711
|
+
(async () => linker.link(view, { name, tenantId }))(),
|
|
712
|
+
new Promise((_, reject) => {
|
|
713
|
+
timer = setTimeout(() => reject(LINK_TIMEOUT), timeoutMs);
|
|
714
|
+
})
|
|
715
|
+
]);
|
|
716
|
+
} catch (e) {
|
|
717
|
+
if (e === LINK_TIMEOUT) {
|
|
718
|
+
counters.subjectLinkTimeouts++;
|
|
719
|
+
warnOnce(
|
|
720
|
+
"timeout",
|
|
721
|
+
`[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.`
|
|
722
|
+
);
|
|
723
|
+
} else {
|
|
724
|
+
counters.subjectLinkErrors++;
|
|
725
|
+
warnOnce(
|
|
726
|
+
"threw",
|
|
727
|
+
`[telemetry] subjectLinker.link() threw \u2014 records are being written unlinked: ${e}. Warned once \u2014 the count is counters.subjectLinkErrors.`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
return null;
|
|
731
|
+
} finally {
|
|
732
|
+
clearTimeout(timer);
|
|
733
|
+
}
|
|
734
|
+
if (!Array.isArray(out)) {
|
|
735
|
+
counters.subjectLinkErrors++;
|
|
736
|
+
warnOnce(
|
|
737
|
+
"shape",
|
|
738
|
+
`[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.`
|
|
739
|
+
);
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
if (!out.length) {
|
|
743
|
+
counters.subjectLinkMisses++;
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
let room = Math.max(0, SUBJECT_MAX - have.length);
|
|
747
|
+
let capped2 = 0;
|
|
748
|
+
const add = [];
|
|
749
|
+
for (const s of out) {
|
|
750
|
+
const ref = wellFormed(s);
|
|
751
|
+
if (!ref) {
|
|
752
|
+
counters.subjectLinkErrors++;
|
|
753
|
+
warnOnce(
|
|
754
|
+
"entry",
|
|
755
|
+
"[telemetry] subjectLinker returned an entry that is not { type, id } \u2014 dropped. Warned once \u2014 the count is counters.subjectLinkErrors."
|
|
756
|
+
);
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const key = `${ref.type}:${ref.id}`;
|
|
760
|
+
if (seen.has(key)) continue;
|
|
761
|
+
if (!spec.subjects.includes(ref.type)) {
|
|
762
|
+
counters.subjectLinkUndeclared++;
|
|
763
|
+
}
|
|
764
|
+
if (room <= 0) {
|
|
765
|
+
capped2++;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
seen.add(key);
|
|
769
|
+
room--;
|
|
770
|
+
add.push(ref);
|
|
771
|
+
}
|
|
772
|
+
counters.subjectLinkCapped += capped2;
|
|
773
|
+
if (!add.length) return null;
|
|
774
|
+
counters.subjectsLinked += add.length;
|
|
775
|
+
return [...have, ...add];
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
function wellFormed(s) {
|
|
779
|
+
if (!s || typeof s !== "object") return null;
|
|
780
|
+
const { type, id, role } = s;
|
|
781
|
+
if (typeof type !== "string" || !type) return null;
|
|
782
|
+
if (typeof id !== "string" || !id) return null;
|
|
783
|
+
return typeof role === "string" && role ? { type, id, role } : { type, id };
|
|
784
|
+
}
|
|
648
785
|
function createEmitter(ctx) {
|
|
649
786
|
const { registry, byKind, RollupModel, rejects, counters } = ctx;
|
|
650
787
|
const burstBuckets = /* @__PURE__ */ new Map();
|
|
@@ -681,16 +818,21 @@ function createEmitter(ctx) {
|
|
|
681
818
|
const durable = kind === TelemetryKind.Usage || (doc.durable ?? spec.durable ?? false);
|
|
682
819
|
const Model = byKind[kind];
|
|
683
820
|
const { forceKeep: _drop, durable: _durable, ...rest } = doc;
|
|
821
|
+
const linked = ctx.linkSubjects ? await ctx.linkSubjects(name, spec, doc.tenantId, doc.subjects) : null;
|
|
684
822
|
const safe = (o) => new Map(Object.entries(o ?? {}).map(([k, v]) => [k.replace(/\./g, "_"), v]));
|
|
685
823
|
const payload = {
|
|
686
824
|
...rest,
|
|
687
825
|
_id: id,
|
|
688
826
|
name,
|
|
827
|
+
// computed like everything below it, and absent when nothing linked, so a
|
|
828
|
+
// host with no linker hands the model the exact object 0.4.0 did
|
|
829
|
+
...linked ? { subjects: linked } : {},
|
|
689
830
|
sampleRate: forced ? 1 : baseRate,
|
|
690
831
|
forced,
|
|
691
832
|
attrs: safe(doc.attrs),
|
|
692
833
|
metrics: safe(doc.metrics)
|
|
693
834
|
};
|
|
835
|
+
noteUndeclaredAttrs(counters, name, spec, doc.attrs);
|
|
694
836
|
const onFail = async (e) => {
|
|
695
837
|
counters.rejected++;
|
|
696
838
|
await rejects().insertOne({ at: /* @__PURE__ */ new Date(), name, reason: String(e), raw: plain(doc) }).catch(() => {
|
|
@@ -896,10 +1038,10 @@ function parseKeyString(raw) {
|
|
|
896
1038
|
if (!raw) return null;
|
|
897
1039
|
const m = KEY_RE.exec(raw.trim());
|
|
898
1040
|
if (!m) return null;
|
|
899
|
-
const [, prefix,
|
|
1041
|
+
const [, prefix, label3, id, secret] = m;
|
|
900
1042
|
if (prefix === "sk" && !secret) return null;
|
|
901
1043
|
if (prefix === "pk" && secret) return null;
|
|
902
|
-
return { kind: prefix === "pk" ? KeyKind.Publishable : KeyKind.Secret, label:
|
|
1044
|
+
return { kind: prefix === "pk" ? KeyKind.Publishable : KeyKind.Secret, label: label3, id, secret };
|
|
903
1045
|
}
|
|
904
1046
|
var SCRYPT_VERSION = "scrypt1";
|
|
905
1047
|
var SCRYPT = { N: 16384, r: 8, p: 1, keylen: 32 };
|
|
@@ -957,7 +1099,7 @@ async function createKey(KeyModel, input) {
|
|
|
957
1099
|
tenantId,
|
|
958
1100
|
service,
|
|
959
1101
|
env,
|
|
960
|
-
label:
|
|
1102
|
+
label: label3 = "live",
|
|
961
1103
|
origins = [],
|
|
962
1104
|
allowedNames,
|
|
963
1105
|
maxPerMinute = 600
|
|
@@ -990,7 +1132,7 @@ async function createKey(KeyModel, input) {
|
|
|
990
1132
|
createdAt: /* @__PURE__ */ new Date()
|
|
991
1133
|
});
|
|
992
1134
|
const prefix = kind === KeyKind.Publishable ? "pk" : "sk";
|
|
993
|
-
return { key: secret ? `${prefix}_${
|
|
1135
|
+
return { key: secret ? `${prefix}_${label3}_${id}_${secret}` : `${prefix}_${label3}_${id}`, id };
|
|
994
1136
|
}
|
|
995
1137
|
var BATCH_MAX = 100;
|
|
996
1138
|
function createIngest(opts) {
|
|
@@ -1192,6 +1334,9 @@ function createIngest(opts) {
|
|
|
1192
1334
|
const occurredRaw = rec.occurredAt ? Date.parse(rec.occurredAt) : NaN;
|
|
1193
1335
|
const occurredAt = Number.isFinite(occurredRaw) ? new Date(occurredRaw - clockSkewMs) : receivedAt;
|
|
1194
1336
|
const safeMap = (o) => o && typeof o === "object" ? new Map(Object.entries(o).map(([k, v]) => [k.replace(/\./g, "_"), v])) : /* @__PURE__ */ new Map();
|
|
1337
|
+
noteUndeclaredAttrs(t.counters, name, spec, rec.attrs);
|
|
1338
|
+
const subjects = mergeSubjects(rec.subjects);
|
|
1339
|
+
const linked = t.linkSubjects ? await t.linkSubjects(name, spec, tenantId, subjects) : null;
|
|
1195
1340
|
const Model = t.models.byKind[spec.kind];
|
|
1196
1341
|
const d = new Model({
|
|
1197
1342
|
// facts the wire may not assert: tenant, service, env, origin, plane
|
|
@@ -1202,7 +1347,7 @@ function createIngest(opts) {
|
|
|
1202
1347
|
tenantId,
|
|
1203
1348
|
occurredAt,
|
|
1204
1349
|
severity: typeof rec.severity === "string" ? rec.severity : void 0,
|
|
1205
|
-
subjects:
|
|
1350
|
+
subjects: linked ?? subjects,
|
|
1206
1351
|
actor: ctx.actor ?? (typeof rec.actor === "string" ? rec.actor : batchActor),
|
|
1207
1352
|
onBehalfOf: typeof rec.onBehalfOf === "string" ? rec.onBehalfOf : void 0,
|
|
1208
1353
|
service: key.service,
|
|
@@ -1262,6 +1407,292 @@ function createIngest(opts) {
|
|
|
1262
1407
|
return router;
|
|
1263
1408
|
}
|
|
1264
1409
|
|
|
1410
|
+
// src/server/catalog.ts
|
|
1411
|
+
var label2 = (src) => src.slice(src.indexOf(":") + 1);
|
|
1412
|
+
var LEAF_TYPES = {
|
|
1413
|
+
string: "string",
|
|
1414
|
+
number: "number",
|
|
1415
|
+
int: "number",
|
|
1416
|
+
bigint: "number",
|
|
1417
|
+
boolean: "boolean",
|
|
1418
|
+
date: "date"
|
|
1419
|
+
};
|
|
1420
|
+
function walkAttr(schema) {
|
|
1421
|
+
let node = schema;
|
|
1422
|
+
let optional = false;
|
|
1423
|
+
for (let depth = 0; node && depth < 20; depth++) {
|
|
1424
|
+
const def = node._zod?.def ?? node.def;
|
|
1425
|
+
if (!def?.type) break;
|
|
1426
|
+
switch (def.type) {
|
|
1427
|
+
// these three all mean "the value may be absent from a stored record",
|
|
1428
|
+
// which is the only thing `optional` claims
|
|
1429
|
+
case "optional":
|
|
1430
|
+
case "nullable":
|
|
1431
|
+
case "default":
|
|
1432
|
+
optional = true;
|
|
1433
|
+
node = def.innerType;
|
|
1434
|
+
continue;
|
|
1435
|
+
case "catch":
|
|
1436
|
+
case "readonly":
|
|
1437
|
+
node = def.innerType;
|
|
1438
|
+
continue;
|
|
1439
|
+
// a pipe is `in -> out`; the INPUT side is what a caller may send and so
|
|
1440
|
+
// what a stored value was validated as. The output of a transform is
|
|
1441
|
+
// frequently a shape no filter could ever be written against.
|
|
1442
|
+
case "pipe":
|
|
1443
|
+
node = def.in;
|
|
1444
|
+
continue;
|
|
1445
|
+
case "enum": {
|
|
1446
|
+
const options = Array.isArray(node.options) ? node.options : Object.values(def.entries ?? {});
|
|
1447
|
+
return { type: "enum", values: options.map(String), optional };
|
|
1448
|
+
}
|
|
1449
|
+
case "literal":
|
|
1450
|
+
return { type: "enum", values: [...def.values ?? []].map(String), optional };
|
|
1451
|
+
default:
|
|
1452
|
+
return { type: LEAF_TYPES[def.type] ?? "string", optional };
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return { type: "string", optional };
|
|
1456
|
+
}
|
|
1457
|
+
var dim = (key, type, o = {}) => ({
|
|
1458
|
+
key,
|
|
1459
|
+
// the two pseudo-dims are derived at query time from subjectKeys / actor, so
|
|
1460
|
+
// they carry no `field:` prefix and label to themselves
|
|
1461
|
+
label: key.startsWith("field:") ? key.slice(6) : key,
|
|
1462
|
+
type,
|
|
1463
|
+
...o.values ? { values: [...o.values] } : {},
|
|
1464
|
+
optional: o.optional ?? false,
|
|
1465
|
+
indexed: o.indexed ?? false
|
|
1466
|
+
});
|
|
1467
|
+
var envelopeDims = (platforms) => [
|
|
1468
|
+
dim("field:kind", "enum", { values: TELEMETRY_KINDS, indexed: true }),
|
|
1469
|
+
dim("field:name", "string", { indexed: true }),
|
|
1470
|
+
dim("field:severity", "enum", { values: Object.values(LogLevel) }),
|
|
1471
|
+
dim("field:env", "enum", { values: Object.values(Env) }),
|
|
1472
|
+
dim("field:service", "string"),
|
|
1473
|
+
dim("field:release", "string"),
|
|
1474
|
+
dim("field:origin", "enum", { values: Object.values(Origin) }),
|
|
1475
|
+
// client context is absent on server-origin records, so both of its dims are optional
|
|
1476
|
+
dim("field:client.platform", "enum", { values: platforms, optional: true }),
|
|
1477
|
+
dim("field:client.appVersion", "string", { optional: true }),
|
|
1478
|
+
dim("subjectType", "string", { optional: true, indexed: true }),
|
|
1479
|
+
dim("actorType", "string", { optional: true })
|
|
1480
|
+
];
|
|
1481
|
+
var kindDims = (kind) => {
|
|
1482
|
+
switch (kind) {
|
|
1483
|
+
case TelemetryKind.Usage:
|
|
1484
|
+
return [
|
|
1485
|
+
dim("field:usage.meter", "string", { indexed: true }),
|
|
1486
|
+
dim("field:usage.billedTo", "string"),
|
|
1487
|
+
dim("field:usage.unit", "string")
|
|
1488
|
+
];
|
|
1489
|
+
case TelemetryKind.State:
|
|
1490
|
+
return [
|
|
1491
|
+
dim("field:state.key", "string", { indexed: true }),
|
|
1492
|
+
dim("field:state.to", "string", { indexed: true })
|
|
1493
|
+
];
|
|
1494
|
+
case TelemetryKind.Error:
|
|
1495
|
+
return [dim("field:error.type", "string"), dim("field:error.handled", "boolean")];
|
|
1496
|
+
default:
|
|
1497
|
+
return [];
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
var RAW_OPS = ["avg", "p50", "p95", "p99"];
|
|
1501
|
+
function deriveCatalog(registry, opts = {}) {
|
|
1502
|
+
const platforms = [.../* @__PURE__ */ new Set([...BUILTIN_PLATFORMS, ...opts.platforms ?? []])];
|
|
1503
|
+
const families = {};
|
|
1504
|
+
for (const [name, spec] of Object.entries(registry)) {
|
|
1505
|
+
for (const r of spec.rollups ?? []) {
|
|
1506
|
+
const as = r.as ?? name;
|
|
1507
|
+
const seen = families[as];
|
|
1508
|
+
if (!seen) {
|
|
1509
|
+
families[as] = {
|
|
1510
|
+
as,
|
|
1511
|
+
by: [...r.by],
|
|
1512
|
+
labels: r.by.map(label2),
|
|
1513
|
+
bucket: r.bucket ?? null,
|
|
1514
|
+
lifetime: !r.bucket,
|
|
1515
|
+
// `subjects` only means anything when there is a subject dim to
|
|
1516
|
+
// restrict; without one it selects nothing and claiming it would
|
|
1517
|
+
// offer a subject filter the family cannot answer
|
|
1518
|
+
subjectTypes: r.by.includes("subject") ? [...r.subjects ?? []] : [],
|
|
1519
|
+
sums: [...r.sum ?? []],
|
|
1520
|
+
capture: (r.capture ?? []).map(label2),
|
|
1521
|
+
feeders: [name],
|
|
1522
|
+
retentionDays: r.retentionDays ?? null
|
|
1523
|
+
};
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
for (const k of r.sum ?? []) if (!seen.sums.includes(k)) seen.sums.push(k);
|
|
1527
|
+
for (const c of r.capture ?? []) {
|
|
1528
|
+
const l = label2(c);
|
|
1529
|
+
if (!seen.capture.includes(l)) seen.capture.push(l);
|
|
1530
|
+
}
|
|
1531
|
+
if (!seen.feeders.includes(name)) seen.feeders.push(name);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
const events = {};
|
|
1535
|
+
const namespaces = {};
|
|
1536
|
+
const subjectTypes = [];
|
|
1537
|
+
const noteSubject = (t) => {
|
|
1538
|
+
if (!subjectTypes.includes(t)) subjectTypes.push(t);
|
|
1539
|
+
};
|
|
1540
|
+
for (const [name, spec] of Object.entries(registry)) {
|
|
1541
|
+
const dot = name.indexOf(".");
|
|
1542
|
+
const namespace = dot === -1 ? name : name.slice(0, dot);
|
|
1543
|
+
(namespaces[namespace] ??= []).push(name);
|
|
1544
|
+
for (const s of spec.subjects) noteSubject(s);
|
|
1545
|
+
const indexedAttrs = [...spec.indexedAttrs ?? []];
|
|
1546
|
+
const dims = Object.entries(spec.attrs?.shape ?? {}).map(
|
|
1547
|
+
([key, schema]) => {
|
|
1548
|
+
const walked = walkAttr(schema);
|
|
1549
|
+
return {
|
|
1550
|
+
key: `attr:${key}`,
|
|
1551
|
+
label: key,
|
|
1552
|
+
type: walked.type,
|
|
1553
|
+
...walked.values ? { values: walked.values } : {},
|
|
1554
|
+
optional: walked.optional,
|
|
1555
|
+
indexed: indexedAttrs.includes(key)
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
);
|
|
1559
|
+
dims.push(...kindDims(spec.kind));
|
|
1560
|
+
const eventFamilies = [];
|
|
1561
|
+
const ownSums = /* @__PURE__ */ new Map();
|
|
1562
|
+
for (const r of spec.rollups ?? []) {
|
|
1563
|
+
const as = r.as ?? name;
|
|
1564
|
+
if (!eventFamilies.includes(as)) eventFamilies.push(as);
|
|
1565
|
+
const set = ownSums.get(as) ?? /* @__PURE__ */ new Set();
|
|
1566
|
+
for (const k of r.sum ?? []) set.add(k);
|
|
1567
|
+
ownSums.set(as, set);
|
|
1568
|
+
for (const s of r.subjects ?? []) noteSubject(s);
|
|
1569
|
+
}
|
|
1570
|
+
const measures = [{ key: "count", exactVia: [] }];
|
|
1571
|
+
for (const k of Object.keys(spec.metrics?.shape ?? {})) {
|
|
1572
|
+
measures.push({
|
|
1573
|
+
key: `sum:${k}`,
|
|
1574
|
+
metric: k,
|
|
1575
|
+
exactVia: eventFamilies.filter((as) => ownSums.get(as)?.has(k))
|
|
1576
|
+
});
|
|
1577
|
+
for (const op of RAW_OPS) measures.push({ key: `${op}:${k}`, metric: k, exactVia: [] });
|
|
1578
|
+
}
|
|
1579
|
+
if (spec.kind === TelemetryKind.Span) {
|
|
1580
|
+
for (const op of RAW_OPS) {
|
|
1581
|
+
measures.push({ key: `${op}:durationMs`, metric: "durationMs", exactVia: [] });
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
events[name] = {
|
|
1585
|
+
kind: spec.kind,
|
|
1586
|
+
origin: spec.origin,
|
|
1587
|
+
subjects: [...spec.subjects],
|
|
1588
|
+
description: spec.description,
|
|
1589
|
+
namespace,
|
|
1590
|
+
dims,
|
|
1591
|
+
measures,
|
|
1592
|
+
families: eventFamilies,
|
|
1593
|
+
indexedAttrs,
|
|
1594
|
+
indexedMetrics: [...spec.indexedMetrics ?? []],
|
|
1595
|
+
// `hasOwnProperty` rather than `??`, exactly as model.ts stamps expiresAt:
|
|
1596
|
+
// an explicit `retentionDays: null` means immortal and must not fall
|
|
1597
|
+
// through to the per-kind default
|
|
1598
|
+
retentionDays: Object.prototype.hasOwnProperty.call(spec, "retentionDays") ? spec.retentionDays ?? null : RETENTION_DAYS[spec.kind]
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
return { events, families, namespaces, envelope: envelopeDims(platforms), subjectTypes };
|
|
1602
|
+
}
|
|
1603
|
+
function projectRegistry(catalog) {
|
|
1604
|
+
return Object.fromEntries(
|
|
1605
|
+
Object.entries(catalog.events).map(([name, e]) => [
|
|
1606
|
+
name,
|
|
1607
|
+
{
|
|
1608
|
+
kind: e.kind,
|
|
1609
|
+
origin: e.origin,
|
|
1610
|
+
subjects: e.subjects,
|
|
1611
|
+
description: e.description,
|
|
1612
|
+
attrKeys: e.dims.filter((d) => d.key.startsWith("attr:")).map((d) => d.label),
|
|
1613
|
+
// every metric key gets exactly one `sum:` measure and nothing else does
|
|
1614
|
+
metricKeys: e.measures.filter((m) => m.key.startsWith("sum:")).map((m) => m.metric),
|
|
1615
|
+
indexedAttrs: e.indexedAttrs,
|
|
1616
|
+
indexedMetrics: e.indexedMetrics,
|
|
1617
|
+
rollups: e.families.map((as) => {
|
|
1618
|
+
const f = catalog.families[as];
|
|
1619
|
+
return { as: f.as, by: f.by, bucket: f.bucket, sum: f.sums, subjects: f.subjectTypes };
|
|
1620
|
+
})
|
|
1621
|
+
}
|
|
1622
|
+
])
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/server/suggest.ts
|
|
1627
|
+
var UNREGISTERED_REASON = "unregistered event";
|
|
1628
|
+
var MAX_SUGGESTIONS = 50;
|
|
1629
|
+
var NAME_MAX = 120;
|
|
1630
|
+
var quote = (s) => /^[A-Za-z0-9_.:$-]+$/.test(s) ? `'${s}'` : JSON.stringify(s);
|
|
1631
|
+
var prop = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s) ? s : quote(s);
|
|
1632
|
+
var times = (n) => `${n} time${n === 1 ? "" : "s"}`;
|
|
1633
|
+
var split = (k) => {
|
|
1634
|
+
const i = k.indexOf("|");
|
|
1635
|
+
return i === -1 ? [k, ""] : [k.slice(0, i), k.slice(i + 1)];
|
|
1636
|
+
};
|
|
1637
|
+
function deriveSuggestions(input) {
|
|
1638
|
+
const { counters, catalog, quarantine = [] } = input;
|
|
1639
|
+
const out = [];
|
|
1640
|
+
for (const [k, count2] of Object.entries(counters.undeclaredAttrs ?? {})) {
|
|
1641
|
+
if (k === COUNTER_OVERFLOW_KEY || !count2) continue;
|
|
1642
|
+
const [name, key] = split(k);
|
|
1643
|
+
if (!name || !key) continue;
|
|
1644
|
+
const facet = catalog.events[name];
|
|
1645
|
+
const line = `${prop(key)}: z.string().max(64),`;
|
|
1646
|
+
const hasAttrs = !!facet?.dims.some((d) => d.key.startsWith("attr:"));
|
|
1647
|
+
out.push({
|
|
1648
|
+
kind: "undeclared_attr",
|
|
1649
|
+
target: name,
|
|
1650
|
+
key,
|
|
1651
|
+
count: count2,
|
|
1652
|
+
message: `\`${name}\` has been sent with attr \`${key}\` ${times(count2)} \u2014 not declared`,
|
|
1653
|
+
fix: hasAttrs ? line : `attrs: z.object({ ${prop(key)}: z.string().max(64) }),`
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
for (const [k, count2] of Object.entries(counters.rollupSkippedBy ?? {})) {
|
|
1657
|
+
if (k === COUNTER_OVERFLOW_KEY || !count2) continue;
|
|
1658
|
+
const [as, dim2] = split(k);
|
|
1659
|
+
if (!as || !dim2) continue;
|
|
1660
|
+
const feeders = catalog.families[as]?.feeders ?? [];
|
|
1661
|
+
const where = feeders.length ? `// on the \`${as}\` rollup of ${feeders.map((f) => `\`${f}\``).join(", ")}
|
|
1662
|
+
` : "";
|
|
1663
|
+
out.push({
|
|
1664
|
+
kind: "missing_dim_default",
|
|
1665
|
+
target: as,
|
|
1666
|
+
key: dim2,
|
|
1667
|
+
count: count2,
|
|
1668
|
+
message: `\`${as}\` skipped ${count2} record${count2 === 1 ? "" : "s"} with no \`${dim2}\` \u2014 declare \`dimDefault\``,
|
|
1669
|
+
fix: `${where}dimDefault: 'unknown',`
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
const unregistered = /* @__PURE__ */ new Map();
|
|
1673
|
+
for (const row of quarantine) {
|
|
1674
|
+
if (typeof row?.reason !== "string" || !row.reason.includes(UNREGISTERED_REASON)) continue;
|
|
1675
|
+
const name = typeof row.name === "string" ? row.name.slice(0, NAME_MAX) : "";
|
|
1676
|
+
if (!name || name === "(unnamed)") continue;
|
|
1677
|
+
unregistered.set(name, (unregistered.get(name) ?? 0) + 1);
|
|
1678
|
+
}
|
|
1679
|
+
for (const [name, count2] of unregistered) {
|
|
1680
|
+
out.push({
|
|
1681
|
+
kind: "unregistered_event",
|
|
1682
|
+
target: name,
|
|
1683
|
+
count: count2,
|
|
1684
|
+
message: `\`${name}\` was rejected ${times(count2)} \u2014 not in the registry`,
|
|
1685
|
+
// the minimum that boots: validateRegistry wants a kind, an origin, and
|
|
1686
|
+
// a subjects array, and nothing here can guess the rest
|
|
1687
|
+
fix: `${quote(name)}: { kind: 'event', origin: 'client', subjects: [], description: '' },`
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
out.sort(
|
|
1691
|
+
(a, b) => b.count - a.count || a.target.localeCompare(b.target) || (a.key ?? "").localeCompare(b.key ?? "")
|
|
1692
|
+
);
|
|
1693
|
+
return out.slice(0, MAX_SUGGESTIONS);
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1265
1696
|
// src/server/funnel.ts
|
|
1266
1697
|
var DAY_MS = 864e5;
|
|
1267
1698
|
function median(values) {
|
|
@@ -1469,6 +1900,10 @@ var DEFAULT_LIMITS = {
|
|
|
1469
1900
|
rollups: 500,
|
|
1470
1901
|
trace: 500,
|
|
1471
1902
|
journey: 500,
|
|
1903
|
+
breakdown: 50,
|
|
1904
|
+
// top groups — a starting point, to be measured on real hosts
|
|
1905
|
+
values: 200,
|
|
1906
|
+
// top values of one dimension — a picker, not a table
|
|
1472
1907
|
distribution: 1e5,
|
|
1473
1908
|
distinct: 1e5,
|
|
1474
1909
|
funnel: 5e3
|
|
@@ -1482,7 +1917,10 @@ function buildMatch(scope, range, f) {
|
|
|
1482
1917
|
occurredAt: { $gte: range.from, $lt: range.to }
|
|
1483
1918
|
};
|
|
1484
1919
|
for (const k of ["kind", "name", "severity", "env", "service", "release", "traceId"]) {
|
|
1485
|
-
|
|
1920
|
+
const v = f[k];
|
|
1921
|
+
if (Array.isArray(v)) {
|
|
1922
|
+
if (v.length) match[k] = { $in: v };
|
|
1923
|
+
} else if (v) match[k] = v;
|
|
1486
1924
|
}
|
|
1487
1925
|
if (f.subject) match.subjectKeys = f.subject;
|
|
1488
1926
|
for (const [k, v] of Object.entries(f.attrs ?? {})) match[`attrs.${k}`] = v;
|
|
@@ -1505,6 +1943,68 @@ function buildMatch(scope, range, f) {
|
|
|
1505
1943
|
}
|
|
1506
1944
|
return match;
|
|
1507
1945
|
}
|
|
1946
|
+
var INTERVALS = ["hour", "day", "week", "month"];
|
|
1947
|
+
var truncTo = (path3, unit) => ({
|
|
1948
|
+
$dateTrunc: { date: path3, unit, ...unit === "week" ? { startOfWeek: "monday" } : {} }
|
|
1949
|
+
});
|
|
1950
|
+
function measureAccumulator(measure) {
|
|
1951
|
+
const m = /^(sum|avg):(.+)$/.exec(measure);
|
|
1952
|
+
if (!m) return { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } };
|
|
1953
|
+
const path3 = m[2] === "durationMs" ? "$durationMs" : `$metrics.${m[2]}`;
|
|
1954
|
+
return m[1] === "sum" ? { $sum: path3 } : { $avg: path3 };
|
|
1955
|
+
}
|
|
1956
|
+
var badRequest = (message) => Object.assign(new Error(`telemetry: breakdown() \u2014 ${message}`), { status: 400 });
|
|
1957
|
+
var BREAKDOWN_FIELDS = [
|
|
1958
|
+
"kind",
|
|
1959
|
+
"name",
|
|
1960
|
+
"severity",
|
|
1961
|
+
"env",
|
|
1962
|
+
"service",
|
|
1963
|
+
"release",
|
|
1964
|
+
"origin",
|
|
1965
|
+
"client.platform",
|
|
1966
|
+
"client.appVersion",
|
|
1967
|
+
"usage.meter",
|
|
1968
|
+
"usage.billedTo",
|
|
1969
|
+
"usage.unit",
|
|
1970
|
+
"state.key",
|
|
1971
|
+
"state.to",
|
|
1972
|
+
"error.type",
|
|
1973
|
+
"error.handled"
|
|
1974
|
+
];
|
|
1975
|
+
var typePrefix = (ref) => ({
|
|
1976
|
+
$let: {
|
|
1977
|
+
vars: { ref },
|
|
1978
|
+
in: {
|
|
1979
|
+
$cond: [
|
|
1980
|
+
{ $eq: [{ $type: "$$ref" }, "string"] },
|
|
1981
|
+
{ $arrayElemAt: [{ $split: ["$$ref", ":"] }, 0] },
|
|
1982
|
+
null
|
|
1983
|
+
]
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
function dimExpression(dim2) {
|
|
1988
|
+
if (dim2.startsWith("attr:")) {
|
|
1989
|
+
const key = dim2.slice(5);
|
|
1990
|
+
if (!key) throw badRequest('`attr:` needs a key, e.g. "attr:plan"');
|
|
1991
|
+
return { $ifNull: [`$attrs.${key}`, null] };
|
|
1992
|
+
}
|
|
1993
|
+
if (dim2.startsWith("field:")) {
|
|
1994
|
+
const path3 = dim2.slice(6);
|
|
1995
|
+
if (!BREAKDOWN_FIELDS.includes(path3)) {
|
|
1996
|
+
throw badRequest(
|
|
1997
|
+
`"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.`
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
return { $ifNull: [`$${path3}`, null] };
|
|
2001
|
+
}
|
|
2002
|
+
if (dim2 === "subjectType") return typePrefix({ $arrayElemAt: ["$subjectKeys", 0] });
|
|
2003
|
+
if (dim2 === "actorType") return typePrefix("$actor");
|
|
2004
|
+
throw badRequest(
|
|
2005
|
+
`"${dim2}" is not a dimension. Use "attr:<key>", "field:<path>", "subjectType" or "actorType".`
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
1508
2008
|
var QueryCache = class {
|
|
1509
2009
|
constructor(ttlMs, cap) {
|
|
1510
2010
|
this.ttlMs = ttlMs;
|
|
@@ -1578,16 +2078,9 @@ function createQueries(ctx) {
|
|
|
1578
2078
|
return cache.get(
|
|
1579
2079
|
key,
|
|
1580
2080
|
() => timed("series", { scope, filter, measure, interval }, async () => {
|
|
1581
|
-
const m = /^(sum|avg):(.+)$/.exec(measure);
|
|
1582
|
-
const value = !m ? { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } } : m[1] === "sum" ? { $sum: `$metrics.${m[2]}` } : { $avg: `$metrics.${m[2]}` };
|
|
1583
2081
|
const buckets = await ctx.TelemetryModel.aggregate([
|
|
1584
2082
|
{ $match: buildMatch(scope, range, filter) },
|
|
1585
|
-
{
|
|
1586
|
-
$group: {
|
|
1587
|
-
_id: { $dateTrunc: { date: "$occurredAt", unit: interval, ...interval === "week" ? { startOfWeek: "monday" } : {} } },
|
|
1588
|
-
value
|
|
1589
|
-
}
|
|
1590
|
-
},
|
|
2083
|
+
{ $group: { _id: truncTo("$occurredAt", interval), value: measureAccumulator(measure) } },
|
|
1591
2084
|
{ $sort: { _id: 1 } },
|
|
1592
2085
|
{ $limit: limits.series }
|
|
1593
2086
|
]);
|
|
@@ -1595,6 +2088,114 @@ function createQueries(ctx) {
|
|
|
1595
2088
|
})
|
|
1596
2089
|
);
|
|
1597
2090
|
},
|
|
2091
|
+
/**
|
|
2092
|
+
* Top groups of a measure by one or two dimensions — "which models cost the
|
|
2093
|
+
* most", "errors by release", "events by platform per week". The primitive
|
|
2094
|
+
* that replaces a page's client-side grouping of whatever rows it happened
|
|
2095
|
+
* to have fetched, which answered "this page" while reading like it
|
|
2096
|
+
* answered the range (reports §6).
|
|
2097
|
+
*
|
|
2098
|
+
* THE CAP IS ON GROUPS RETURNED, NEVER ON ROWS SCANNED. Every `$limit`
|
|
2099
|
+
* below sits AFTER a `$group`, exactly as series() does: the scan is bounded
|
|
2100
|
+
* by buildMatch — tenant, range, filters, indexes — and nothing else, so a
|
|
2101
|
+
* quarter of a million records is one pass and 50 rows. Truncation
|
|
2102
|
+
* therefore keeps the TOP groups by measure, which is what a breakdown
|
|
2103
|
+
* table means; a cap on documents scanned would return an arbitrary prefix
|
|
2104
|
+
* and call it the top.
|
|
2105
|
+
*
|
|
2106
|
+
* With an `interval` this runs a SECOND aggregate restricted to the top
|
|
2107
|
+
* groups, rather than one pipeline that groups by (dims, bucket) and folds.
|
|
2108
|
+
* Two reasons: the ranking must be the measure over the WHOLE range (the
|
|
2109
|
+
* same number the no-interval call reports), and folding in one pass means
|
|
2110
|
+
* `$push`-ing every bucket of every group before the cap can apply — the
|
|
2111
|
+
* unbounded intermediate this primitive exists to avoid. The restriction is
|
|
2112
|
+
* an `$expr`/`$or` over the ≤ cap tuples because a dim can be a computed
|
|
2113
|
+
* expression (subjectType), which a plain `$in` on a path cannot address.
|
|
2114
|
+
*
|
|
2115
|
+
* Under PLATFORM_SCOPE it aggregates ACROSS tenants, like series() — one
|
|
2116
|
+
* set of groups with every tenant summed into it, which is the platform-wide
|
|
2117
|
+
* table a platform operator came for. Ask for a per-tenant split by scoping
|
|
2118
|
+
* to a tenant, or with rollups().
|
|
2119
|
+
*/
|
|
2120
|
+
breakdown(scope, range, filter, opts) {
|
|
2121
|
+
const groupBy = opts.groupBy ?? [];
|
|
2122
|
+
if (groupBy.length < 1 || groupBy.length > 2) {
|
|
2123
|
+
throw badRequest(`groupBy takes 1 or 2 dimensions, got ${groupBy.length}`);
|
|
2124
|
+
}
|
|
2125
|
+
const measure = opts.measure ?? "count";
|
|
2126
|
+
const interval = opts.interval;
|
|
2127
|
+
if (interval && !INTERVALS.includes(interval)) {
|
|
2128
|
+
throw badRequest(`interval must be one of ${INTERVALS.join(", ")}`);
|
|
2129
|
+
}
|
|
2130
|
+
const dims = groupBy.map(dimExpression);
|
|
2131
|
+
const cap = Math.min(Math.max(1, opts.limit ?? limits.breakdown), limits.breakdown);
|
|
2132
|
+
const key = JSON.stringify([
|
|
2133
|
+
"breakdown",
|
|
2134
|
+
scope,
|
|
2135
|
+
range.from,
|
|
2136
|
+
range.to,
|
|
2137
|
+
filter,
|
|
2138
|
+
groupBy,
|
|
2139
|
+
measure,
|
|
2140
|
+
interval ?? null,
|
|
2141
|
+
cap
|
|
2142
|
+
]);
|
|
2143
|
+
return cache.get(
|
|
2144
|
+
key,
|
|
2145
|
+
() => timed("breakdown", { scope, filter, groupBy, measure, interval }, async () => {
|
|
2146
|
+
const match = buildMatch(scope, range, filter);
|
|
2147
|
+
const dimId = Object.fromEntries(dims.map((expr, i) => [`d${i}`, expr]));
|
|
2148
|
+
const top = await ctx.TelemetryModel.aggregate([
|
|
2149
|
+
{ $match: match },
|
|
2150
|
+
{ $group: { _id: dimId, value: measureAccumulator(measure) } },
|
|
2151
|
+
{ $sort: { value: -1, _id: 1 } },
|
|
2152
|
+
{ $limit: cap + 1 }
|
|
2153
|
+
]);
|
|
2154
|
+
const truncated = top.length > cap;
|
|
2155
|
+
if (truncated) top.pop();
|
|
2156
|
+
const tuples = top.map(
|
|
2157
|
+
(g) => groupBy.map((_, i) => g._id?.[`d${i}`] ?? null)
|
|
2158
|
+
);
|
|
2159
|
+
if (!interval) {
|
|
2160
|
+
return {
|
|
2161
|
+
rows: top.map((g, i) => ({ dims: tuples[i], value: g.value })),
|
|
2162
|
+
groups: top.length,
|
|
2163
|
+
truncated,
|
|
2164
|
+
bucketsTruncated: false,
|
|
2165
|
+
dataSource: "raw"
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
if (!tuples.length) {
|
|
2169
|
+
return { rows: [], groups: 0, truncated, bucketsTruncated: false, dataSource: "raw" };
|
|
2170
|
+
}
|
|
2171
|
+
const inTop = {
|
|
2172
|
+
$or: tuples.map((t) => ({ $and: dims.map((expr, i) => ({ $eq: [expr, t[i] ?? null] })) }))
|
|
2173
|
+
};
|
|
2174
|
+
const bucketCap = limits.series * top.length;
|
|
2175
|
+
const perBucket = await ctx.TelemetryModel.aggregate([
|
|
2176
|
+
{ $match: { ...match, $expr: inTop } },
|
|
2177
|
+
{ $group: { _id: { at: truncTo("$occurredAt", interval), ...dimId }, value: measureAccumulator(measure) } },
|
|
2178
|
+
// `at` is the first key of `_id`, so one BSON sort orders by bucket
|
|
2179
|
+
// then by dims — deterministic without a second sort key
|
|
2180
|
+
{ $sort: { _id: 1 } },
|
|
2181
|
+
{ $limit: bucketCap + 1 }
|
|
2182
|
+
]);
|
|
2183
|
+
const bucketsTruncated = perBucket.length > bucketCap;
|
|
2184
|
+
if (bucketsTruncated) perBucket.pop();
|
|
2185
|
+
return {
|
|
2186
|
+
rows: perBucket.map((b) => ({
|
|
2187
|
+
dims: groupBy.map((_, i) => b._id?.[`d${i}`] ?? null),
|
|
2188
|
+
at: b._id.at,
|
|
2189
|
+
value: b.value
|
|
2190
|
+
})),
|
|
2191
|
+
groups: top.length,
|
|
2192
|
+
truncated,
|
|
2193
|
+
bucketsTruncated,
|
|
2194
|
+
dataSource: "raw"
|
|
2195
|
+
};
|
|
2196
|
+
})
|
|
2197
|
+
);
|
|
2198
|
+
},
|
|
1598
2199
|
/**
|
|
1599
2200
|
* Percentiles + histogram off raw. Keep-all makes the SAMPLE complete —
|
|
1600
2201
|
* no sampling stands between the match and the math (§5.3) — but the
|
|
@@ -1836,6 +2437,820 @@ function requireDistinctFamily(registry, as) {
|
|
|
1836
2437
|
}
|
|
1837
2438
|
return spec;
|
|
1838
2439
|
}
|
|
2440
|
+
|
|
2441
|
+
// src/server/values.ts
|
|
2442
|
+
var empty = (source) => ({
|
|
2443
|
+
values: [],
|
|
2444
|
+
source,
|
|
2445
|
+
truncated: false,
|
|
2446
|
+
dataSource: source
|
|
2447
|
+
});
|
|
2448
|
+
var SAMPLED_COUNT = { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } };
|
|
2449
|
+
function createValues(ctx) {
|
|
2450
|
+
const limits = { ...DEFAULT_LIMITS, ...ctx.limits };
|
|
2451
|
+
const slowMs = ctx.slowMs ?? 500;
|
|
2452
|
+
const cache = new QueryCache(ctx.cacheTtlMs ?? 10 * 6e4, ctx.cacheSize ?? 60);
|
|
2453
|
+
const { catalog } = ctx;
|
|
2454
|
+
const timed = async (op, params, run) => {
|
|
2455
|
+
const t0 = Date.now();
|
|
2456
|
+
try {
|
|
2457
|
+
return await run();
|
|
2458
|
+
} finally {
|
|
2459
|
+
const ms = Date.now() - t0;
|
|
2460
|
+
if (ms > slowMs) ctx.onSlowQuery?.({ op, ms, params });
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2463
|
+
const eventNames = (names) => names?.length ? names.filter((n) => catalog.events[n]) : Object.keys(catalog.events);
|
|
2464
|
+
function fromCatalog(dim2, names) {
|
|
2465
|
+
const facets = [];
|
|
2466
|
+
for (const d of catalog.envelope) if (d.key === dim2) facets.push(d);
|
|
2467
|
+
for (const name of eventNames(names)) {
|
|
2468
|
+
for (const d of catalog.events[name].dims) if (d.key === dim2) facets.push(d);
|
|
2469
|
+
}
|
|
2470
|
+
const out = [];
|
|
2471
|
+
for (const f of facets) for (const v of f.values ?? []) if (!out.includes(v)) out.push(v);
|
|
2472
|
+
return out;
|
|
2473
|
+
}
|
|
2474
|
+
function pickFamily(dim2, names) {
|
|
2475
|
+
if (dim2 === "subjectType" || dim2 === "actorType") return null;
|
|
2476
|
+
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));
|
|
2477
|
+
const best = matches[0];
|
|
2478
|
+
return best ? { as: best.f.as, index: best.index, label: best.f.labels[best.index] } : null;
|
|
2479
|
+
}
|
|
2480
|
+
function rawReadable(dim2, names) {
|
|
2481
|
+
try {
|
|
2482
|
+
dimExpression(dim2);
|
|
2483
|
+
} catch {
|
|
2484
|
+
return false;
|
|
2485
|
+
}
|
|
2486
|
+
if (!dim2.startsWith("attr:")) return true;
|
|
2487
|
+
const key = dim2.slice(5);
|
|
2488
|
+
return eventNames(names).some((n) => catalog.events[n].indexedAttrs.includes(key));
|
|
2489
|
+
}
|
|
2490
|
+
return async function values(scope, params) {
|
|
2491
|
+
const { dim: dim2, names, range } = params;
|
|
2492
|
+
if (!dim2) return empty("none");
|
|
2493
|
+
const cap = Math.min(Math.max(1, params.limit ?? limits.values), limits.values);
|
|
2494
|
+
const key = JSON.stringify([
|
|
2495
|
+
"values",
|
|
2496
|
+
scope,
|
|
2497
|
+
dim2,
|
|
2498
|
+
names ?? null,
|
|
2499
|
+
range?.from ?? null,
|
|
2500
|
+
range?.to ?? null,
|
|
2501
|
+
cap
|
|
2502
|
+
]);
|
|
2503
|
+
return cache.get(
|
|
2504
|
+
key,
|
|
2505
|
+
() => timed("values", { scope, dim: dim2, names }, async () => {
|
|
2506
|
+
const declared = fromCatalog(dim2, names);
|
|
2507
|
+
if (declared.length) {
|
|
2508
|
+
return { values: declared, source: "catalog", truncated: false, dataSource: "catalog" };
|
|
2509
|
+
}
|
|
2510
|
+
const family = pickFamily(dim2, names);
|
|
2511
|
+
if (family) {
|
|
2512
|
+
const rows2 = await ctx.RollupModel.aggregate([
|
|
2513
|
+
{
|
|
2514
|
+
$match: {
|
|
2515
|
+
...isPlatformScope(scope) ? {} : { tenantId: scope },
|
|
2516
|
+
as: family.as
|
|
2517
|
+
}
|
|
2518
|
+
},
|
|
2519
|
+
{ $project: { v: { $arrayElemAt: ["$dims", family.index] }, count: 1 } },
|
|
2520
|
+
{ $match: { v: { $type: "string" } } },
|
|
2521
|
+
{ $group: { _id: "$v", count: { $sum: "$count" } } },
|
|
2522
|
+
{ $sort: { count: -1, _id: 1 } },
|
|
2523
|
+
{ $limit: cap + 1 }
|
|
2524
|
+
]);
|
|
2525
|
+
const truncated2 = rows2.length > cap;
|
|
2526
|
+
if (truncated2) rows2.pop();
|
|
2527
|
+
const prefix = `${family.label}=`;
|
|
2528
|
+
return {
|
|
2529
|
+
values: rows2.map(
|
|
2530
|
+
(r) => String(r._id).startsWith(prefix) ? String(r._id).slice(prefix.length) : String(r._id)
|
|
2531
|
+
),
|
|
2532
|
+
counts: rows2.map((r) => r.count),
|
|
2533
|
+
source: "rollups",
|
|
2534
|
+
via: family.as,
|
|
2535
|
+
truncated: truncated2,
|
|
2536
|
+
dataSource: "rollups"
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
if (!rawReadable(dim2, names)) return empty("none");
|
|
2540
|
+
if (!range) return empty("none");
|
|
2541
|
+
const rows = await ctx.TelemetryModel.aggregate([
|
|
2542
|
+
{ $match: buildMatch(scope, range, names?.length ? { name: names } : {}) },
|
|
2543
|
+
{ $group: { _id: dimExpression(dim2), count: SAMPLED_COUNT } },
|
|
2544
|
+
// a "no value" is not a value to pick — the null group is real
|
|
2545
|
+
// (breakdown reports it) but it is not something a filter can name
|
|
2546
|
+
{ $match: { _id: { $ne: null } } },
|
|
2547
|
+
{ $sort: { count: -1, _id: 1 } },
|
|
2548
|
+
{ $limit: cap + 1 }
|
|
2549
|
+
]);
|
|
2550
|
+
const truncated = rows.length > cap;
|
|
2551
|
+
if (truncated) rows.pop();
|
|
2552
|
+
return {
|
|
2553
|
+
values: rows.map((r) => String(r._id)),
|
|
2554
|
+
counts: rows.map((r) => r.count),
|
|
2555
|
+
source: "raw",
|
|
2556
|
+
truncated,
|
|
2557
|
+
dataSource: "raw"
|
|
2558
|
+
};
|
|
2559
|
+
})
|
|
2560
|
+
);
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
// src/server/report.ts
|
|
2565
|
+
var RANGE_MS = {
|
|
2566
|
+
"1h": 36e5,
|
|
2567
|
+
"24h": 864e5,
|
|
2568
|
+
"7d": 7 * 864e5,
|
|
2569
|
+
"30d": 30 * 864e5,
|
|
2570
|
+
"90d": 90 * 864e5
|
|
2571
|
+
};
|
|
2572
|
+
var badRequest2 = (message) => Object.assign(new Error(`telemetry: ${message}`), { status: 400 });
|
|
2573
|
+
function rangeOf(range, now = /* @__PURE__ */ new Date()) {
|
|
2574
|
+
if (typeof range === "string") {
|
|
2575
|
+
const ms = RANGE_MS[range] ?? spanOf(range);
|
|
2576
|
+
if (ms == null) {
|
|
2577
|
+
throw badRequest2(
|
|
2578
|
+
`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`
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
return { from: new Date(now.getTime() - ms), to: now };
|
|
2582
|
+
}
|
|
2583
|
+
const from = new Date(range.from);
|
|
2584
|
+
const to = new Date(range.to);
|
|
2585
|
+
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()) || from >= to) {
|
|
2586
|
+
throw badRequest2(
|
|
2587
|
+
`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)`
|
|
2588
|
+
);
|
|
2589
|
+
}
|
|
2590
|
+
return { from, to };
|
|
2591
|
+
}
|
|
2592
|
+
function spanOf(range) {
|
|
2593
|
+
const m = /^(\d+)([hd])$/.exec(range);
|
|
2594
|
+
if (!m) return null;
|
|
2595
|
+
return Number(m[1]) * (m[2] === "h" ? 36e5 : 864e5);
|
|
2596
|
+
}
|
|
2597
|
+
function intervalForRange(range, now = /* @__PURE__ */ new Date()) {
|
|
2598
|
+
if (typeof range === "string" && RANGE_MS[range] != null) {
|
|
2599
|
+
return range === "1h" || range === "24h" ? "hour" : range === "90d" ? "week" : "day";
|
|
2600
|
+
}
|
|
2601
|
+
const { from, to } = rangeOf(range, now);
|
|
2602
|
+
const ms = to.getTime() - from.getTime();
|
|
2603
|
+
if (ms <= 864e5) return "hour";
|
|
2604
|
+
if (ms < 90 * 864e5) return "day";
|
|
2605
|
+
return "week";
|
|
2606
|
+
}
|
|
2607
|
+
var INTERVAL_RANK = { hour: 0, day: 1, week: 2, month: 3 };
|
|
2608
|
+
var shift = (range) => ({
|
|
2609
|
+
from: new Date(range.from.getTime() - (range.to.getTime() - range.from.getTime())),
|
|
2610
|
+
to: range.from
|
|
2611
|
+
});
|
|
2612
|
+
function expandSource(source, catalog) {
|
|
2613
|
+
if ("event" in source) {
|
|
2614
|
+
if (!catalog.events[source.event]) {
|
|
2615
|
+
return unavailable(
|
|
2616
|
+
`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`
|
|
2617
|
+
);
|
|
2618
|
+
}
|
|
2619
|
+
return { form: "event", events: [source.event] };
|
|
2620
|
+
}
|
|
2621
|
+
if ("namespace" in source) {
|
|
2622
|
+
const events = catalog.namespaces[source.namespace];
|
|
2623
|
+
if (!events?.length) {
|
|
2624
|
+
return unavailable(
|
|
2625
|
+
`no event name starts with "${source.namespace}." \u2014 the registered namespaces are ${Object.keys(catalog.namespaces).join(", ")}`
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2628
|
+
return { form: "namespace", events: [...events] };
|
|
2629
|
+
}
|
|
2630
|
+
if ("kind" in source) {
|
|
2631
|
+
const events = Object.keys(catalog.events).filter((n) => catalog.events[n].kind === source.kind);
|
|
2632
|
+
if (!events.length) {
|
|
2633
|
+
return unavailable(
|
|
2634
|
+
`no event is registered with kind "${source.kind}" \u2014 declare one, or pick a kind the registry uses`
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
return { form: "kind", events, kind: source.kind };
|
|
2638
|
+
}
|
|
2639
|
+
const family = catalog.families[source.family];
|
|
2640
|
+
if (!family) {
|
|
2641
|
+
return unavailable(
|
|
2642
|
+
`no rollup family "${source.family}" is declared \u2014 add a \`rollups: [{ as: '${source.family}', by: [...] }]\` block to the event that should feed it`
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
return { form: "family", events: [...family.feeders], family };
|
|
2646
|
+
}
|
|
2647
|
+
var FILTER_ONLY = {
|
|
2648
|
+
"field:subject": "subject",
|
|
2649
|
+
"field:traceId": "traceId"
|
|
2650
|
+
};
|
|
2651
|
+
function dimsFor(catalog, events) {
|
|
2652
|
+
const out = /* @__PURE__ */ new Map();
|
|
2653
|
+
for (const d of catalog.envelope) out.set(d.key, d);
|
|
2654
|
+
for (const name of events) {
|
|
2655
|
+
for (const d of catalog.events[name]?.dims ?? []) {
|
|
2656
|
+
const seen = out.get(d.key);
|
|
2657
|
+
out.set(d.key, seen ? { ...seen, indexed: seen.indexed && d.indexed } : d);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
return out;
|
|
2661
|
+
}
|
|
2662
|
+
var measureDeclared = (catalog, events, key) => key === "count" || events.some((n) => catalog.events[n]?.measures.some((m) => m.key === key));
|
|
2663
|
+
var MEASURE_OP = /^(sum|avg|p50|p90|p95|p99):(.+)$/;
|
|
2664
|
+
var unavailable = (why) => ({ unavailable: true, why });
|
|
2665
|
+
var count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
2666
|
+
function resolveReport(report, catalog, opts = {}) {
|
|
2667
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
2668
|
+
const limits = opts.limits ?? {};
|
|
2669
|
+
const src = expandSource(report.source, catalog);
|
|
2670
|
+
if ("unavailable" in src) return src;
|
|
2671
|
+
const measure = report.measure ?? "count";
|
|
2672
|
+
const groupBy = report.groupBy ?? [];
|
|
2673
|
+
if (groupBy.length > 2) {
|
|
2674
|
+
return unavailable(
|
|
2675
|
+
`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`
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
if (report.interval && INTERVAL_RANK[report.interval] == null) {
|
|
2679
|
+
return unavailable(`interval "${report.interval}" is not one of hour, day, week, month`);
|
|
2680
|
+
}
|
|
2681
|
+
if (measure === "funnel") return planFunnel(report, catalog, now, limits);
|
|
2682
|
+
if (measure.startsWith("distinct:")) return planDistinct(report, catalog, src, measure, now);
|
|
2683
|
+
const opMatch = MEASURE_OP.exec(measure);
|
|
2684
|
+
if (measure !== "count" && !opMatch) {
|
|
2685
|
+
return unavailable(
|
|
2686
|
+
`measure "${measure}" is not a measure \u2014 use 'count', 'sum:<metric>', 'avg:<metric>', 'p50|p95|p99:<metric>', 'distinct:<subjectType>' or 'funnel'`
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
if (opMatch && !measureDeclared(catalog, src.events, measure)) {
|
|
2690
|
+
return unavailable(
|
|
2691
|
+
`"${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)}`
|
|
2692
|
+
);
|
|
2693
|
+
}
|
|
2694
|
+
const exact = planRollups(report, catalog, src, measure, groupBy, now, limits);
|
|
2695
|
+
if (exact) return exact;
|
|
2696
|
+
const filter = toRecordFilter(report, catalog, src);
|
|
2697
|
+
if ("unavailable" in filter) return filter;
|
|
2698
|
+
const range = rangeOf(report.range, now);
|
|
2699
|
+
const dims = dimsFor(catalog, src.events);
|
|
2700
|
+
const touched = [...groupBy, ...(report.filters ?? []).map((f) => f.dim)];
|
|
2701
|
+
const unindexed = touched.find((k) => !(dims.get(k)?.indexed ?? FILTER_ONLY[k] != null));
|
|
2702
|
+
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` : "") : "";
|
|
2703
|
+
const exactness = unindexed != null ? "scan" : "raw";
|
|
2704
|
+
if (opMatch && opMatch[1] !== "sum" && opMatch[1] !== "avg") {
|
|
2705
|
+
if (groupBy.length) {
|
|
2706
|
+
return unavailable(
|
|
2707
|
+
`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`
|
|
2708
|
+
);
|
|
2709
|
+
}
|
|
2710
|
+
return withCompare(report, {
|
|
2711
|
+
primitive: "distribution",
|
|
2712
|
+
args: [range, filter, { measure: opMatch[2] }],
|
|
2713
|
+
exactness,
|
|
2714
|
+
why: `${measure} is approximate by construction ($percentile t-digest over the matched records)${scanWhy}`
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
if (groupBy.length) {
|
|
2718
|
+
const bad = groupBy.find((k) => !dims.has(k));
|
|
2719
|
+
if (bad) {
|
|
2720
|
+
return unavailable(
|
|
2721
|
+
`"${bad}" is not a dimension of ${describe(src)} \u2014 group by one of ${[...dims.keys()].join(", ")}`
|
|
2722
|
+
);
|
|
2723
|
+
}
|
|
2724
|
+
return withCompare(report, {
|
|
2725
|
+
primitive: "breakdown",
|
|
2726
|
+
args: [
|
|
2727
|
+
range,
|
|
2728
|
+
filter,
|
|
2729
|
+
{
|
|
2730
|
+
groupBy,
|
|
2731
|
+
measure,
|
|
2732
|
+
...report.interval ? { interval: report.interval } : {},
|
|
2733
|
+
...report.limit ? { limit: capped(report.limit, limits.breakdown) } : {}
|
|
2734
|
+
}
|
|
2735
|
+
],
|
|
2736
|
+
exactness,
|
|
2737
|
+
why: `raw ${measure} by ${groupBy.join(" \xD7 ")} over the range${scanWhy}`
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
if (!report.measure && !report.interval) {
|
|
2741
|
+
return withCompare(report, {
|
|
2742
|
+
primitive: "records",
|
|
2743
|
+
args: [range, filter, report.limit ? { limit: capped(report.limit, limits.records) } : {}],
|
|
2744
|
+
exactness,
|
|
2745
|
+
why: `the matching records themselves, newest first${scanWhy}`
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
const interval = report.interval ?? intervalForRange(report.range, now);
|
|
2749
|
+
return withCompare(report, {
|
|
2750
|
+
primitive: "series",
|
|
2751
|
+
args: [range, filter, { measure, interval }],
|
|
2752
|
+
exactness,
|
|
2753
|
+
why: `raw ${measure} per ${interval} over the range${scanWhy}`
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
var capped = (limit, cap) => cap == null ? limit : Math.max(1, Math.min(limit, cap));
|
|
2757
|
+
var describe = (src) => src.form === "family" ? `family "${src.family.as}"` : src.events.join(", ");
|
|
2758
|
+
var metricList = (catalog, events) => {
|
|
2759
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2760
|
+
for (const n of events) for (const m of catalog.events[n]?.measures ?? []) keys.add(m.key);
|
|
2761
|
+
return keys.size ? [...keys].join(", ") : "nothing but count";
|
|
2762
|
+
};
|
|
2763
|
+
function planFunnel(report, catalog, now, limits) {
|
|
2764
|
+
const stages = report.stages ?? [];
|
|
2765
|
+
if (!stages.length) {
|
|
2766
|
+
return unavailable(
|
|
2767
|
+
"`measure: 'funnel'` needs `stages` \u2014 one or more lifetime `by: ['subject']` rollup family names, in the order a subject reaches them"
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
const anchor = report.anchor ?? stages[0];
|
|
2771
|
+
const exits = report.exits ?? [];
|
|
2772
|
+
for (const as of [...stages, anchor, ...exits]) {
|
|
2773
|
+
const refusal = milestoneRefusal(catalog, as);
|
|
2774
|
+
if (refusal) return unavailable(refusal);
|
|
2775
|
+
}
|
|
2776
|
+
const first = catalog.families[stages[0]];
|
|
2777
|
+
for (const as of [...stages.slice(1), anchor]) {
|
|
2778
|
+
const f = catalog.families[as];
|
|
2779
|
+
if (!sameSet(f.subjectTypes, first.subjectTypes)) {
|
|
2780
|
+
return unavailable(
|
|
2781
|
+
`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`
|
|
2782
|
+
);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
if (report.subjectType && !first.subjectTypes.includes(report.subjectType)) {
|
|
2786
|
+
return unavailable(
|
|
2787
|
+
`subjectType "${report.subjectType}" is not one of the stages' subjects (${first.subjectTypes.join(", ")}) \u2014 the cohort would be empty`
|
|
2788
|
+
);
|
|
2789
|
+
}
|
|
2790
|
+
if (report.interval === "hour") {
|
|
2791
|
+
return unavailable(
|
|
2792
|
+
"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"
|
|
2793
|
+
);
|
|
2794
|
+
}
|
|
2795
|
+
const params = {
|
|
2796
|
+
stages: stages.map((as) => ({ as })),
|
|
2797
|
+
anchor,
|
|
2798
|
+
...exits.length ? { exits: exits.map((as) => ({ as })) } : {},
|
|
2799
|
+
cohort: rangeOf(report.range, now),
|
|
2800
|
+
...report.subjectType ? { subjectType: report.subjectType } : {},
|
|
2801
|
+
...report.interval ? { interval: report.interval } : {},
|
|
2802
|
+
...report.limit ? { limit: capped(report.limit, limits.funnel) } : {}
|
|
2803
|
+
};
|
|
2804
|
+
return withCompare(report, {
|
|
2805
|
+
primitive: "funnel",
|
|
2806
|
+
args: [params],
|
|
2807
|
+
exactness: "exact",
|
|
2808
|
+
via: anchor,
|
|
2809
|
+
why: `cohort funnel over ${count(stages.length, "lifetime milestone family")}, anchored on "${anchor}" \u2014 rollups only, no raw scan`
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
function milestoneRefusal(catalog, as) {
|
|
2813
|
+
const f = catalog.families[as];
|
|
2814
|
+
if (!f) {
|
|
2815
|
+
return `no rollup family "${as}" is declared. Add a \`rollups: [{ as: '${as}', by: ['subject'], subjects: [...] }]\` block to the event that marks it`;
|
|
2816
|
+
}
|
|
2817
|
+
const shape = `by: [${f.by.map((d) => `'${d}'`).join(", ")}]${f.bucket ? `, bucket: '${f.bucket}'` : ""}`;
|
|
2818
|
+
if (!f.lifetime) {
|
|
2819
|
+
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`;
|
|
2820
|
+
}
|
|
2821
|
+
if (f.by.length !== 1 || f.by[0] !== "subject") {
|
|
2822
|
+
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`;
|
|
2823
|
+
}
|
|
2824
|
+
return null;
|
|
2825
|
+
}
|
|
2826
|
+
function planDistinct(report, catalog, src, measure, now) {
|
|
2827
|
+
const subjectType = measure.slice("distinct:".length);
|
|
2828
|
+
if (!subjectType) {
|
|
2829
|
+
return unavailable(
|
|
2830
|
+
`"${measure}" needs a subject type \u2014 'distinct:account', one of ${catalog.subjectTypes.join(", ")}`
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
if (!catalog.subjectTypes.includes(subjectType)) {
|
|
2834
|
+
return unavailable(
|
|
2835
|
+
`no event or rollup declares the subject type "${subjectType}" \u2014 the registry knows ${catalog.subjectTypes.join(", ") || "no subject types at all"}`
|
|
2836
|
+
);
|
|
2837
|
+
}
|
|
2838
|
+
if (report.groupBy?.length) {
|
|
2839
|
+
return unavailable(
|
|
2840
|
+
`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`
|
|
2841
|
+
);
|
|
2842
|
+
}
|
|
2843
|
+
const wanted = new Set(src.events);
|
|
2844
|
+
const fits = Object.values(catalog.families).filter(
|
|
2845
|
+
(f) => f.bucket != null && f.by.length === 1 && f.by[0] === "subject" && f.subjectTypes.includes(subjectType) && src.events.every((e) => f.feeders.includes(e))
|
|
2846
|
+
);
|
|
2847
|
+
const family = fits.find((f) => sameSet(f.feeders, [...wanted])) ?? fits[0];
|
|
2848
|
+
if (!family) {
|
|
2849
|
+
return unavailable(
|
|
2850
|
+
`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`
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
const superset = !sameSet(family.feeders, [...wanted]);
|
|
2854
|
+
return withCompare(report, {
|
|
2855
|
+
primitive: "distinctCount",
|
|
2856
|
+
args: [
|
|
2857
|
+
{
|
|
2858
|
+
as: family.as,
|
|
2859
|
+
subjectType,
|
|
2860
|
+
range: rangeOf(report.range, now),
|
|
2861
|
+
...report.interval ? { interval: report.interval } : {}
|
|
2862
|
+
}
|
|
2863
|
+
],
|
|
2864
|
+
exactness: "exact",
|
|
2865
|
+
via: family.as,
|
|
2866
|
+
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` : "")
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
var famDims = (f) => f.by.map((b) => b === "subject" ? "subjectType" : b);
|
|
2870
|
+
function planRollups(report, catalog, src, measure, groupBy, now, limits) {
|
|
2871
|
+
if (report.excludeActorTypes?.length) return null;
|
|
2872
|
+
const candidates = src.family ? [src.family] : Object.values(catalog.families).filter((f) => sameSet(f.feeders, src.events));
|
|
2873
|
+
const op = MEASURE_OP.exec(measure);
|
|
2874
|
+
const filters = report.filters ?? [];
|
|
2875
|
+
const fits = candidates.filter((f) => {
|
|
2876
|
+
const dims2 = famDims(f);
|
|
2877
|
+
if (!groupBy.every((k) => dims2.includes(k))) return false;
|
|
2878
|
+
if (report.interval && (!f.bucket || INTERVAL_RANK[f.bucket] > INTERVAL_RANK[report.interval])) return false;
|
|
2879
|
+
if (op) {
|
|
2880
|
+
if (op[1] !== "sum" && op[1] !== "avg" || !f.sums.includes(op[2])) return false;
|
|
2881
|
+
}
|
|
2882
|
+
return filters.every(
|
|
2883
|
+
(t) => dims2.includes(t.dim) && (t.op === "eq" || t.op === "in") || nameFilterCovers(t, f)
|
|
2884
|
+
);
|
|
2885
|
+
});
|
|
2886
|
+
const family = fits.sort((a, b) => a.by.length - b.by.length)[0];
|
|
2887
|
+
if (!family) return null;
|
|
2888
|
+
const dims = famDims(family);
|
|
2889
|
+
const range = rangeOf(report.range, now);
|
|
2890
|
+
const on = family.lifetime ? "firstAt" : "bucketAt";
|
|
2891
|
+
const fold = filters.filter((t) => dims.includes(t.dim));
|
|
2892
|
+
return withCompare(report, {
|
|
2893
|
+
primitive: "rollups",
|
|
2894
|
+
args: [
|
|
2895
|
+
{
|
|
2896
|
+
as: family.as,
|
|
2897
|
+
on,
|
|
2898
|
+
range,
|
|
2899
|
+
sort: family.lifetime ? "count" : "bucketAt",
|
|
2900
|
+
...report.limit ? { limit: capped(report.limit, limits.rollups) } : {}
|
|
2901
|
+
}
|
|
2902
|
+
],
|
|
2903
|
+
exactness: "exact",
|
|
2904
|
+
via: family.as,
|
|
2905
|
+
shape: {
|
|
2906
|
+
groupBy,
|
|
2907
|
+
labels: groupBy.map((k) => family.labels[dims.indexOf(k)]),
|
|
2908
|
+
measure,
|
|
2909
|
+
...report.interval ? { interval: report.interval } : {},
|
|
2910
|
+
...fold.length ? { filters: fold.map((t) => ({ ...t, label: family.labels[dims.indexOf(t.dim)] })) } : {}
|
|
2911
|
+
},
|
|
2912
|
+
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` : "")
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
function nameFilterCovers(t, f) {
|
|
2916
|
+
if (t.dim !== "field:name") return false;
|
|
2917
|
+
const admitted = t.op === "eq" ? [String(t.value)] : t.op === "in" ? [...t.value].map(String) : null;
|
|
2918
|
+
return admitted != null && f.feeders.every((n) => admitted.includes(n));
|
|
2919
|
+
}
|
|
2920
|
+
var FIELD_TERMS = {
|
|
2921
|
+
"field:kind": "kind",
|
|
2922
|
+
"field:name": "name",
|
|
2923
|
+
"field:severity": "severity",
|
|
2924
|
+
"field:env": "env",
|
|
2925
|
+
"field:service": "service",
|
|
2926
|
+
"field:release": "release",
|
|
2927
|
+
...FILTER_ONLY
|
|
2928
|
+
};
|
|
2929
|
+
function toRecordFilter(report, catalog, src) {
|
|
2930
|
+
const filter = {};
|
|
2931
|
+
if (src.events.length === 1) {
|
|
2932
|
+
filter.name = src.events[0];
|
|
2933
|
+
} else if (src.kind) {
|
|
2934
|
+
filter.kind = src.kind;
|
|
2935
|
+
} else {
|
|
2936
|
+
filter.name = [...src.events];
|
|
2937
|
+
}
|
|
2938
|
+
for (const t of report.filters ?? []) {
|
|
2939
|
+
const term = FIELD_TERMS[t.dim];
|
|
2940
|
+
if (term) {
|
|
2941
|
+
if (t.op !== "eq") {
|
|
2942
|
+
return unavailable(
|
|
2943
|
+
`"${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`
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2946
|
+
filter[term] = String(t.value);
|
|
2947
|
+
continue;
|
|
2948
|
+
}
|
|
2949
|
+
if (t.dim.startsWith("attr:")) {
|
|
2950
|
+
const key = t.dim.slice(5);
|
|
2951
|
+
if (t.op === "eq") {
|
|
2952
|
+
(filter.attrs ??= {})[key] = String(t.value);
|
|
2953
|
+
continue;
|
|
2954
|
+
}
|
|
2955
|
+
if (t.op === "gte" || t.op === "lte") {
|
|
2956
|
+
if (!measureDeclared(catalog, src.events, `sum:${key}`)) {
|
|
2957
|
+
return unavailable(
|
|
2958
|
+
`"${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`
|
|
2959
|
+
);
|
|
2960
|
+
}
|
|
2961
|
+
const range = (filter.metrics ??= {})[key] ??= {};
|
|
2962
|
+
range[t.op] = Number(t.value);
|
|
2963
|
+
continue;
|
|
2964
|
+
}
|
|
2965
|
+
return unavailable(
|
|
2966
|
+
`"${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`
|
|
2967
|
+
);
|
|
2968
|
+
}
|
|
2969
|
+
if (t.dim === "subjectType" || t.dim === "actorType") {
|
|
2970
|
+
return unavailable(
|
|
2971
|
+
`"${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`" : "")
|
|
2972
|
+
);
|
|
2973
|
+
}
|
|
2974
|
+
return unavailable(
|
|
2975
|
+
`"${t.dim}" is not filterable on the raw path \u2014 RecordFilter carries ${Object.keys(FIELD_TERMS).join(", ")}, \`attr:<key>\` and metric bounds`
|
|
2976
|
+
);
|
|
2977
|
+
}
|
|
2978
|
+
if (report.excludeActorTypes?.length) filter.excludeActorTypes = [...report.excludeActorTypes];
|
|
2979
|
+
return filter;
|
|
2980
|
+
}
|
|
2981
|
+
function withCompare(report, plan) {
|
|
2982
|
+
if (report.compare !== "previous") return plan;
|
|
2983
|
+
const [first, ...rest] = plan.args;
|
|
2984
|
+
if (plan.primitive === "rollups" || plan.primitive === "distinctCount" || plan.primitive === "funnel") {
|
|
2985
|
+
const params = first;
|
|
2986
|
+
const key = plan.primitive === "funnel" ? "cohort" : "range";
|
|
2987
|
+
const window = params[key];
|
|
2988
|
+
if (!window) return plan;
|
|
2989
|
+
return { ...plan, previous: { args: [{ ...params, [key]: { ...window, ...shift(window) } }, ...rest] } };
|
|
2990
|
+
}
|
|
2991
|
+
return { ...plan, previous: { args: [shift(first), ...rest] } };
|
|
2992
|
+
}
|
|
2993
|
+
var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
2994
|
+
var FILTER_OPS = /* @__PURE__ */ new Set(["eq", "in", "gte", "lte"]);
|
|
2995
|
+
var SORTS = /* @__PURE__ */ new Set(["value", "label", "time"]);
|
|
2996
|
+
function parseReportQuery(q) {
|
|
2997
|
+
const str = (k) => {
|
|
2998
|
+
const v = Array.isArray(q[k]) ? q[k][0] : q[k];
|
|
2999
|
+
return typeof v === "string" && v ? v : void 0;
|
|
3000
|
+
};
|
|
3001
|
+
const list = (k) => (str(k) ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3002
|
+
const raw = str("source");
|
|
3003
|
+
if (!raw) {
|
|
3004
|
+
throw badRequest2(
|
|
3005
|
+
"`source` is required \u2014 one of source=event:<name>, namespace:<ns>, kind:<kind>, family:<as>"
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3008
|
+
const cut = raw.indexOf(":");
|
|
3009
|
+
const form = cut > 0 ? raw.slice(0, cut) : "";
|
|
3010
|
+
const named = cut > 0 ? raw.slice(cut + 1) : "";
|
|
3011
|
+
if (!named || !["event", "namespace", "kind", "family"].includes(form)) {
|
|
3012
|
+
throw badRequest2(
|
|
3013
|
+
`\`source\` must be "event:<name>", "namespace:<ns>", "kind:<kind>" or "family:<as>" \u2014 got "${raw}"`
|
|
3014
|
+
);
|
|
3015
|
+
}
|
|
3016
|
+
const source = form === "event" ? { event: named } : form === "namespace" ? { namespace: named } : form === "kind" ? { kind: named } : { family: named };
|
|
3017
|
+
const shorthand = str("range");
|
|
3018
|
+
const from = str("from");
|
|
3019
|
+
const to = str("to");
|
|
3020
|
+
if (!shorthand && !(from && to)) {
|
|
3021
|
+
throw badRequest2("a range is required \u2014 either `range=7d` or both `from` and `to` as ISO times");
|
|
3022
|
+
}
|
|
3023
|
+
const range = shorthand ?? { from, to };
|
|
3024
|
+
const interval = str("interval");
|
|
3025
|
+
if (interval && INTERVAL_RANK[interval] == null) {
|
|
3026
|
+
throw badRequest2(`\`interval\` must be one of hour, day, week, month \u2014 got "${interval}"`);
|
|
3027
|
+
}
|
|
3028
|
+
const sort = str("sort");
|
|
3029
|
+
if (sort && !SORTS.has(sort)) {
|
|
3030
|
+
throw badRequest2(`\`sort\` must be one of value, label, time \u2014 got "${sort}"`);
|
|
3031
|
+
}
|
|
3032
|
+
const compare = str("compare");
|
|
3033
|
+
if (compare && compare !== "previous") {
|
|
3034
|
+
throw badRequest2(`\`compare\` takes only "previous" \u2014 got "${compare}"`);
|
|
3035
|
+
}
|
|
3036
|
+
const limitRaw = str("limit");
|
|
3037
|
+
const limit = limitRaw == null ? void 0 : Number(limitRaw);
|
|
3038
|
+
if (limit != null && (!Number.isInteger(limit) || limit < 1)) {
|
|
3039
|
+
throw badRequest2(`\`limit\` must be a positive integer \u2014 got "${limitRaw}"`);
|
|
3040
|
+
}
|
|
3041
|
+
const measure = str("measure");
|
|
3042
|
+
const groupBy = list("groupBy");
|
|
3043
|
+
const excludeActorTypes = list("excludeActors");
|
|
3044
|
+
const stages = list("stages");
|
|
3045
|
+
const exits = list("exits");
|
|
3046
|
+
const anchor = str("anchor");
|
|
3047
|
+
const subjectType = str("subjectType");
|
|
3048
|
+
const filters = (q.filter == null ? [] : Array.isArray(q.filter) ? q.filter.map(String) : [String(q.filter)]).map(parseFilterTerm);
|
|
3049
|
+
return {
|
|
3050
|
+
source,
|
|
3051
|
+
range,
|
|
3052
|
+
...interval ? { interval } : {},
|
|
3053
|
+
...measure ? { measure } : {},
|
|
3054
|
+
...groupBy.length ? { groupBy } : {},
|
|
3055
|
+
...filters.length ? { filters } : {},
|
|
3056
|
+
...excludeActorTypes.length ? { excludeActorTypes } : {},
|
|
3057
|
+
...sort ? { sort } : {},
|
|
3058
|
+
...limit != null ? { limit } : {},
|
|
3059
|
+
...compare ? { compare: "previous" } : {},
|
|
3060
|
+
...stages.length ? { stages } : {},
|
|
3061
|
+
...anchor ? { anchor } : {},
|
|
3062
|
+
...exits.length ? { exits } : {},
|
|
3063
|
+
...subjectType ? { subjectType } : {}
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
function parseFilterTerm(term) {
|
|
3067
|
+
const parts = term.split(":");
|
|
3068
|
+
const i = parts.findIndex((p) => FILTER_OPS.has(p));
|
|
3069
|
+
const rest = i < 0 ? "" : parts.slice(i + 1).join(":");
|
|
3070
|
+
if (i < 1 || !rest) {
|
|
3071
|
+
throw badRequest2(
|
|
3072
|
+
`\`filter\` must be "<dim>:<op>:<value>" with op one of eq, in, gte, lte \u2014 got "${term}"`
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
const dim2 = parts.slice(0, i).join(":");
|
|
3076
|
+
const op = parts[i];
|
|
3077
|
+
if (op === "in") {
|
|
3078
|
+
const values = rest.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3079
|
+
if (!values.length) throw badRequest2(`\`filter\` "${term}" has an empty \`in\` list`);
|
|
3080
|
+
return { dim: dim2, op, value: values };
|
|
3081
|
+
}
|
|
3082
|
+
if (op === "gte" || op === "lte") {
|
|
3083
|
+
const n = Number(rest);
|
|
3084
|
+
if (Number.isNaN(n)) throw badRequest2(`\`filter\` bound "${term}" is not a number`);
|
|
3085
|
+
return { dim: dim2, op, value: n };
|
|
3086
|
+
}
|
|
3087
|
+
return { dim: dim2, op, value: rest };
|
|
3088
|
+
}
|
|
3089
|
+
function reportToQuery(report) {
|
|
3090
|
+
const s = report.source;
|
|
3091
|
+
const q = {
|
|
3092
|
+
source: "event" in s ? `event:${s.event}` : "namespace" in s ? `namespace:${s.namespace}` : "kind" in s ? `kind:${s.kind}` : `family:${s.family}`
|
|
3093
|
+
};
|
|
3094
|
+
if (typeof report.range === "string") q.range = report.range;
|
|
3095
|
+
else {
|
|
3096
|
+
q.from = report.range.from;
|
|
3097
|
+
q.to = report.range.to;
|
|
3098
|
+
}
|
|
3099
|
+
if (report.interval) q.interval = report.interval;
|
|
3100
|
+
if (report.measure) q.measure = report.measure;
|
|
3101
|
+
if (report.groupBy?.length) q.groupBy = report.groupBy.join(",");
|
|
3102
|
+
if (report.filters?.length) {
|
|
3103
|
+
const terms = report.filters.map(
|
|
3104
|
+
(f) => `${f.dim}:${f.op}:${Array.isArray(f.value) ? f.value.join(",") : String(f.value)}`
|
|
3105
|
+
);
|
|
3106
|
+
q.filter = terms.length === 1 ? terms[0] : terms;
|
|
3107
|
+
}
|
|
3108
|
+
if (report.excludeActorTypes?.length) q.excludeActors = report.excludeActorTypes.join(",");
|
|
3109
|
+
if (report.sort) q.sort = report.sort;
|
|
3110
|
+
if (report.limit != null) q.limit = String(report.limit);
|
|
3111
|
+
if (report.compare) q.compare = report.compare;
|
|
3112
|
+
if (report.stages?.length) q.stages = report.stages.join(",");
|
|
3113
|
+
if (report.anchor) q.anchor = report.anchor;
|
|
3114
|
+
if (report.exits?.length) q.exits = report.exits.join(",");
|
|
3115
|
+
if (report.subjectType) q.subjectType = report.subjectType;
|
|
3116
|
+
return q;
|
|
3117
|
+
}
|
|
3118
|
+
var LEGACY_DIMS = {
|
|
3119
|
+
kind: "field:kind",
|
|
3120
|
+
name: "field:name",
|
|
3121
|
+
severity: "field:severity",
|
|
3122
|
+
env: "field:env",
|
|
3123
|
+
service: "field:service",
|
|
3124
|
+
release: "field:release",
|
|
3125
|
+
subject: "field:subject",
|
|
3126
|
+
traceId: "field:traceId"
|
|
3127
|
+
};
|
|
3128
|
+
function normalizeQuery(query) {
|
|
3129
|
+
if (!query || typeof query !== "object") return null;
|
|
3130
|
+
if ("source" in query && query.source) return query;
|
|
3131
|
+
const q = query;
|
|
3132
|
+
const filters = q.filters ?? {};
|
|
3133
|
+
const str = (v) => typeof v === "string" && v ? v : null;
|
|
3134
|
+
const name = str(filters.name);
|
|
3135
|
+
const family = str(filters.rollup);
|
|
3136
|
+
const kind = str(filters.kind);
|
|
3137
|
+
const source = name ? { event: name } : family ? { family } : kind ? { kind } : null;
|
|
3138
|
+
if (!source) return null;
|
|
3139
|
+
const consumed = name ? "name" : family ? "rollup" : "kind";
|
|
3140
|
+
const terms = [];
|
|
3141
|
+
for (const [k, v] of Object.entries(filters)) {
|
|
3142
|
+
if (k === consumed || k === "rollup") continue;
|
|
3143
|
+
if (k === "excludeActorTypes") continue;
|
|
3144
|
+
if (k === "attrs") {
|
|
3145
|
+
const entries = typeof v === "string" ? v.split(",").map((pair) => pair.split(":").map((s) => s.trim())) : Object.entries(v ?? {}).map(([a, b]) => [a, String(b)]);
|
|
3146
|
+
for (const [key, value2] of entries) {
|
|
3147
|
+
if (key && value2 != null) terms.push({ dim: `attr:${key}`, op: "eq", value: String(value2) });
|
|
3148
|
+
}
|
|
3149
|
+
continue;
|
|
3150
|
+
}
|
|
3151
|
+
const dim2 = LEGACY_DIMS[k];
|
|
3152
|
+
const value = str(v);
|
|
3153
|
+
if (dim2 && value) terms.push({ dim: dim2, op: "eq", value });
|
|
3154
|
+
}
|
|
3155
|
+
const groupBy = (q.groupBy ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3156
|
+
const sort = q.sort === "value" || q.sort === "label" || q.sort === "time" ? q.sort : void 0;
|
|
3157
|
+
const actors = filters.excludeActorTypes;
|
|
3158
|
+
return {
|
|
3159
|
+
source,
|
|
3160
|
+
range: q.range ?? "7d",
|
|
3161
|
+
...terms.length ? { filters: terms } : {},
|
|
3162
|
+
...groupBy.length ? { groupBy } : {},
|
|
3163
|
+
...sort ? { sort } : {},
|
|
3164
|
+
...Array.isArray(actors) && actors.length ? { excludeActorTypes: actors.map(String) } : {}
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
// src/server/execute.ts
|
|
3169
|
+
async function executeReport(q, scope, report, catalog, opts = {}) {
|
|
3170
|
+
const plan = resolveReport(report, catalog, { now: opts.now, limits: opts.limits });
|
|
3171
|
+
if ("unavailable" in plan) throw Object.assign(new Error(plan.why), { status: 400 });
|
|
3172
|
+
const run = async (args) => {
|
|
3173
|
+
const raw = await q[plan.primitive](scope, ...args);
|
|
3174
|
+
if (plan.primitive === "rollups" && plan.shape) {
|
|
3175
|
+
return foldRollups(raw?.rows ?? [], plan.shape, !!raw?.truncated);
|
|
3176
|
+
}
|
|
3177
|
+
if (plan.primitive === "records" && opts.redact) {
|
|
3178
|
+
return { ...raw, items: opts.redact(raw?.items ?? []) };
|
|
3179
|
+
}
|
|
3180
|
+
return raw;
|
|
3181
|
+
};
|
|
3182
|
+
const [result, previous] = await Promise.all([
|
|
3183
|
+
run(plan.args),
|
|
3184
|
+
plan.previous ? run(plan.previous.args) : Promise.resolve(void 0)
|
|
3185
|
+
]);
|
|
3186
|
+
return {
|
|
3187
|
+
report,
|
|
3188
|
+
plan,
|
|
3189
|
+
result,
|
|
3190
|
+
...plan.previous ? { previous } : {},
|
|
3191
|
+
dataSource: result?.dataSource ?? "raw"
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
var MEASURE_OP2 = /^(sum|avg):(.+)$/;
|
|
3195
|
+
function foldRollups(rows, shape, truncated = false) {
|
|
3196
|
+
const op = MEASURE_OP2.exec(shape.measure);
|
|
3197
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3198
|
+
for (const doc of rows) {
|
|
3199
|
+
const dims = doc?.dims ?? [];
|
|
3200
|
+
if (!(shape.filters ?? []).every((f) => admits(f, dimValue(dims, f.label)))) continue;
|
|
3201
|
+
const tuple = shape.labels.map((label3) => dimValue(dims, label3));
|
|
3202
|
+
const at = shape.interval && doc.bucketAt ? truncate(new Date(doc.bucketAt), shape.interval) : void 0;
|
|
3203
|
+
const key = `${JSON.stringify(tuple)}|${at ? at.getTime() : ""}`;
|
|
3204
|
+
let g = groups.get(key);
|
|
3205
|
+
if (!g) groups.set(key, g = { dims: tuple, ...at ? { at } : {}, sum: 0, count: 0 });
|
|
3206
|
+
g.count += typeof doc.count === "number" ? doc.count : 0;
|
|
3207
|
+
if (op) g.sum += sumOf(doc.sums, op[2]);
|
|
3208
|
+
}
|
|
3209
|
+
const rowsOut = [...groups.values()].map((g) => ({
|
|
3210
|
+
dims: g.dims,
|
|
3211
|
+
...g.at ? { at: g.at } : {},
|
|
3212
|
+
// avg is sums[k]/count off the SAME doc, which is exact — not an average of
|
|
3213
|
+
// averages, which is what folding a per-bucket mean would have produced
|
|
3214
|
+
value: !op ? g.count : op[1] === "sum" ? g.sum : g.count ? g.sum / g.count : 0
|
|
3215
|
+
}));
|
|
3216
|
+
rowsOut.sort(
|
|
3217
|
+
shape.interval ? (a, b) => (a.at?.getTime() ?? 0) - (b.at?.getTime() ?? 0) || byDims(a, b) : (a, b) => b.value - a.value || byDims(a, b)
|
|
3218
|
+
);
|
|
3219
|
+
return {
|
|
3220
|
+
rows: rowsOut,
|
|
3221
|
+
groups: new Set([...groups.values()].map((g) => JSON.stringify(g.dims))).size,
|
|
3222
|
+
truncated,
|
|
3223
|
+
dataSource: "rollups"
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
function dimValue(dims, label3) {
|
|
3227
|
+
const prefix = `${label3}=`;
|
|
3228
|
+
for (const d of dims) if (d.startsWith(prefix)) return d.slice(prefix.length);
|
|
3229
|
+
for (const d of dims) if (!d.includes("=")) return d;
|
|
3230
|
+
return null;
|
|
3231
|
+
}
|
|
3232
|
+
function admits(f, value) {
|
|
3233
|
+
if (f.op === "in") return f.value.map(String).includes(String(value));
|
|
3234
|
+
if (f.op === "gte" || f.op === "lte") {
|
|
3235
|
+
const n = Number(value);
|
|
3236
|
+
if (Number.isNaN(n)) return false;
|
|
3237
|
+
return f.op === "gte" ? n >= Number(f.value) : n <= Number(f.value);
|
|
3238
|
+
}
|
|
3239
|
+
return String(value) === String(f.value);
|
|
3240
|
+
}
|
|
3241
|
+
function sumOf(sums, key) {
|
|
3242
|
+
if (!sums) return 0;
|
|
3243
|
+
const v = sums instanceof Map ? sums.get(key) : sums[key];
|
|
3244
|
+
return typeof v === "number" ? v : 0;
|
|
3245
|
+
}
|
|
3246
|
+
function byDims(a, b) {
|
|
3247
|
+
for (let i = 0; i < a.dims.length; i++) {
|
|
3248
|
+
const x = a.dims[i] ?? "";
|
|
3249
|
+
const y = b.dims[i] ?? "";
|
|
3250
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
3251
|
+
}
|
|
3252
|
+
return 0;
|
|
3253
|
+
}
|
|
1839
3254
|
function buildViewModel(connection, modelName, collection) {
|
|
1840
3255
|
const existing = connection.models?.[modelName];
|
|
1841
3256
|
if (existing) return existing;
|
|
@@ -1862,31 +3277,58 @@ var KIND_PAGE = {
|
|
|
1862
3277
|
state: "journeys",
|
|
1863
3278
|
usage: "usage"
|
|
1864
3279
|
};
|
|
1865
|
-
function deriveViews(registry) {
|
|
3280
|
+
function deriveViews(registry, catalog = deriveCatalog(registry)) {
|
|
1866
3281
|
const views = [];
|
|
1867
|
-
const
|
|
1868
|
-
for (const [name,
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
query: { range: "7d", filters: { name }, display: spec.kind === "event" ? "series" : "table" }
|
|
3282
|
+
const derived = (name, page, query) => views.push({ origin: "derived", name, page, query });
|
|
3283
|
+
for (const [name, e] of Object.entries(catalog.events)) {
|
|
3284
|
+
derived(name, KIND_PAGE[e.kind] ?? "events", {
|
|
3285
|
+
source: { event: name },
|
|
3286
|
+
range: "7d",
|
|
3287
|
+
interval: intervalForRange("7d")
|
|
1874
3288
|
});
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
3289
|
+
}
|
|
3290
|
+
for (const as of Object.keys(catalog.families)) {
|
|
3291
|
+
derived(`rollup: ${as}`, "journeys", { source: { family: as }, range: "30d" });
|
|
3292
|
+
}
|
|
3293
|
+
for (const [ns, names] of Object.entries(catalog.namespaces)) {
|
|
3294
|
+
if (names.length < 2) continue;
|
|
3295
|
+
derived(`namespace: ${ns}`, "explore", {
|
|
3296
|
+
source: { namespace: ns },
|
|
3297
|
+
range: "30d",
|
|
3298
|
+
interval: "day",
|
|
3299
|
+
groupBy: ["field:name"]
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
for (const [name, e] of Object.entries(catalog.events)) {
|
|
3303
|
+
if (e.kind !== "usage") continue;
|
|
3304
|
+
const money = e.measures.find((m) => m.key.startsWith("sum:") && m.key.endsWith("_usd"));
|
|
3305
|
+
if (!money) continue;
|
|
3306
|
+
derived(`spend: ${name}`, "usage", {
|
|
3307
|
+
source: { event: name },
|
|
3308
|
+
range: "30d",
|
|
3309
|
+
interval: "day",
|
|
3310
|
+
measure: money.key
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
for (const subjectType of catalog.subjectTypes) {
|
|
3314
|
+
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);
|
|
3315
|
+
if (stages.length < 2) continue;
|
|
3316
|
+
derived(`funnel: ${subjectType}`, "journeys", {
|
|
3317
|
+
// any source expands; the family the funnel is anchored on is the honest one
|
|
3318
|
+
source: { family: stages[0] },
|
|
3319
|
+
range: "30d",
|
|
3320
|
+
interval: "week",
|
|
3321
|
+
measure: "funnel",
|
|
3322
|
+
stages,
|
|
3323
|
+
anchor: stages[0],
|
|
3324
|
+
subjectType
|
|
1883
3325
|
});
|
|
1884
3326
|
}
|
|
1885
3327
|
return views;
|
|
1886
3328
|
}
|
|
1887
3329
|
async function resolveViews(opts) {
|
|
1888
3330
|
const byName = /* @__PURE__ */ new Map();
|
|
1889
|
-
for (const v of deriveViews(opts.registry)) byName.set(v.name, v);
|
|
3331
|
+
for (const v of deriveViews(opts.registry, opts.catalog)) byName.set(v.name, v);
|
|
1890
3332
|
for (const v of opts.configured) byName.set(v.name, { ...v, origin: "configured" });
|
|
1891
3333
|
const saved = await opts.ViewModel.find({
|
|
1892
3334
|
tenantId: opts.tenantId,
|
|
@@ -1945,6 +3387,10 @@ var parseFilter = (q) => {
|
|
|
1945
3387
|
for (const k of ["kind", "name", "severity", "env", "service", "release", "subject", "traceId"]) {
|
|
1946
3388
|
if (typeof q[k] === "string" && q[k]) f[k] = q[k];
|
|
1947
3389
|
}
|
|
3390
|
+
if (typeof q.name === "string" && q.name.includes(",")) {
|
|
3391
|
+
const names = q.name.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3392
|
+
if (names.length) f.name = names.length === 1 ? names[0] : names;
|
|
3393
|
+
}
|
|
1948
3394
|
if (typeof q.attrs === "string" && q.attrs) {
|
|
1949
3395
|
f.attrs = Object.fromEntries(
|
|
1950
3396
|
String(q.attrs).split(",").map((p) => p.split(":")).filter((p) => p.length >= 2).map(([k, ...v]) => [k, v.join(":")])
|
|
@@ -1976,30 +3422,6 @@ var parseDims = (v) => {
|
|
|
1976
3422
|
}
|
|
1977
3423
|
return v.length ? v : void 0;
|
|
1978
3424
|
};
|
|
1979
|
-
function registryProjection(t) {
|
|
1980
|
-
return Object.fromEntries(
|
|
1981
|
-
Object.entries(t.registry).map(([name, spec]) => [
|
|
1982
|
-
name,
|
|
1983
|
-
{
|
|
1984
|
-
kind: spec.kind,
|
|
1985
|
-
origin: spec.origin,
|
|
1986
|
-
subjects: spec.subjects,
|
|
1987
|
-
description: spec.description,
|
|
1988
|
-
attrKeys: spec.attrs ? Object.keys(spec.attrs.shape) : [],
|
|
1989
|
-
metricKeys: spec.metrics ? Object.keys(spec.metrics.shape) : [],
|
|
1990
|
-
indexedAttrs: spec.indexedAttrs ?? [],
|
|
1991
|
-
indexedMetrics: spec.indexedMetrics ?? [],
|
|
1992
|
-
rollups: (spec.rollups ?? []).map((r) => ({
|
|
1993
|
-
as: r.as ?? name,
|
|
1994
|
-
by: r.by,
|
|
1995
|
-
bucket: r.bucket ?? null,
|
|
1996
|
-
sum: r.sum ?? [],
|
|
1997
|
-
subjects: r.subjects ?? []
|
|
1998
|
-
}))
|
|
1999
|
-
}
|
|
2000
|
-
])
|
|
2001
|
-
);
|
|
2002
|
-
}
|
|
2003
3425
|
function createDashboard(opts) {
|
|
2004
3426
|
const { telemetry: t, viewerAdapter, subjectAdapter, views: configured = [] } = opts;
|
|
2005
3427
|
if (!viewerAdapter?.resolveViewer) {
|
|
@@ -2029,6 +3451,20 @@ function createDashboard(opts) {
|
|
|
2029
3451
|
cacheTtlMs: opts.cacheTtlMs,
|
|
2030
3452
|
cacheSize: opts.cacheSize
|
|
2031
3453
|
});
|
|
3454
|
+
const catalog = deriveCatalog(t.registry, {
|
|
3455
|
+
platforms: t.models.telemetry.schema.path("client")?.schema?.path("platform")?.enumValues
|
|
3456
|
+
});
|
|
3457
|
+
const registry = projectRegistry(catalog);
|
|
3458
|
+
const values = createValues({
|
|
3459
|
+
catalog,
|
|
3460
|
+
TelemetryModel: t.models.telemetry,
|
|
3461
|
+
RollupModel: t.models.rollups,
|
|
3462
|
+
limits: opts.queryLimits,
|
|
3463
|
+
onSlowQuery: opts.onSlowQuery,
|
|
3464
|
+
slowMs: opts.slowMs,
|
|
3465
|
+
cacheTtlMs: opts.cacheTtlMs,
|
|
3466
|
+
cacheSize: opts.cacheSize
|
|
3467
|
+
});
|
|
2032
3468
|
const api = express2__default.default.Router();
|
|
2033
3469
|
api.use(express2__default.default.json({ limit: "64kb" }));
|
|
2034
3470
|
api.use(async (req, res, next) => {
|
|
@@ -2047,7 +3483,8 @@ function createDashboard(opts) {
|
|
|
2047
3483
|
}, next);
|
|
2048
3484
|
};
|
|
2049
3485
|
api.get("/registry", h(async (req) => ({
|
|
2050
|
-
registry
|
|
3486
|
+
registry,
|
|
3487
|
+
catalog,
|
|
2051
3488
|
kinds: ["event", "error", "span", "state", "usage"],
|
|
2052
3489
|
role: req.viewer.role,
|
|
2053
3490
|
scope: req.viewer.tenantId,
|
|
@@ -2070,6 +3507,14 @@ function createDashboard(opts) {
|
|
|
2070
3507
|
measure: typeof req.query.measure === "string" ? req.query.measure : void 0
|
|
2071
3508
|
})
|
|
2072
3509
|
));
|
|
3510
|
+
api.get("/breakdown", h(
|
|
3511
|
+
async (req) => q.breakdown(req.viewer.tenantId, parseRange(req.query), parseFilter(req.query), {
|
|
3512
|
+
groupBy: String(req.query.groupBy ?? "").split(",").map((s) => s.trim()).filter(Boolean),
|
|
3513
|
+
measure: typeof req.query.measure === "string" ? req.query.measure : void 0,
|
|
3514
|
+
interval: req.query.interval || void 0,
|
|
3515
|
+
limit: req.query.limit ? Number(req.query.limit) : void 0
|
|
3516
|
+
})
|
|
3517
|
+
));
|
|
2073
3518
|
api.get("/rollups", h(async (req) => {
|
|
2074
3519
|
if (typeof req.query.as !== "string" || !req.query.as) {
|
|
2075
3520
|
throw Object.assign(new Error("rollup family required"), { status: 400 });
|
|
@@ -2100,7 +3545,7 @@ function createDashboard(opts) {
|
|
|
2100
3545
|
limit: req.query.limit ? Number(req.query.limit) : void 0
|
|
2101
3546
|
})
|
|
2102
3547
|
));
|
|
2103
|
-
const
|
|
3548
|
+
const badRequest3 = async (run) => {
|
|
2104
3549
|
try {
|
|
2105
3550
|
return await run();
|
|
2106
3551
|
} catch (e) {
|
|
@@ -2113,7 +3558,7 @@ function createDashboard(opts) {
|
|
|
2113
3558
|
if (!stages.length) {
|
|
2114
3559
|
throw Object.assign(new Error("funnel needs `stages` \u2014 a comma-separated list of rollup families"), { status: 400 });
|
|
2115
3560
|
}
|
|
2116
|
-
return
|
|
3561
|
+
return badRequest3(() => q.funnel(req.viewer.tenantId, {
|
|
2117
3562
|
stages,
|
|
2118
3563
|
exits: parseStages(req.query.exits),
|
|
2119
3564
|
anchor: typeof req.query.anchor === "string" ? req.query.anchor : void 0,
|
|
@@ -2128,22 +3573,46 @@ function createDashboard(opts) {
|
|
|
2128
3573
|
if (typeof req.query.as !== "string" || !req.query.as) {
|
|
2129
3574
|
throw Object.assign(new Error("rollup family required"), { status: 400 });
|
|
2130
3575
|
}
|
|
2131
|
-
return
|
|
3576
|
+
return badRequest3(() => q.distinctCount(req.viewer.tenantId, {
|
|
2132
3577
|
as: req.query.as,
|
|
2133
3578
|
subjectType: typeof req.query.subjectType === "string" ? req.query.subjectType : void 0,
|
|
2134
3579
|
range: parseRange(req.query),
|
|
2135
3580
|
interval: req.query.interval || void 0
|
|
2136
3581
|
}));
|
|
2137
3582
|
}));
|
|
3583
|
+
api.get("/values", h(async (req) => {
|
|
3584
|
+
const dim2 = typeof req.query.dim === "string" ? req.query.dim.trim() : "";
|
|
3585
|
+
if (!dim2) {
|
|
3586
|
+
throw Object.assign(new Error("dim required"), { status: 400 });
|
|
3587
|
+
}
|
|
3588
|
+
const names = String(req.query.names ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3589
|
+
return values(req.viewer.tenantId, {
|
|
3590
|
+
dim: dim2,
|
|
3591
|
+
names: names.length ? names : void 0,
|
|
3592
|
+
range: req.query.from || req.query.to ? parseRange(req.query) : void 0,
|
|
3593
|
+
limit: req.query.limit ? Number(req.query.limit) : void 0
|
|
3594
|
+
});
|
|
3595
|
+
}));
|
|
2138
3596
|
api.get("/subjects/describe", h(async (req) => {
|
|
2139
3597
|
const refs = String(req.query.refs ?? "").split(",").filter(Boolean).slice(0, 100);
|
|
2140
3598
|
if (!subjectAdapter) return { refs: {} };
|
|
2141
3599
|
return { refs: await subjectAdapter.describe(refs) };
|
|
2142
3600
|
}));
|
|
3601
|
+
api.get("/report", h(
|
|
3602
|
+
async (req) => badRequest3(
|
|
3603
|
+
() => executeReport(q, req.viewer.tenantId, parseReportQuery(req.query), catalog)
|
|
3604
|
+
)
|
|
3605
|
+
));
|
|
3606
|
+
api.get("/report/plan", h(
|
|
3607
|
+
async (req) => badRequest3(
|
|
3608
|
+
async () => resolveReport(parseReportQuery(req.query), catalog)
|
|
3609
|
+
)
|
|
3610
|
+
));
|
|
2143
3611
|
api.get("/views", h(async (req) => ({
|
|
2144
3612
|
views: await resolveViews({
|
|
2145
3613
|
ViewModel,
|
|
2146
3614
|
registry: t.registry,
|
|
3615
|
+
catalog,
|
|
2147
3616
|
configured,
|
|
2148
3617
|
tenantId: req.viewer.tenantId,
|
|
2149
3618
|
viewerRef: req.viewer.viewerRef
|
|
@@ -2186,7 +3655,13 @@ function createDashboard(opts) {
|
|
|
2186
3655
|
indexCount: indexes.length,
|
|
2187
3656
|
indexBudget: INDEX_BUDGET,
|
|
2188
3657
|
keys,
|
|
2189
|
-
role: req.viewer.role
|
|
3658
|
+
role: req.viewer.role,
|
|
3659
|
+
// The same three sources, read the other way round: what the data says
|
|
3660
|
+
// the registry is missing, each with the line that would fix it. Derived
|
|
3661
|
+
// from the counters and the quarantine ALREADY fetched above, so the
|
|
3662
|
+
// page costs no extra read. Nothing is written — the host still edits
|
|
3663
|
+
// the registry by hand (reports §9).
|
|
3664
|
+
suggestions: deriveSuggestions({ counters: t.counters, catalog, quarantine })
|
|
2190
3665
|
};
|
|
2191
3666
|
}));
|
|
2192
3667
|
api.post("/system/keys/:id/revoke", h(async (req, res) => {
|
|
@@ -2275,7 +3750,22 @@ function createTelemetry(config) {
|
|
|
2275
3750
|
inFlight.add(p);
|
|
2276
3751
|
void p.finally(() => inFlight.delete(p));
|
|
2277
3752
|
};
|
|
2278
|
-
const
|
|
3753
|
+
const linkSubjects = createSubjectLinking({
|
|
3754
|
+
linker: config.subjectLinker,
|
|
3755
|
+
timeoutMs: config.subjectLinkTimeoutMs,
|
|
3756
|
+
counters,
|
|
3757
|
+
logger
|
|
3758
|
+
});
|
|
3759
|
+
const emit = createEmitter({
|
|
3760
|
+
registry,
|
|
3761
|
+
byKind,
|
|
3762
|
+
RollupModel,
|
|
3763
|
+
rejects,
|
|
3764
|
+
counters,
|
|
3765
|
+
logger,
|
|
3766
|
+
track,
|
|
3767
|
+
linkSubjects
|
|
3768
|
+
});
|
|
2279
3769
|
const forget = createForget({
|
|
2280
3770
|
TelemetryModel,
|
|
2281
3771
|
RollupModel,
|
|
@@ -2338,6 +3828,17 @@ function createTelemetry(config) {
|
|
|
2338
3828
|
counters,
|
|
2339
3829
|
/** the registry, exposed for the router factories — hosts should import their own */
|
|
2340
3830
|
registry,
|
|
3831
|
+
/**
|
|
3832
|
+
* Write-time subject linking, exposed for the router factories. `null` when
|
|
3833
|
+
* no `subjectLinker` is configured.
|
|
3834
|
+
*
|
|
3835
|
+
* The wire path does not go through emit() — createIngest() builds its
|
|
3836
|
+
* record itself, because at-least-once delivery inverts the plane order
|
|
3837
|
+
* (insert first, THEN aggregate). So it reaches the linker the same way it
|
|
3838
|
+
* reaches the registry and the models: off the instance, running the one
|
|
3839
|
+
* implementation, rather than growing a second copy of the rules.
|
|
3840
|
+
*/
|
|
3841
|
+
linkSubjects,
|
|
2341
3842
|
logger,
|
|
2342
3843
|
/** mint an ingest key; the full key string is returned once, never again */
|
|
2343
3844
|
createKey: (input) => createKey(KeyModel, input),
|
|
@@ -2355,16 +3856,21 @@ function createTelemetry(config) {
|
|
|
2355
3856
|
}
|
|
2356
3857
|
|
|
2357
3858
|
exports.BODY_MAX_CHARS = BODY_MAX_CHARS;
|
|
3859
|
+
exports.COUNTER_MAP_MAX = COUNTER_MAP_MAX;
|
|
3860
|
+
exports.COUNTER_OVERFLOW_KEY = COUNTER_OVERFLOW_KEY;
|
|
2358
3861
|
exports.DEFAULT_LIMITS = DEFAULT_LIMITS;
|
|
2359
3862
|
exports.Env = Env;
|
|
2360
3863
|
exports.INDEX_BUDGET = INDEX_BUDGET;
|
|
2361
3864
|
exports.KeyKind = KeyKind;
|
|
2362
3865
|
exports.LogLevel = LogLevel;
|
|
3866
|
+
exports.MAX_SUGGESTIONS = MAX_SUGGESTIONS;
|
|
2363
3867
|
exports.Origin = Origin;
|
|
2364
3868
|
exports.PLATFORM_SCOPE = PLATFORM_SCOPE;
|
|
2365
3869
|
exports.RETENTION_DAYS = RETENTION_DAYS;
|
|
2366
3870
|
exports.SAMPLE_RATE = SAMPLE_RATE;
|
|
2367
3871
|
exports.SCHEMA_VERSION = SCHEMA_VERSION;
|
|
3872
|
+
exports.SUBJECT_LINK_TIMEOUT_MS = SUBJECT_LINK_TIMEOUT_MS;
|
|
3873
|
+
exports.SUBJECT_MAX = SUBJECT_MAX;
|
|
2368
3874
|
exports.TelemetryKind = TelemetryKind;
|
|
2369
3875
|
exports.TenantMode = TenantMode;
|
|
2370
3876
|
exports.boundedMeta = boundedMeta;
|
|
@@ -2373,18 +3879,30 @@ exports.createIngest = createIngest;
|
|
|
2373
3879
|
exports.createKey = createKey;
|
|
2374
3880
|
exports.createQueries = createQueries;
|
|
2375
3881
|
exports.createTelemetry = createTelemetry;
|
|
3882
|
+
exports.createValues = createValues;
|
|
2376
3883
|
exports.defaultSpaDir = defaultSpaDir;
|
|
2377
3884
|
exports.defineRegistry = defineRegistry;
|
|
3885
|
+
exports.deriveCatalog = deriveCatalog;
|
|
3886
|
+
exports.deriveSuggestions = deriveSuggestions;
|
|
2378
3887
|
exports.deriveViews = deriveViews;
|
|
3888
|
+
exports.executeReport = executeReport;
|
|
2379
3889
|
exports.findFamily = findFamily;
|
|
3890
|
+
exports.foldRollups = foldRollups;
|
|
2380
3891
|
exports.hashSecret = hashSecret;
|
|
3892
|
+
exports.intervalForRange = intervalForRange;
|
|
2381
3893
|
exports.isPlatformScope = isPlatformScope;
|
|
2382
3894
|
exports.median = median;
|
|
2383
3895
|
exports.newId = newId;
|
|
3896
|
+
exports.normalizeQuery = normalizeQuery;
|
|
2384
3897
|
exports.parseKeyString = parseKeyString;
|
|
3898
|
+
exports.parseReportQuery = parseReportQuery;
|
|
2385
3899
|
exports.plain = plain;
|
|
3900
|
+
exports.projectRegistry = projectRegistry;
|
|
3901
|
+
exports.rangeOf = rangeOf;
|
|
3902
|
+
exports.reportToQuery = reportToQuery;
|
|
2386
3903
|
exports.requireMilestoneFamily = requireMilestoneFamily;
|
|
2387
3904
|
exports.resolveDim = resolveDim;
|
|
3905
|
+
exports.resolveReport = resolveReport;
|
|
2388
3906
|
exports.summarizeStages = summarizeStages;
|
|
2389
3907
|
exports.traceKeep = traceKeep;
|
|
2390
3908
|
exports.truncate = truncate;
|