@jeffjassky/telemetry 0.4.0 → 0.6.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/dist/index.cjs +319 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +317 -46
- package/dist/index.js.map +1 -1
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/ui/_assets/index-kvwB9_3A.js +41 -0
- package/dist/ui/_assets/{index-Deqhu-hT.js.map → index-kvwB9_3A.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/types/index.d.ts +162 -0
- package/types/test-d.ts +38 -0
- package/dist/ui/_assets/index-Deqhu-hT.js +0 -41
package/dist/index.cjs
CHANGED
|
@@ -70,7 +70,13 @@ var newCounters = () => ({
|
|
|
70
70
|
deduped: 0,
|
|
71
71
|
truncated: 0,
|
|
72
72
|
rollupSkippedBy: {},
|
|
73
|
-
undeclaredAttrs: {}
|
|
73
|
+
undeclaredAttrs: {},
|
|
74
|
+
subjectsLinked: 0,
|
|
75
|
+
subjectLinkMisses: 0,
|
|
76
|
+
subjectLinkErrors: 0,
|
|
77
|
+
subjectLinkTimeouts: 0,
|
|
78
|
+
subjectLinkUndeclared: 0,
|
|
79
|
+
subjectLinkCapped: 0
|
|
74
80
|
});
|
|
75
81
|
var COUNTER_MAP_MAX = 1e3;
|
|
76
82
|
var COUNTER_OVERFLOW_KEY = "(other)|(other)";
|
|
@@ -554,10 +560,11 @@ var truncate = (d, b) => {
|
|
|
554
560
|
if (b === "day") return day;
|
|
555
561
|
return new Date(day.getTime() - (day.getUTCDay() + 6) % 7 * 864e5);
|
|
556
562
|
};
|
|
557
|
-
async function recordRollup(RollupModel, doc, name, spec, counters) {
|
|
563
|
+
async function recordRollup(RollupModel, doc, name, spec, counters, opts) {
|
|
564
|
+
const dry = opts?.dryRun === true;
|
|
558
565
|
if (spec.actors && doc.actor) {
|
|
559
566
|
const actorType = String(doc.actor).split(":")[0];
|
|
560
|
-
if (!spec.actors.includes(actorType)) return;
|
|
567
|
+
if (!spec.actors.includes(actorType)) return 0;
|
|
561
568
|
}
|
|
562
569
|
const as = spec.as ?? name;
|
|
563
570
|
const at = doc.occurredAt;
|
|
@@ -569,9 +576,11 @@ async function recordRollup(RollupModel, doc, name, spec, counters) {
|
|
|
569
576
|
let v = resolveDim(src, doc);
|
|
570
577
|
if (v == null || v === "") {
|
|
571
578
|
if (spec.dimDefault === void 0) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
579
|
+
if (!dry) {
|
|
580
|
+
counters.rollupSkipped++;
|
|
581
|
+
bumpCounterMap(counters.rollupSkippedBy, `${as}|${label(src)}`);
|
|
582
|
+
}
|
|
583
|
+
return 0;
|
|
575
584
|
}
|
|
576
585
|
v = spec.dimDefault;
|
|
577
586
|
}
|
|
@@ -581,48 +590,48 @@ async function recordRollup(RollupModel, doc, name, spec, counters) {
|
|
|
581
590
|
const refs = fansOut ? (doc.subjectKeys ?? []).filter(
|
|
582
591
|
(r) => !spec.subjects || spec.subjects.includes(r.split(":")[0])
|
|
583
592
|
) : [null];
|
|
584
|
-
if (!refs.length) return;
|
|
593
|
+
if (!refs.length) return 0;
|
|
585
594
|
const firstCapture = Object.fromEntries(
|
|
586
595
|
(spec.capture ?? []).map((src) => [label(src), resolveDim(src, doc)]).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])
|
|
587
596
|
);
|
|
588
597
|
const expiresAt = spec.retentionDays != null ? new Date(at.getTime() + spec.retentionDays * 864e5) : void 0;
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
{
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
firstCapture: { $cond: [isNewFirst, { $literal: firstCapture }, "$firstCapture"] }
|
|
617
|
-
}
|
|
598
|
+
const ops = refs.map((ref) => {
|
|
599
|
+
const dims = spec.by.map((src) => src === "subject" ? ref : fixed.get(src));
|
|
600
|
+
const isNewFirst = {
|
|
601
|
+
$or: [{ $eq: [{ $type: "$firstAt" }, "missing"] }, { $lt: [at, "$firstAt"] }]
|
|
602
|
+
};
|
|
603
|
+
const sums = Object.fromEntries(
|
|
604
|
+
(spec.sum ?? []).map((k) => [k, doc.metrics?.get(k)]).filter(([, v]) => typeof v === "number").map(([k, v]) => [`sums.${k}`, { $add: [{ $ifNull: [`$sums.${k}`, 0] }, v] }])
|
|
605
|
+
);
|
|
606
|
+
return {
|
|
607
|
+
updateOne: {
|
|
608
|
+
filter: { _id: `${doc.tenantId}|${as}|${dims.join("|")}|${bucketKey}` },
|
|
609
|
+
update: [
|
|
610
|
+
{
|
|
611
|
+
$set: {
|
|
612
|
+
tenantId: doc.tenantId,
|
|
613
|
+
as,
|
|
614
|
+
dims,
|
|
615
|
+
...ref ? { subjectType: ref.split(":")[0] } : {},
|
|
616
|
+
...bucketAt ? { bucketAt } : {},
|
|
617
|
+
...expiresAt ? { expiresAt } : {},
|
|
618
|
+
// aggregation $min/$max ignore missing, so correct on insert too
|
|
619
|
+
firstAt: { $min: ["$firstAt", at] },
|
|
620
|
+
lastAt: { $max: ["$lastAt", at] },
|
|
621
|
+
count: { $add: [{ $ifNull: ["$count", 0] }, 1] },
|
|
622
|
+
...sums,
|
|
623
|
+
firstTraceId: { $cond: [isNewFirst, doc.traceId ?? null, "$firstTraceId"] },
|
|
624
|
+
firstCapture: { $cond: [isNewFirst, { $literal: firstCapture }, "$firstCapture"] }
|
|
618
625
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
);
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
upsert: true
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
});
|
|
632
|
+
if (dry) return ops.length;
|
|
633
|
+
await RollupModel.bulkWrite(ops, { ordered: false });
|
|
634
|
+
return ops.length;
|
|
626
635
|
}
|
|
627
636
|
function buildCheckpointModel(connection, modelName, collection) {
|
|
628
637
|
const existing = connection.models?.[modelName];
|
|
@@ -672,6 +681,110 @@ function noteUndeclaredAttrs(counters, name, spec, attrs) {
|
|
|
672
681
|
bumpCounterMap(counters.undeclaredAttrs, `${name}|${key}`);
|
|
673
682
|
}
|
|
674
683
|
}
|
|
684
|
+
var SUBJECT_MAX = 8;
|
|
685
|
+
var SUBJECT_LINK_TIMEOUT_MS = 50;
|
|
686
|
+
var LINK_TIMEOUT = /* @__PURE__ */ Symbol("telemetry.subjectLink.timeout");
|
|
687
|
+
function createSubjectLinking(opts) {
|
|
688
|
+
const { linker, counters, logger } = opts;
|
|
689
|
+
if (!linker) return null;
|
|
690
|
+
const timeoutMs = opts.timeoutMs ?? SUBJECT_LINK_TIMEOUT_MS;
|
|
691
|
+
const warned = /* @__PURE__ */ new Set();
|
|
692
|
+
const warnOnce = (key, msg) => {
|
|
693
|
+
if (warned.has(key) || warned.size >= COUNTER_MAP_MAX) return;
|
|
694
|
+
warned.add(key);
|
|
695
|
+
logger.warn(msg);
|
|
696
|
+
};
|
|
697
|
+
return async function linkSubjects(name, spec, tenantId, declared) {
|
|
698
|
+
const have = Array.isArray(declared) ? declared : [];
|
|
699
|
+
const seen = /* @__PURE__ */ new Set();
|
|
700
|
+
const view = [];
|
|
701
|
+
for (const s of have) {
|
|
702
|
+
const ref = wellFormed(s);
|
|
703
|
+
if (!ref) continue;
|
|
704
|
+
seen.add(`${ref.type}:${ref.id}`);
|
|
705
|
+
view.push(ref);
|
|
706
|
+
}
|
|
707
|
+
let out;
|
|
708
|
+
let timer;
|
|
709
|
+
try {
|
|
710
|
+
out = await Promise.race([
|
|
711
|
+
// the async wrapper turns a SYNCHRONOUS throw into a rejection, so a
|
|
712
|
+
// linker that dies on its first line lands in the same catch as one
|
|
713
|
+
// whose promise rejects
|
|
714
|
+
(async () => linker.link(view, { name, tenantId }))(),
|
|
715
|
+
new Promise((_, reject) => {
|
|
716
|
+
timer = setTimeout(() => reject(LINK_TIMEOUT), timeoutMs);
|
|
717
|
+
})
|
|
718
|
+
]);
|
|
719
|
+
} catch (e) {
|
|
720
|
+
if (e === LINK_TIMEOUT) {
|
|
721
|
+
counters.subjectLinkTimeouts++;
|
|
722
|
+
warnOnce(
|
|
723
|
+
"timeout",
|
|
724
|
+
`[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.`
|
|
725
|
+
);
|
|
726
|
+
} else {
|
|
727
|
+
counters.subjectLinkErrors++;
|
|
728
|
+
warnOnce(
|
|
729
|
+
"threw",
|
|
730
|
+
`[telemetry] subjectLinker.link() threw \u2014 records are being written unlinked: ${e}. Warned once \u2014 the count is counters.subjectLinkErrors.`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
return null;
|
|
734
|
+
} finally {
|
|
735
|
+
clearTimeout(timer);
|
|
736
|
+
}
|
|
737
|
+
if (!Array.isArray(out)) {
|
|
738
|
+
counters.subjectLinkErrors++;
|
|
739
|
+
warnOnce(
|
|
740
|
+
"shape",
|
|
741
|
+
`[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.`
|
|
742
|
+
);
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
if (!out.length) {
|
|
746
|
+
counters.subjectLinkMisses++;
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
let room = Math.max(0, SUBJECT_MAX - have.length);
|
|
750
|
+
let capped2 = 0;
|
|
751
|
+
const add = [];
|
|
752
|
+
for (const s of out) {
|
|
753
|
+
const ref = wellFormed(s);
|
|
754
|
+
if (!ref) {
|
|
755
|
+
counters.subjectLinkErrors++;
|
|
756
|
+
warnOnce(
|
|
757
|
+
"entry",
|
|
758
|
+
"[telemetry] subjectLinker returned an entry that is not { type, id } \u2014 dropped. Warned once \u2014 the count is counters.subjectLinkErrors."
|
|
759
|
+
);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
const key = `${ref.type}:${ref.id}`;
|
|
763
|
+
if (seen.has(key)) continue;
|
|
764
|
+
if (!spec.subjects.includes(ref.type)) {
|
|
765
|
+
counters.subjectLinkUndeclared++;
|
|
766
|
+
}
|
|
767
|
+
if (room <= 0) {
|
|
768
|
+
capped2++;
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
seen.add(key);
|
|
772
|
+
room--;
|
|
773
|
+
add.push(ref);
|
|
774
|
+
}
|
|
775
|
+
counters.subjectLinkCapped += capped2;
|
|
776
|
+
if (!add.length) return null;
|
|
777
|
+
counters.subjectsLinked += add.length;
|
|
778
|
+
return [...have, ...add];
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function wellFormed(s) {
|
|
782
|
+
if (!s || typeof s !== "object") return null;
|
|
783
|
+
const { type, id, role } = s;
|
|
784
|
+
if (typeof type !== "string" || !type) return null;
|
|
785
|
+
if (typeof id !== "string" || !id) return null;
|
|
786
|
+
return typeof role === "string" && role ? { type, id, role } : { type, id };
|
|
787
|
+
}
|
|
675
788
|
function createEmitter(ctx) {
|
|
676
789
|
const { registry, byKind, RollupModel, rejects, counters } = ctx;
|
|
677
790
|
const burstBuckets = /* @__PURE__ */ new Map();
|
|
@@ -708,11 +821,15 @@ function createEmitter(ctx) {
|
|
|
708
821
|
const durable = kind === TelemetryKind.Usage || (doc.durable ?? spec.durable ?? false);
|
|
709
822
|
const Model = byKind[kind];
|
|
710
823
|
const { forceKeep: _drop, durable: _durable, ...rest } = doc;
|
|
824
|
+
const linked = ctx.linkSubjects ? await ctx.linkSubjects(name, spec, doc.tenantId, doc.subjects) : null;
|
|
711
825
|
const safe = (o) => new Map(Object.entries(o ?? {}).map(([k, v]) => [k.replace(/\./g, "_"), v]));
|
|
712
826
|
const payload = {
|
|
713
827
|
...rest,
|
|
714
828
|
_id: id,
|
|
715
829
|
name,
|
|
830
|
+
// computed like everything below it, and absent when nothing linked, so a
|
|
831
|
+
// host with no linker hands the model the exact object 0.4.0 did
|
|
832
|
+
...linked ? { subjects: linked } : {},
|
|
716
833
|
sampleRate: forced ? 1 : baseRate,
|
|
717
834
|
forced,
|
|
718
835
|
attrs: safe(doc.attrs),
|
|
@@ -878,6 +995,109 @@ function createForget(ctx) {
|
|
|
878
995
|
};
|
|
879
996
|
}
|
|
880
997
|
|
|
998
|
+
// src/server/relink.ts
|
|
999
|
+
var RELINK_BATCH_SIZE = 500;
|
|
1000
|
+
var refKey = (s) => `${s.type}:${s.id}`;
|
|
1001
|
+
function createRelink(ctx) {
|
|
1002
|
+
const { registry, TelemetryModel, RollupModel, counters, logger, linkSubjects } = ctx;
|
|
1003
|
+
return async function relink(opts = {}) {
|
|
1004
|
+
const dryRun = opts.dryRun !== false;
|
|
1005
|
+
const batchSize = opts.batchSize ?? RELINK_BATCH_SIZE;
|
|
1006
|
+
const result = {
|
|
1007
|
+
examined: 0,
|
|
1008
|
+
linked: 0,
|
|
1009
|
+
subjects: 0,
|
|
1010
|
+
rollups: 0,
|
|
1011
|
+
misses: 0,
|
|
1012
|
+
errors: 0,
|
|
1013
|
+
skipped: 0
|
|
1014
|
+
};
|
|
1015
|
+
if (opts.names) {
|
|
1016
|
+
const unknown = opts.names.filter((n) => !registry[n]);
|
|
1017
|
+
if (unknown.length) {
|
|
1018
|
+
throw new Error(
|
|
1019
|
+
`telemetry: relink() was given names this registry does not declare: ${unknown.join(", ")}. A name with no spec has no rollup families to replay, so relinking it could only desynchronize its rows from its aggregates. Omit \`names\` to sweep everything.`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (!linkSubjects) {
|
|
1024
|
+
logger.warn(
|
|
1025
|
+
"[telemetry] relink() has no subjectLinker to ask \u2014 nothing was read and nothing was written. Configure createTelemetry({ subjectLinker }) first; relink() backfills what that hook would have done, it does not replace it."
|
|
1026
|
+
);
|
|
1027
|
+
result.skipped = 1;
|
|
1028
|
+
return result;
|
|
1029
|
+
}
|
|
1030
|
+
if (opts.limit != null && opts.limit <= 0) return result;
|
|
1031
|
+
const filter = {};
|
|
1032
|
+
if (opts.names) filter.name = { $in: opts.names };
|
|
1033
|
+
if (opts.since) filter.occurredAt = { $gte: opts.since };
|
|
1034
|
+
const query = TelemetryModel.find(filter).hint({ _id: 1 }).batchSize(batchSize);
|
|
1035
|
+
if (opts.limit != null) query.limit(opts.limit);
|
|
1036
|
+
const progress = () => {
|
|
1037
|
+
if (!opts.onProgress) return;
|
|
1038
|
+
try {
|
|
1039
|
+
opts.onProgress({ ...result });
|
|
1040
|
+
} catch (e) {
|
|
1041
|
+
logger.warn(`[telemetry] relink() onProgress threw \u2014 ignored, the backfill continues: ${e}`);
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
const cursor = query.cursor();
|
|
1045
|
+
let sinceProgress = 0;
|
|
1046
|
+
try {
|
|
1047
|
+
for await (const doc of cursor) {
|
|
1048
|
+
result.examined++;
|
|
1049
|
+
sinceProgress++;
|
|
1050
|
+
if (sinceProgress >= batchSize) {
|
|
1051
|
+
sinceProgress = 0;
|
|
1052
|
+
progress();
|
|
1053
|
+
}
|
|
1054
|
+
const spec = registry[doc.name];
|
|
1055
|
+
if (!spec) {
|
|
1056
|
+
result.skipped++;
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
const declared = (doc.subjects ?? []).map(
|
|
1060
|
+
(s) => s.role ? { type: s.type, id: s.id, role: s.role } : { type: s.type, id: s.id }
|
|
1061
|
+
);
|
|
1062
|
+
const m0 = counters.subjectLinkMisses;
|
|
1063
|
+
const e0 = counters.subjectLinkErrors + counters.subjectLinkTimeouts;
|
|
1064
|
+
const merged = await linkSubjects(doc.name, spec, doc.tenantId, declared);
|
|
1065
|
+
result.misses += counters.subjectLinkMisses - m0;
|
|
1066
|
+
if (counters.subjectLinkErrors + counters.subjectLinkTimeouts > e0) result.errors++;
|
|
1067
|
+
if (!merged) continue;
|
|
1068
|
+
const had = new Set(declared.map(refKey));
|
|
1069
|
+
const mergedKeys = [];
|
|
1070
|
+
const newKeys = [];
|
|
1071
|
+
for (const s of merged) {
|
|
1072
|
+
const key = refKey(s);
|
|
1073
|
+
if (mergedKeys.includes(key)) continue;
|
|
1074
|
+
mergedKeys.push(key);
|
|
1075
|
+
if (!had.has(key)) newKeys.push(key);
|
|
1076
|
+
}
|
|
1077
|
+
if (!newKeys.length) continue;
|
|
1078
|
+
result.linked++;
|
|
1079
|
+
result.subjects += newKeys.length;
|
|
1080
|
+
if (!dryRun) {
|
|
1081
|
+
await TelemetryModel.updateOne(
|
|
1082
|
+
{ _id: doc._id },
|
|
1083
|
+
{ $set: { subjects: merged, subjectKeys: mergedKeys } }
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
doc.subjectKeys = newKeys;
|
|
1087
|
+
for (const r of spec.rollups ?? []) {
|
|
1088
|
+
if (!r.by.includes("subject")) continue;
|
|
1089
|
+
result.rollups += await recordRollup(RollupModel, doc, doc.name, r, counters, { dryRun });
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
} finally {
|
|
1093
|
+
await cursor.close().catch(() => {
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
if (sinceProgress) progress();
|
|
1097
|
+
return result;
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
881
1101
|
// src/server/indexes.ts
|
|
882
1102
|
var INDEX_BUDGET = 24;
|
|
883
1103
|
function createSyncIndexes(ctx) {
|
|
@@ -1221,6 +1441,8 @@ function createIngest(opts) {
|
|
|
1221
1441
|
const occurredAt = Number.isFinite(occurredRaw) ? new Date(occurredRaw - clockSkewMs) : receivedAt;
|
|
1222
1442
|
const safeMap = (o) => o && typeof o === "object" ? new Map(Object.entries(o).map(([k, v]) => [k.replace(/\./g, "_"), v])) : /* @__PURE__ */ new Map();
|
|
1223
1443
|
noteUndeclaredAttrs(t.counters, name, spec, rec.attrs);
|
|
1444
|
+
const subjects = mergeSubjects(rec.subjects);
|
|
1445
|
+
const linked = t.linkSubjects ? await t.linkSubjects(name, spec, tenantId, subjects) : null;
|
|
1224
1446
|
const Model = t.models.byKind[spec.kind];
|
|
1225
1447
|
const d = new Model({
|
|
1226
1448
|
// facts the wire may not assert: tenant, service, env, origin, plane
|
|
@@ -1231,7 +1453,7 @@ function createIngest(opts) {
|
|
|
1231
1453
|
tenantId,
|
|
1232
1454
|
occurredAt,
|
|
1233
1455
|
severity: typeof rec.severity === "string" ? rec.severity : void 0,
|
|
1234
|
-
subjects:
|
|
1456
|
+
subjects: linked ?? subjects,
|
|
1235
1457
|
actor: ctx.actor ?? (typeof rec.actor === "string" ? rec.actor : batchActor),
|
|
1236
1458
|
onBehalfOf: typeof rec.onBehalfOf === "string" ? rec.onBehalfOf : void 0,
|
|
1237
1459
|
service: key.service,
|
|
@@ -3634,7 +3856,22 @@ function createTelemetry(config) {
|
|
|
3634
3856
|
inFlight.add(p);
|
|
3635
3857
|
void p.finally(() => inFlight.delete(p));
|
|
3636
3858
|
};
|
|
3637
|
-
const
|
|
3859
|
+
const linkSubjects = createSubjectLinking({
|
|
3860
|
+
linker: config.subjectLinker,
|
|
3861
|
+
timeoutMs: config.subjectLinkTimeoutMs,
|
|
3862
|
+
counters,
|
|
3863
|
+
logger
|
|
3864
|
+
});
|
|
3865
|
+
const emit = createEmitter({
|
|
3866
|
+
registry,
|
|
3867
|
+
byKind,
|
|
3868
|
+
RollupModel,
|
|
3869
|
+
rejects,
|
|
3870
|
+
counters,
|
|
3871
|
+
logger,
|
|
3872
|
+
track,
|
|
3873
|
+
linkSubjects
|
|
3874
|
+
});
|
|
3638
3875
|
const forget = createForget({
|
|
3639
3876
|
TelemetryModel,
|
|
3640
3877
|
RollupModel,
|
|
@@ -3652,6 +3889,14 @@ function createTelemetry(config) {
|
|
|
3652
3889
|
},
|
|
3653
3890
|
globalSubjectRefs: () => config.globalSubjectRefs === true
|
|
3654
3891
|
});
|
|
3892
|
+
const relink = createRelink({
|
|
3893
|
+
registry,
|
|
3894
|
+
TelemetryModel,
|
|
3895
|
+
RollupModel,
|
|
3896
|
+
counters,
|
|
3897
|
+
logger,
|
|
3898
|
+
linkSubjects
|
|
3899
|
+
});
|
|
3655
3900
|
const syncIndexes = createSyncIndexes({
|
|
3656
3901
|
registry,
|
|
3657
3902
|
TelemetryModel,
|
|
@@ -3663,6 +3908,21 @@ function createTelemetry(config) {
|
|
|
3663
3908
|
emit,
|
|
3664
3909
|
/** erasure: delete sole-party rows, redact shared ones, rekey rollups, drop aliases */
|
|
3665
3910
|
forget,
|
|
3911
|
+
/**
|
|
3912
|
+
* Backfill: re-ask the `subjectLinker` about records ALREADY on disk, and
|
|
3913
|
+
* replay the rollups the new subjects reach.
|
|
3914
|
+
*
|
|
3915
|
+
* Linking happens at write time, so configuring it fixes the future and
|
|
3916
|
+
* nothing else — a lifetime `by:['subject']` family is keyed on the subject
|
|
3917
|
+
* the record was written with, and a read-time join cannot reach back into
|
|
3918
|
+
* it. This is how a host catches up the backlog it adopted the hook with.
|
|
3919
|
+
*
|
|
3920
|
+
* DRY RUN BY DEFAULT: it rewrites historical aggregates, so the short call
|
|
3921
|
+
* reports and the writing call says `{ dryRun: false }`. Idempotent by
|
|
3922
|
+
* construction — a row that already carries the linked subject yields
|
|
3923
|
+
* nothing new, so a second run is a no-op.
|
|
3924
|
+
*/
|
|
3925
|
+
relink,
|
|
3666
3926
|
/**
|
|
3667
3927
|
* Tenant scope is not optional — force every read through here. The five
|
|
3668
3928
|
* dashboard query primitives (records/series/distribution/rollups/journey)
|
|
@@ -3697,6 +3957,17 @@ function createTelemetry(config) {
|
|
|
3697
3957
|
counters,
|
|
3698
3958
|
/** the registry, exposed for the router factories — hosts should import their own */
|
|
3699
3959
|
registry,
|
|
3960
|
+
/**
|
|
3961
|
+
* Write-time subject linking, exposed for the router factories. `null` when
|
|
3962
|
+
* no `subjectLinker` is configured.
|
|
3963
|
+
*
|
|
3964
|
+
* The wire path does not go through emit() — createIngest() builds its
|
|
3965
|
+
* record itself, because at-least-once delivery inverts the plane order
|
|
3966
|
+
* (insert first, THEN aggregate). So it reaches the linker the same way it
|
|
3967
|
+
* reaches the registry and the models: off the instance, running the one
|
|
3968
|
+
* implementation, rather than growing a second copy of the rules.
|
|
3969
|
+
*/
|
|
3970
|
+
linkSubjects,
|
|
3700
3971
|
logger,
|
|
3701
3972
|
/** mint an ingest key; the full key string is returned once, never again */
|
|
3702
3973
|
createKey: (input) => createKey(KeyModel, input),
|
|
@@ -3724,9 +3995,12 @@ exports.LogLevel = LogLevel;
|
|
|
3724
3995
|
exports.MAX_SUGGESTIONS = MAX_SUGGESTIONS;
|
|
3725
3996
|
exports.Origin = Origin;
|
|
3726
3997
|
exports.PLATFORM_SCOPE = PLATFORM_SCOPE;
|
|
3998
|
+
exports.RELINK_BATCH_SIZE = RELINK_BATCH_SIZE;
|
|
3727
3999
|
exports.RETENTION_DAYS = RETENTION_DAYS;
|
|
3728
4000
|
exports.SAMPLE_RATE = SAMPLE_RATE;
|
|
3729
4001
|
exports.SCHEMA_VERSION = SCHEMA_VERSION;
|
|
4002
|
+
exports.SUBJECT_LINK_TIMEOUT_MS = SUBJECT_LINK_TIMEOUT_MS;
|
|
4003
|
+
exports.SUBJECT_MAX = SUBJECT_MAX;
|
|
3730
4004
|
exports.TelemetryKind = TelemetryKind;
|
|
3731
4005
|
exports.TenantMode = TenantMode;
|
|
3732
4006
|
exports.boundedMeta = boundedMeta;
|