@jeffjassky/telemetry 0.4.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/dist/index.js CHANGED
@@ -61,7 +61,13 @@ var newCounters = () => ({
61
61
  deduped: 0,
62
62
  truncated: 0,
63
63
  rollupSkippedBy: {},
64
- undeclaredAttrs: {}
64
+ undeclaredAttrs: {},
65
+ subjectsLinked: 0,
66
+ subjectLinkMisses: 0,
67
+ subjectLinkErrors: 0,
68
+ subjectLinkTimeouts: 0,
69
+ subjectLinkUndeclared: 0,
70
+ subjectLinkCapped: 0
65
71
  });
66
72
  var COUNTER_MAP_MAX = 1e3;
67
73
  var COUNTER_OVERFLOW_KEY = "(other)|(other)";
@@ -663,6 +669,110 @@ function noteUndeclaredAttrs(counters, name, spec, attrs) {
663
669
  bumpCounterMap(counters.undeclaredAttrs, `${name}|${key}`);
664
670
  }
665
671
  }
672
+ var SUBJECT_MAX = 8;
673
+ var SUBJECT_LINK_TIMEOUT_MS = 50;
674
+ var LINK_TIMEOUT = /* @__PURE__ */ Symbol("telemetry.subjectLink.timeout");
675
+ function createSubjectLinking(opts) {
676
+ const { linker, counters, logger } = opts;
677
+ if (!linker) return null;
678
+ const timeoutMs = opts.timeoutMs ?? SUBJECT_LINK_TIMEOUT_MS;
679
+ const warned = /* @__PURE__ */ new Set();
680
+ const warnOnce = (key, msg) => {
681
+ if (warned.has(key) || warned.size >= COUNTER_MAP_MAX) return;
682
+ warned.add(key);
683
+ logger.warn(msg);
684
+ };
685
+ return async function linkSubjects(name, spec, tenantId, declared) {
686
+ const have = Array.isArray(declared) ? declared : [];
687
+ const seen = /* @__PURE__ */ new Set();
688
+ const view = [];
689
+ for (const s of have) {
690
+ const ref = wellFormed(s);
691
+ if (!ref) continue;
692
+ seen.add(`${ref.type}:${ref.id}`);
693
+ view.push(ref);
694
+ }
695
+ let out;
696
+ let timer;
697
+ try {
698
+ out = await Promise.race([
699
+ // the async wrapper turns a SYNCHRONOUS throw into a rejection, so a
700
+ // linker that dies on its first line lands in the same catch as one
701
+ // whose promise rejects
702
+ (async () => linker.link(view, { name, tenantId }))(),
703
+ new Promise((_, reject) => {
704
+ timer = setTimeout(() => reject(LINK_TIMEOUT), timeoutMs);
705
+ })
706
+ ]);
707
+ } catch (e) {
708
+ if (e === LINK_TIMEOUT) {
709
+ counters.subjectLinkTimeouts++;
710
+ warnOnce(
711
+ "timeout",
712
+ `[telemetry] subjectLinker.link() exceeded ${timeoutMs}ms \u2014 records are being written UNLINKED rather than waiting. The hook is expected to answer from a cache; a resolver that queries per record cannot keep up with ingest. Warned once \u2014 the count is counters.subjectLinkTimeouts.`
713
+ );
714
+ } else {
715
+ counters.subjectLinkErrors++;
716
+ warnOnce(
717
+ "threw",
718
+ `[telemetry] subjectLinker.link() threw \u2014 records are being written unlinked: ${e}. Warned once \u2014 the count is counters.subjectLinkErrors.`
719
+ );
720
+ }
721
+ return null;
722
+ } finally {
723
+ clearTimeout(timer);
724
+ }
725
+ if (!Array.isArray(out)) {
726
+ counters.subjectLinkErrors++;
727
+ warnOnce(
728
+ "shape",
729
+ `[telemetry] subjectLinker.link() resolved to ${typeof out}, not an array \u2014 records are being written unlinked. Return [] when nothing links. Warned once \u2014 the count is counters.subjectLinkErrors.`
730
+ );
731
+ return null;
732
+ }
733
+ if (!out.length) {
734
+ counters.subjectLinkMisses++;
735
+ return null;
736
+ }
737
+ let room = Math.max(0, SUBJECT_MAX - have.length);
738
+ let capped2 = 0;
739
+ const add = [];
740
+ for (const s of out) {
741
+ const ref = wellFormed(s);
742
+ if (!ref) {
743
+ counters.subjectLinkErrors++;
744
+ warnOnce(
745
+ "entry",
746
+ "[telemetry] subjectLinker returned an entry that is not { type, id } \u2014 dropped. Warned once \u2014 the count is counters.subjectLinkErrors."
747
+ );
748
+ continue;
749
+ }
750
+ const key = `${ref.type}:${ref.id}`;
751
+ if (seen.has(key)) continue;
752
+ if (!spec.subjects.includes(ref.type)) {
753
+ counters.subjectLinkUndeclared++;
754
+ }
755
+ if (room <= 0) {
756
+ capped2++;
757
+ continue;
758
+ }
759
+ seen.add(key);
760
+ room--;
761
+ add.push(ref);
762
+ }
763
+ counters.subjectLinkCapped += capped2;
764
+ if (!add.length) return null;
765
+ counters.subjectsLinked += add.length;
766
+ return [...have, ...add];
767
+ };
768
+ }
769
+ function wellFormed(s) {
770
+ if (!s || typeof s !== "object") return null;
771
+ const { type, id, role } = s;
772
+ if (typeof type !== "string" || !type) return null;
773
+ if (typeof id !== "string" || !id) return null;
774
+ return typeof role === "string" && role ? { type, id, role } : { type, id };
775
+ }
666
776
  function createEmitter(ctx) {
667
777
  const { registry, byKind, RollupModel, rejects, counters } = ctx;
668
778
  const burstBuckets = /* @__PURE__ */ new Map();
@@ -699,11 +809,15 @@ function createEmitter(ctx) {
699
809
  const durable = kind === TelemetryKind.Usage || (doc.durable ?? spec.durable ?? false);
700
810
  const Model = byKind[kind];
701
811
  const { forceKeep: _drop, durable: _durable, ...rest } = doc;
812
+ const linked = ctx.linkSubjects ? await ctx.linkSubjects(name, spec, doc.tenantId, doc.subjects) : null;
702
813
  const safe = (o) => new Map(Object.entries(o ?? {}).map(([k, v]) => [k.replace(/\./g, "_"), v]));
703
814
  const payload = {
704
815
  ...rest,
705
816
  _id: id,
706
817
  name,
818
+ // computed like everything below it, and absent when nothing linked, so a
819
+ // host with no linker hands the model the exact object 0.4.0 did
820
+ ...linked ? { subjects: linked } : {},
707
821
  sampleRate: forced ? 1 : baseRate,
708
822
  forced,
709
823
  attrs: safe(doc.attrs),
@@ -1212,6 +1326,8 @@ function createIngest(opts) {
1212
1326
  const occurredAt = Number.isFinite(occurredRaw) ? new Date(occurredRaw - clockSkewMs) : receivedAt;
1213
1327
  const safeMap = (o) => o && typeof o === "object" ? new Map(Object.entries(o).map(([k, v]) => [k.replace(/\./g, "_"), v])) : /* @__PURE__ */ new Map();
1214
1328
  noteUndeclaredAttrs(t.counters, name, spec, rec.attrs);
1329
+ const subjects = mergeSubjects(rec.subjects);
1330
+ const linked = t.linkSubjects ? await t.linkSubjects(name, spec, tenantId, subjects) : null;
1215
1331
  const Model = t.models.byKind[spec.kind];
1216
1332
  const d = new Model({
1217
1333
  // facts the wire may not assert: tenant, service, env, origin, plane
@@ -1222,7 +1338,7 @@ function createIngest(opts) {
1222
1338
  tenantId,
1223
1339
  occurredAt,
1224
1340
  severity: typeof rec.severity === "string" ? rec.severity : void 0,
1225
- subjects: mergeSubjects(rec.subjects),
1341
+ subjects: linked ?? subjects,
1226
1342
  actor: ctx.actor ?? (typeof rec.actor === "string" ? rec.actor : batchActor),
1227
1343
  onBehalfOf: typeof rec.onBehalfOf === "string" ? rec.onBehalfOf : void 0,
1228
1344
  service: key.service,
@@ -3625,7 +3741,22 @@ function createTelemetry(config) {
3625
3741
  inFlight.add(p);
3626
3742
  void p.finally(() => inFlight.delete(p));
3627
3743
  };
3628
- const emit = createEmitter({ registry, byKind, RollupModel, rejects, counters, logger, track });
3744
+ const linkSubjects = createSubjectLinking({
3745
+ linker: config.subjectLinker,
3746
+ timeoutMs: config.subjectLinkTimeoutMs,
3747
+ counters,
3748
+ logger
3749
+ });
3750
+ const emit = createEmitter({
3751
+ registry,
3752
+ byKind,
3753
+ RollupModel,
3754
+ rejects,
3755
+ counters,
3756
+ logger,
3757
+ track,
3758
+ linkSubjects
3759
+ });
3629
3760
  const forget = createForget({
3630
3761
  TelemetryModel,
3631
3762
  RollupModel,
@@ -3688,6 +3819,17 @@ function createTelemetry(config) {
3688
3819
  counters,
3689
3820
  /** the registry, exposed for the router factories — hosts should import their own */
3690
3821
  registry,
3822
+ /**
3823
+ * Write-time subject linking, exposed for the router factories. `null` when
3824
+ * no `subjectLinker` is configured.
3825
+ *
3826
+ * The wire path does not go through emit() — createIngest() builds its
3827
+ * record itself, because at-least-once delivery inverts the plane order
3828
+ * (insert first, THEN aggregate). So it reaches the linker the same way it
3829
+ * reaches the registry and the models: off the instance, running the one
3830
+ * implementation, rather than growing a second copy of the rules.
3831
+ */
3832
+ linkSubjects,
3691
3833
  logger,
3692
3834
  /** mint an ingest key; the full key string is returned once, never again */
3693
3835
  createKey: (input) => createKey(KeyModel, input),
@@ -3704,6 +3846,6 @@ function createTelemetry(config) {
3704
3846
  };
3705
3847
  }
3706
3848
 
3707
- export { BODY_MAX_CHARS, COUNTER_MAP_MAX, COUNTER_OVERFLOW_KEY, DEFAULT_LIMITS, Env, INDEX_BUDGET, KeyKind, LogLevel, MAX_SUGGESTIONS, Origin, PLATFORM_SCOPE, RETENTION_DAYS, SAMPLE_RATE, SCHEMA_VERSION, TelemetryKind, TenantMode, boundedMeta, createDashboard, createIngest, createKey, createQueries, createTelemetry, createValues, defaultSpaDir, defineRegistry, deriveCatalog, deriveSuggestions, deriveViews, executeReport, findFamily, foldRollups, hashSecret, intervalForRange, isPlatformScope, median, newId, normalizeQuery, parseKeyString, parseReportQuery, plain, projectRegistry, rangeOf, reportToQuery, requireMilestoneFamily, resolveDim, resolveReport, summarizeStages, traceKeep, truncate, validateRegistry, verifySecret };
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 };
3708
3850
  //# sourceMappingURL=index.js.map
3709
3851
  //# sourceMappingURL=index.js.map