@jeffjassky/telemetry 0.5.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 +172 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +172 -43
- package/dist/index.js.map +1 -1
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/types/index.d.ts +75 -1
- package/types/test-d.ts +18 -0
package/dist/index.js
CHANGED
|
@@ -551,10 +551,11 @@ var truncate = (d, b) => {
|
|
|
551
551
|
if (b === "day") return day;
|
|
552
552
|
return new Date(day.getTime() - (day.getUTCDay() + 6) % 7 * 864e5);
|
|
553
553
|
};
|
|
554
|
-
async function recordRollup(RollupModel, doc, name, spec, counters) {
|
|
554
|
+
async function recordRollup(RollupModel, doc, name, spec, counters, opts) {
|
|
555
|
+
const dry = opts?.dryRun === true;
|
|
555
556
|
if (spec.actors && doc.actor) {
|
|
556
557
|
const actorType = String(doc.actor).split(":")[0];
|
|
557
|
-
if (!spec.actors.includes(actorType)) return;
|
|
558
|
+
if (!spec.actors.includes(actorType)) return 0;
|
|
558
559
|
}
|
|
559
560
|
const as = spec.as ?? name;
|
|
560
561
|
const at = doc.occurredAt;
|
|
@@ -566,9 +567,11 @@ async function recordRollup(RollupModel, doc, name, spec, counters) {
|
|
|
566
567
|
let v = resolveDim(src, doc);
|
|
567
568
|
if (v == null || v === "") {
|
|
568
569
|
if (spec.dimDefault === void 0) {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
570
|
+
if (!dry) {
|
|
571
|
+
counters.rollupSkipped++;
|
|
572
|
+
bumpCounterMap(counters.rollupSkippedBy, `${as}|${label(src)}`);
|
|
573
|
+
}
|
|
574
|
+
return 0;
|
|
572
575
|
}
|
|
573
576
|
v = spec.dimDefault;
|
|
574
577
|
}
|
|
@@ -578,48 +581,48 @@ async function recordRollup(RollupModel, doc, name, spec, counters) {
|
|
|
578
581
|
const refs = fansOut ? (doc.subjectKeys ?? []).filter(
|
|
579
582
|
(r) => !spec.subjects || spec.subjects.includes(r.split(":")[0])
|
|
580
583
|
) : [null];
|
|
581
|
-
if (!refs.length) return;
|
|
584
|
+
if (!refs.length) return 0;
|
|
582
585
|
const firstCapture = Object.fromEntries(
|
|
583
586
|
(spec.capture ?? []).map((src) => [label(src), resolveDim(src, doc)]).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])
|
|
584
587
|
);
|
|
585
588
|
const expiresAt = spec.retentionDays != null ? new Date(at.getTime() + spec.retentionDays * 864e5) : void 0;
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
firstCapture: { $cond: [isNewFirst, { $literal: firstCapture }, "$firstCapture"] }
|
|
614
|
-
}
|
|
589
|
+
const ops = refs.map((ref) => {
|
|
590
|
+
const dims = spec.by.map((src) => src === "subject" ? ref : fixed.get(src));
|
|
591
|
+
const isNewFirst = {
|
|
592
|
+
$or: [{ $eq: [{ $type: "$firstAt" }, "missing"] }, { $lt: [at, "$firstAt"] }]
|
|
593
|
+
};
|
|
594
|
+
const sums = Object.fromEntries(
|
|
595
|
+
(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] }])
|
|
596
|
+
);
|
|
597
|
+
return {
|
|
598
|
+
updateOne: {
|
|
599
|
+
filter: { _id: `${doc.tenantId}|${as}|${dims.join("|")}|${bucketKey}` },
|
|
600
|
+
update: [
|
|
601
|
+
{
|
|
602
|
+
$set: {
|
|
603
|
+
tenantId: doc.tenantId,
|
|
604
|
+
as,
|
|
605
|
+
dims,
|
|
606
|
+
...ref ? { subjectType: ref.split(":")[0] } : {},
|
|
607
|
+
...bucketAt ? { bucketAt } : {},
|
|
608
|
+
...expiresAt ? { expiresAt } : {},
|
|
609
|
+
// aggregation $min/$max ignore missing, so correct on insert too
|
|
610
|
+
firstAt: { $min: ["$firstAt", at] },
|
|
611
|
+
lastAt: { $max: ["$lastAt", at] },
|
|
612
|
+
count: { $add: [{ $ifNull: ["$count", 0] }, 1] },
|
|
613
|
+
...sums,
|
|
614
|
+
firstTraceId: { $cond: [isNewFirst, doc.traceId ?? null, "$firstTraceId"] },
|
|
615
|
+
firstCapture: { $cond: [isNewFirst, { $literal: firstCapture }, "$firstCapture"] }
|
|
615
616
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
);
|
|
617
|
+
}
|
|
618
|
+
],
|
|
619
|
+
upsert: true
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
});
|
|
623
|
+
if (dry) return ops.length;
|
|
624
|
+
await RollupModel.bulkWrite(ops, { ordered: false });
|
|
625
|
+
return ops.length;
|
|
623
626
|
}
|
|
624
627
|
function buildCheckpointModel(connection, modelName, collection) {
|
|
625
628
|
const existing = connection.models?.[modelName];
|
|
@@ -983,6 +986,109 @@ function createForget(ctx) {
|
|
|
983
986
|
};
|
|
984
987
|
}
|
|
985
988
|
|
|
989
|
+
// src/server/relink.ts
|
|
990
|
+
var RELINK_BATCH_SIZE = 500;
|
|
991
|
+
var refKey = (s) => `${s.type}:${s.id}`;
|
|
992
|
+
function createRelink(ctx) {
|
|
993
|
+
const { registry, TelemetryModel, RollupModel, counters, logger, linkSubjects } = ctx;
|
|
994
|
+
return async function relink(opts = {}) {
|
|
995
|
+
const dryRun = opts.dryRun !== false;
|
|
996
|
+
const batchSize = opts.batchSize ?? RELINK_BATCH_SIZE;
|
|
997
|
+
const result = {
|
|
998
|
+
examined: 0,
|
|
999
|
+
linked: 0,
|
|
1000
|
+
subjects: 0,
|
|
1001
|
+
rollups: 0,
|
|
1002
|
+
misses: 0,
|
|
1003
|
+
errors: 0,
|
|
1004
|
+
skipped: 0
|
|
1005
|
+
};
|
|
1006
|
+
if (opts.names) {
|
|
1007
|
+
const unknown = opts.names.filter((n) => !registry[n]);
|
|
1008
|
+
if (unknown.length) {
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
`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.`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (!linkSubjects) {
|
|
1015
|
+
logger.warn(
|
|
1016
|
+
"[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."
|
|
1017
|
+
);
|
|
1018
|
+
result.skipped = 1;
|
|
1019
|
+
return result;
|
|
1020
|
+
}
|
|
1021
|
+
if (opts.limit != null && opts.limit <= 0) return result;
|
|
1022
|
+
const filter = {};
|
|
1023
|
+
if (opts.names) filter.name = { $in: opts.names };
|
|
1024
|
+
if (opts.since) filter.occurredAt = { $gte: opts.since };
|
|
1025
|
+
const query = TelemetryModel.find(filter).hint({ _id: 1 }).batchSize(batchSize);
|
|
1026
|
+
if (opts.limit != null) query.limit(opts.limit);
|
|
1027
|
+
const progress = () => {
|
|
1028
|
+
if (!opts.onProgress) return;
|
|
1029
|
+
try {
|
|
1030
|
+
opts.onProgress({ ...result });
|
|
1031
|
+
} catch (e) {
|
|
1032
|
+
logger.warn(`[telemetry] relink() onProgress threw \u2014 ignored, the backfill continues: ${e}`);
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
const cursor = query.cursor();
|
|
1036
|
+
let sinceProgress = 0;
|
|
1037
|
+
try {
|
|
1038
|
+
for await (const doc of cursor) {
|
|
1039
|
+
result.examined++;
|
|
1040
|
+
sinceProgress++;
|
|
1041
|
+
if (sinceProgress >= batchSize) {
|
|
1042
|
+
sinceProgress = 0;
|
|
1043
|
+
progress();
|
|
1044
|
+
}
|
|
1045
|
+
const spec = registry[doc.name];
|
|
1046
|
+
if (!spec) {
|
|
1047
|
+
result.skipped++;
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
const declared = (doc.subjects ?? []).map(
|
|
1051
|
+
(s) => s.role ? { type: s.type, id: s.id, role: s.role } : { type: s.type, id: s.id }
|
|
1052
|
+
);
|
|
1053
|
+
const m0 = counters.subjectLinkMisses;
|
|
1054
|
+
const e0 = counters.subjectLinkErrors + counters.subjectLinkTimeouts;
|
|
1055
|
+
const merged = await linkSubjects(doc.name, spec, doc.tenantId, declared);
|
|
1056
|
+
result.misses += counters.subjectLinkMisses - m0;
|
|
1057
|
+
if (counters.subjectLinkErrors + counters.subjectLinkTimeouts > e0) result.errors++;
|
|
1058
|
+
if (!merged) continue;
|
|
1059
|
+
const had = new Set(declared.map(refKey));
|
|
1060
|
+
const mergedKeys = [];
|
|
1061
|
+
const newKeys = [];
|
|
1062
|
+
for (const s of merged) {
|
|
1063
|
+
const key = refKey(s);
|
|
1064
|
+
if (mergedKeys.includes(key)) continue;
|
|
1065
|
+
mergedKeys.push(key);
|
|
1066
|
+
if (!had.has(key)) newKeys.push(key);
|
|
1067
|
+
}
|
|
1068
|
+
if (!newKeys.length) continue;
|
|
1069
|
+
result.linked++;
|
|
1070
|
+
result.subjects += newKeys.length;
|
|
1071
|
+
if (!dryRun) {
|
|
1072
|
+
await TelemetryModel.updateOne(
|
|
1073
|
+
{ _id: doc._id },
|
|
1074
|
+
{ $set: { subjects: merged, subjectKeys: mergedKeys } }
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
doc.subjectKeys = newKeys;
|
|
1078
|
+
for (const r of spec.rollups ?? []) {
|
|
1079
|
+
if (!r.by.includes("subject")) continue;
|
|
1080
|
+
result.rollups += await recordRollup(RollupModel, doc, doc.name, r, counters, { dryRun });
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
} finally {
|
|
1084
|
+
await cursor.close().catch(() => {
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
if (sinceProgress) progress();
|
|
1088
|
+
return result;
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
986
1092
|
// src/server/indexes.ts
|
|
987
1093
|
var INDEX_BUDGET = 24;
|
|
988
1094
|
function createSyncIndexes(ctx) {
|
|
@@ -3774,6 +3880,14 @@ function createTelemetry(config) {
|
|
|
3774
3880
|
},
|
|
3775
3881
|
globalSubjectRefs: () => config.globalSubjectRefs === true
|
|
3776
3882
|
});
|
|
3883
|
+
const relink = createRelink({
|
|
3884
|
+
registry,
|
|
3885
|
+
TelemetryModel,
|
|
3886
|
+
RollupModel,
|
|
3887
|
+
counters,
|
|
3888
|
+
logger,
|
|
3889
|
+
linkSubjects
|
|
3890
|
+
});
|
|
3777
3891
|
const syncIndexes = createSyncIndexes({
|
|
3778
3892
|
registry,
|
|
3779
3893
|
TelemetryModel,
|
|
@@ -3785,6 +3899,21 @@ function createTelemetry(config) {
|
|
|
3785
3899
|
emit,
|
|
3786
3900
|
/** erasure: delete sole-party rows, redact shared ones, rekey rollups, drop aliases */
|
|
3787
3901
|
forget,
|
|
3902
|
+
/**
|
|
3903
|
+
* Backfill: re-ask the `subjectLinker` about records ALREADY on disk, and
|
|
3904
|
+
* replay the rollups the new subjects reach.
|
|
3905
|
+
*
|
|
3906
|
+
* Linking happens at write time, so configuring it fixes the future and
|
|
3907
|
+
* nothing else — a lifetime `by:['subject']` family is keyed on the subject
|
|
3908
|
+
* the record was written with, and a read-time join cannot reach back into
|
|
3909
|
+
* it. This is how a host catches up the backlog it adopted the hook with.
|
|
3910
|
+
*
|
|
3911
|
+
* DRY RUN BY DEFAULT: it rewrites historical aggregates, so the short call
|
|
3912
|
+
* reports and the writing call says `{ dryRun: false }`. Idempotent by
|
|
3913
|
+
* construction — a row that already carries the linked subject yields
|
|
3914
|
+
* nothing new, so a second run is a no-op.
|
|
3915
|
+
*/
|
|
3916
|
+
relink,
|
|
3788
3917
|
/**
|
|
3789
3918
|
* Tenant scope is not optional — force every read through here. The five
|
|
3790
3919
|
* dashboard query primitives (records/series/distribution/rollups/journey)
|
|
@@ -3846,6 +3975,6 @@ function createTelemetry(config) {
|
|
|
3846
3975
|
};
|
|
3847
3976
|
}
|
|
3848
3977
|
|
|
3849
|
-
export { BODY_MAX_CHARS, COUNTER_MAP_MAX, COUNTER_OVERFLOW_KEY, DEFAULT_LIMITS, Env, INDEX_BUDGET, KeyKind, LogLevel, MAX_SUGGESTIONS, Origin, PLATFORM_SCOPE, RETENTION_DAYS, SAMPLE_RATE, SCHEMA_VERSION, SUBJECT_LINK_TIMEOUT_MS, SUBJECT_MAX, TelemetryKind, TenantMode, boundedMeta, createDashboard, createIngest, createKey, createQueries, createTelemetry, createValues, defaultSpaDir, defineRegistry, deriveCatalog, deriveSuggestions, deriveViews, executeReport, findFamily, foldRollups, hashSecret, intervalForRange, isPlatformScope, median, newId, normalizeQuery, parseKeyString, parseReportQuery, plain, projectRegistry, rangeOf, reportToQuery, requireMilestoneFamily, resolveDim, resolveReport, summarizeStages, traceKeep, truncate, validateRegistry, verifySecret };
|
|
3978
|
+
export { BODY_MAX_CHARS, COUNTER_MAP_MAX, COUNTER_OVERFLOW_KEY, DEFAULT_LIMITS, Env, INDEX_BUDGET, KeyKind, LogLevel, MAX_SUGGESTIONS, Origin, PLATFORM_SCOPE, RELINK_BATCH_SIZE, RETENTION_DAYS, SAMPLE_RATE, SCHEMA_VERSION, SUBJECT_LINK_TIMEOUT_MS, SUBJECT_MAX, TelemetryKind, TenantMode, boundedMeta, createDashboard, createIngest, createKey, createQueries, createTelemetry, createValues, defaultSpaDir, defineRegistry, deriveCatalog, deriveSuggestions, deriveViews, executeReport, findFamily, foldRollups, hashSecret, intervalForRange, isPlatformScope, median, newId, normalizeQuery, parseKeyString, parseReportQuery, plain, projectRegistry, rangeOf, reportToQuery, requireMilestoneFamily, resolveDim, resolveReport, summarizeStages, traceKeep, truncate, validateRegistry, verifySecret };
|
|
3850
3979
|
//# sourceMappingURL=index.js.map
|
|
3851
3980
|
//# sourceMappingURL=index.js.map
|