@merittdev/horus 0.12.3 → 0.12.5
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 +597 -155
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -65880,7 +65880,7 @@ init_cjs_shims();
|
|
|
65880
65880
|
|
|
65881
65881
|
// ../../packages/core/src/version.ts
|
|
65882
65882
|
init_cjs_shims();
|
|
65883
|
-
var HORUS_VERSION = true ? "0.12.
|
|
65883
|
+
var HORUS_VERSION = true ? "0.12.5" : "dev";
|
|
65884
65884
|
var PINNED_SOURCE_VERSION = "2.0.2";
|
|
65885
65885
|
|
|
65886
65886
|
// ../../packages/core/src/evidence.ts
|
|
@@ -71622,6 +71622,167 @@ var SourceMemoryVectorIndex = class {
|
|
|
71622
71622
|
// ../../packages/connectors/src/contract.ts
|
|
71623
71623
|
init_cjs_shims();
|
|
71624
71624
|
|
|
71625
|
+
// ../../packages/connectors/src/duration.ts
|
|
71626
|
+
init_cjs_shims();
|
|
71627
|
+
function readPath(obj, path) {
|
|
71628
|
+
if (Object.prototype.hasOwnProperty.call(obj, path)) return obj[path];
|
|
71629
|
+
const parts2 = path.split(".");
|
|
71630
|
+
let node = obj;
|
|
71631
|
+
for (const part of parts2) {
|
|
71632
|
+
if (node === null || typeof node !== "object") return void 0;
|
|
71633
|
+
node = node[part];
|
|
71634
|
+
}
|
|
71635
|
+
return node;
|
|
71636
|
+
}
|
|
71637
|
+
var UNIT_MS = {
|
|
71638
|
+
h: 36e5,
|
|
71639
|
+
m: 6e4,
|
|
71640
|
+
s: 1e3,
|
|
71641
|
+
ms: 1,
|
|
71642
|
+
us: 1e-3,
|
|
71643
|
+
"\xB5s": 1e-3,
|
|
71644
|
+
ns: 1e-6
|
|
71645
|
+
};
|
|
71646
|
+
var DURATION_TOKEN = /(\d+(?:\.\d+)?)\s*(ms|us|µs|ns|h|m|s)(?![a-zA-Z])/gi;
|
|
71647
|
+
function parseDurationMs(raw) {
|
|
71648
|
+
if (typeof raw !== "string") return null;
|
|
71649
|
+
const s5 = raw.trim().replace(/^~/, "");
|
|
71650
|
+
if (s5 === "") return null;
|
|
71651
|
+
let total = 0;
|
|
71652
|
+
let matched = false;
|
|
71653
|
+
DURATION_TOKEN.lastIndex = 0;
|
|
71654
|
+
let m5;
|
|
71655
|
+
while ((m5 = DURATION_TOKEN.exec(s5)) !== null) {
|
|
71656
|
+
const value = Number(m5[1]);
|
|
71657
|
+
const unit = m5[2].toLowerCase();
|
|
71658
|
+
const factor = UNIT_MS[unit];
|
|
71659
|
+
if (Number.isFinite(value) && factor !== void 0) {
|
|
71660
|
+
total += value * factor;
|
|
71661
|
+
matched = true;
|
|
71662
|
+
}
|
|
71663
|
+
}
|
|
71664
|
+
if (matched) return total;
|
|
71665
|
+
if (/^\d+(?:\.\d+)?$/.test(s5)) {
|
|
71666
|
+
const n4 = Number(s5);
|
|
71667
|
+
return Number.isFinite(n4) ? n4 : null;
|
|
71668
|
+
}
|
|
71669
|
+
return null;
|
|
71670
|
+
}
|
|
71671
|
+
function extractByRegex(text2, regex) {
|
|
71672
|
+
const m5 = regex.exec(text2);
|
|
71673
|
+
if (m5 === null) return null;
|
|
71674
|
+
const captured = m5[1] ?? m5[0];
|
|
71675
|
+
const v4 = captured.trim();
|
|
71676
|
+
return v4 === "" ? null : v4;
|
|
71677
|
+
}
|
|
71678
|
+
function percentile(sortedAsc, p5) {
|
|
71679
|
+
const n4 = sortedAsc.length;
|
|
71680
|
+
if (n4 === 0) return 0;
|
|
71681
|
+
if (n4 === 1) return sortedAsc[0];
|
|
71682
|
+
const rank = Math.ceil(p5 / 100 * n4);
|
|
71683
|
+
const idx = Math.min(n4 - 1, Math.max(0, rank - 1));
|
|
71684
|
+
return sortedAsc[idx];
|
|
71685
|
+
}
|
|
71686
|
+
function computeStat(durations) {
|
|
71687
|
+
const finite = durations.filter((d3) => Number.isFinite(d3));
|
|
71688
|
+
if (finite.length === 0) {
|
|
71689
|
+
return { avg: 0, p95: 0, count: 0, min: 0, max: 0 };
|
|
71690
|
+
}
|
|
71691
|
+
const sorted = [...finite].sort((a2, b5) => a2 - b5);
|
|
71692
|
+
const sum = sorted.reduce((acc, d3) => acc + d3, 0);
|
|
71693
|
+
return {
|
|
71694
|
+
avg: sum / sorted.length,
|
|
71695
|
+
p95: percentile(sorted, 95),
|
|
71696
|
+
count: sorted.length,
|
|
71697
|
+
min: sorted[0],
|
|
71698
|
+
max: sorted[sorted.length - 1]
|
|
71699
|
+
};
|
|
71700
|
+
}
|
|
71701
|
+
function readDimension(rec, spec) {
|
|
71702
|
+
if (spec.field !== void 0) {
|
|
71703
|
+
const raw = readPath(rec.fields, spec.field);
|
|
71704
|
+
if (raw === null || raw === void 0) {
|
|
71705
|
+
if (spec.pattern === void 0) return null;
|
|
71706
|
+
} else {
|
|
71707
|
+
const v4 = String(raw).trim();
|
|
71708
|
+
if (v4 !== "") return v4;
|
|
71709
|
+
if (spec.pattern === void 0) return null;
|
|
71710
|
+
}
|
|
71711
|
+
}
|
|
71712
|
+
if (spec.pattern !== void 0) {
|
|
71713
|
+
const sourceField = spec.patternField;
|
|
71714
|
+
const text2 = sourceField !== void 0 ? typeof readPath(rec.fields, sourceField) === "string" ? readPath(rec.fields, sourceField) : String(readPath(rec.fields, sourceField) ?? "") : rec.message ?? "";
|
|
71715
|
+
if (text2 === "") return null;
|
|
71716
|
+
try {
|
|
71717
|
+
return extractByRegex(text2, new RegExp(spec.pattern));
|
|
71718
|
+
} catch {
|
|
71719
|
+
return null;
|
|
71720
|
+
}
|
|
71721
|
+
}
|
|
71722
|
+
return null;
|
|
71723
|
+
}
|
|
71724
|
+
function readDuration(rec, opts) {
|
|
71725
|
+
if (opts.durationField !== void 0) {
|
|
71726
|
+
const raw = readPath(rec.fields, opts.durationField);
|
|
71727
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
71728
|
+
return opts.durationFieldUnit === "s" ? raw * 1e3 : raw;
|
|
71729
|
+
}
|
|
71730
|
+
if (typeof raw === "string") return parseDurationMs(raw);
|
|
71731
|
+
return null;
|
|
71732
|
+
}
|
|
71733
|
+
const msg = rec.message ?? "";
|
|
71734
|
+
if (msg === "") return null;
|
|
71735
|
+
if (opts.durationPattern !== void 0) {
|
|
71736
|
+
let isolated;
|
|
71737
|
+
try {
|
|
71738
|
+
isolated = extractByRegex(msg, new RegExp(opts.durationPattern));
|
|
71739
|
+
} catch {
|
|
71740
|
+
isolated = null;
|
|
71741
|
+
}
|
|
71742
|
+
return isolated !== null ? parseDurationMs(isolated) : null;
|
|
71743
|
+
}
|
|
71744
|
+
return parseDurationMs(msg);
|
|
71745
|
+
}
|
|
71746
|
+
function extractDurationSamples(records, opts) {
|
|
71747
|
+
const needle = opts.completionText?.toLowerCase();
|
|
71748
|
+
const out2 = [];
|
|
71749
|
+
for (const rec of records) {
|
|
71750
|
+
if (needle !== void 0) {
|
|
71751
|
+
const hay = (rec.message ?? "").toLowerCase();
|
|
71752
|
+
if (!hay.includes(needle)) continue;
|
|
71753
|
+
}
|
|
71754
|
+
const dimension = readDimension(rec, opts.dimension);
|
|
71755
|
+
if (dimension === null) continue;
|
|
71756
|
+
const durationMs = readDuration(rec, opts);
|
|
71757
|
+
if (durationMs === null || !Number.isFinite(durationMs)) continue;
|
|
71758
|
+
out2.push({ dimension, durationMs });
|
|
71759
|
+
}
|
|
71760
|
+
return out2;
|
|
71761
|
+
}
|
|
71762
|
+
function aggregateDurations(samples, dimensionName) {
|
|
71763
|
+
if (samples.length === 0) return null;
|
|
71764
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
71765
|
+
for (const s5 of samples) {
|
|
71766
|
+
const list = grouped.get(s5.dimension);
|
|
71767
|
+
if (list === void 0) grouped.set(s5.dimension, [s5.durationMs]);
|
|
71768
|
+
else list.push(s5.durationMs);
|
|
71769
|
+
}
|
|
71770
|
+
const byValue = {};
|
|
71771
|
+
for (const [value, durations] of grouped) {
|
|
71772
|
+
byValue[value] = computeStat(durations);
|
|
71773
|
+
}
|
|
71774
|
+
return {
|
|
71775
|
+
dimension: dimensionName,
|
|
71776
|
+
unit: "ms",
|
|
71777
|
+
byValue,
|
|
71778
|
+
sampleCount: samples.length
|
|
71779
|
+
};
|
|
71780
|
+
}
|
|
71781
|
+
function durationsByDimension(records, opts) {
|
|
71782
|
+
const samples = extractDurationSamples(records, opts);
|
|
71783
|
+
return aggregateDurations(samples, opts.dimension.name);
|
|
71784
|
+
}
|
|
71785
|
+
|
|
71625
71786
|
// ../../packages/connectors/src/factory.ts
|
|
71626
71787
|
init_cjs_shims();
|
|
71627
71788
|
|
|
@@ -72522,6 +72683,33 @@ var ElasticsearchLogsProvider = class {
|
|
|
72522
72683
|
}
|
|
72523
72684
|
return analysis;
|
|
72524
72685
|
}
|
|
72686
|
+
async analyzeDurations(opts) {
|
|
72687
|
+
try {
|
|
72688
|
+
const q2 = {
|
|
72689
|
+
level: opts.level ?? "info",
|
|
72690
|
+
limit: opts.limit ?? 500,
|
|
72691
|
+
...opts.service !== void 0 ? { service: opts.service } : {},
|
|
72692
|
+
...opts.from !== void 0 ? { from: opts.from } : {},
|
|
72693
|
+
...opts.to !== void 0 ? { to: opts.to } : {},
|
|
72694
|
+
...opts.completionText !== void 0 ? { text: opts.completionText, broadText: true } : {}
|
|
72695
|
+
};
|
|
72696
|
+
const records = await this.searchLogs(q2);
|
|
72697
|
+
const normalized = records.map((r) => ({
|
|
72698
|
+
message: r.message,
|
|
72699
|
+
fields: {
|
|
72700
|
+
...r.raw,
|
|
72701
|
+
...r.context !== void 0 ? { context: r.context } : {},
|
|
72702
|
+
...r.eventCode !== void 0 ? { event_code: r.eventCode } : {},
|
|
72703
|
+
...r.service !== void 0 ? { service_name: r.service } : {},
|
|
72704
|
+
...r.detail !== void 0 ? { detail: r.detail } : {},
|
|
72705
|
+
message: r.message
|
|
72706
|
+
}
|
|
72707
|
+
}));
|
|
72708
|
+
return durationsByDimension(normalized, opts);
|
|
72709
|
+
} catch {
|
|
72710
|
+
return null;
|
|
72711
|
+
}
|
|
72712
|
+
}
|
|
72525
72713
|
async checkCompatibility(opts) {
|
|
72526
72714
|
const fields = [
|
|
72527
72715
|
this.mapping.timestampField,
|
|
@@ -72742,6 +72930,85 @@ function compareWindows(baseline, current, spikeRatio = 1.5) {
|
|
|
72742
72930
|
comparisons.sort((a2, b5) => b5.delta - a2.delta);
|
|
72743
72931
|
return comparisons;
|
|
72744
72932
|
}
|
|
72933
|
+
function quantileSorted(sortedAsc, p5) {
|
|
72934
|
+
const n4 = sortedAsc.length;
|
|
72935
|
+
if (n4 === 0) return NaN;
|
|
72936
|
+
if (n4 === 1) return sortedAsc[0];
|
|
72937
|
+
const pos = (n4 - 1) * p5;
|
|
72938
|
+
const lo = Math.floor(pos);
|
|
72939
|
+
const hi = Math.ceil(pos);
|
|
72940
|
+
if (lo === hi) return sortedAsc[lo];
|
|
72941
|
+
const frac = pos - lo;
|
|
72942
|
+
return sortedAsc[lo] * (1 - frac) + sortedAsc[hi] * frac;
|
|
72943
|
+
}
|
|
72944
|
+
function median(values2) {
|
|
72945
|
+
const finite = values2.filter((v4) => Number.isFinite(v4));
|
|
72946
|
+
if (finite.length === 0) return NaN;
|
|
72947
|
+
const sorted = [...finite].sort((a2, b5) => a2 - b5);
|
|
72948
|
+
return quantileSorted(sorted, 0.5);
|
|
72949
|
+
}
|
|
72950
|
+
function detectBimodalPopulation(comparisons, opts) {
|
|
72951
|
+
const spike = opts?.spike ?? 1.5;
|
|
72952
|
+
const drop = opts?.drop ?? 0.67;
|
|
72953
|
+
const minShare = opts?.minShare ?? 0.2;
|
|
72954
|
+
const minMinority = opts?.minMinority ?? 2;
|
|
72955
|
+
const minTotal = opts?.minTotal ?? 3;
|
|
72956
|
+
const gapFactor = opts?.gapFactor ?? 2;
|
|
72957
|
+
const total = comparisons.length;
|
|
72958
|
+
const empty = {
|
|
72959
|
+
bimodal: false,
|
|
72960
|
+
total,
|
|
72961
|
+
highCount: 0,
|
|
72962
|
+
lowCount: 0,
|
|
72963
|
+
degenerateCount: 0,
|
|
72964
|
+
highShare: 0,
|
|
72965
|
+
lowShare: 0,
|
|
72966
|
+
highRatio: NaN,
|
|
72967
|
+
lowRatio: NaN
|
|
72968
|
+
};
|
|
72969
|
+
const highRatios = [];
|
|
72970
|
+
const lowRatios = [];
|
|
72971
|
+
let degenerateCount = 0;
|
|
72972
|
+
for (const c4 of comparisons) {
|
|
72973
|
+
const r = c4.ratio;
|
|
72974
|
+
if (!Number.isFinite(r)) {
|
|
72975
|
+
degenerateCount += 1;
|
|
72976
|
+
} else if (r >= spike) {
|
|
72977
|
+
highRatios.push(r);
|
|
72978
|
+
} else if (r > 0 && r <= drop) {
|
|
72979
|
+
lowRatios.push(r);
|
|
72980
|
+
} else if (r === 0) {
|
|
72981
|
+
degenerateCount += 1;
|
|
72982
|
+
}
|
|
72983
|
+
}
|
|
72984
|
+
const highCount = highRatios.length;
|
|
72985
|
+
const lowCount = lowRatios.length;
|
|
72986
|
+
const counterCount = lowCount + degenerateCount;
|
|
72987
|
+
const result = {
|
|
72988
|
+
...empty,
|
|
72989
|
+
highCount,
|
|
72990
|
+
lowCount,
|
|
72991
|
+
degenerateCount,
|
|
72992
|
+
highShare: total > 0 ? highCount / total : 0,
|
|
72993
|
+
lowShare: total > 0 ? counterCount / total : 0,
|
|
72994
|
+
highRatio: median(highRatios),
|
|
72995
|
+
lowRatio: median(lowRatios)
|
|
72996
|
+
};
|
|
72997
|
+
if (total < minTotal) return result;
|
|
72998
|
+
if (highCount < 1 || counterCount < 1) return result;
|
|
72999
|
+
if (counterCount < minMinority) return result;
|
|
73000
|
+
if (result.lowShare < minShare) return result;
|
|
73001
|
+
if (highCount < minMinority && result.highShare < minShare) return result;
|
|
73002
|
+
if (lowCount > 0) {
|
|
73003
|
+
const hi = result.highRatio;
|
|
73004
|
+
const lo = result.lowRatio;
|
|
73005
|
+
if (Number.isFinite(hi) && Number.isFinite(lo) && lo > 0 && hi / lo < gapFactor) {
|
|
73006
|
+
return result;
|
|
73007
|
+
}
|
|
73008
|
+
}
|
|
73009
|
+
result.bimodal = true;
|
|
73010
|
+
return result;
|
|
73011
|
+
}
|
|
72745
73012
|
|
|
72746
73013
|
// ../../packages/connectors/src/grafana/panels.ts
|
|
72747
73014
|
init_cjs_shims();
|
|
@@ -72869,8 +73136,19 @@ function classifyAnomaly(kind, cmp, opts) {
|
|
|
72869
73136
|
}
|
|
72870
73137
|
function buildFindings(dashboardUid, panelTitle, kind, baseline, current) {
|
|
72871
73138
|
const comparisons = compareWindows(baseline, current);
|
|
73139
|
+
const bimodal = detectBimodalPopulation(comparisons);
|
|
73140
|
+
const spike = 1.5;
|
|
73141
|
+
const drop = 0.67;
|
|
73142
|
+
const isBimodalMember = (cmp) => {
|
|
73143
|
+
const r = cmp.ratio;
|
|
73144
|
+
if (!Number.isFinite(r)) return true;
|
|
73145
|
+
if (r >= spike) return true;
|
|
73146
|
+
if (r === 0) return true;
|
|
73147
|
+
return r > 0 && r <= drop;
|
|
73148
|
+
};
|
|
72872
73149
|
return comparisons.map((cmp) => {
|
|
72873
|
-
const
|
|
73150
|
+
const baseAnomaly = classifyAnomaly(kind, cmp);
|
|
73151
|
+
const anomaly = bimodal.bimodal && baseAnomaly !== "none" && isBimodalMember(cmp) ? "bimodal-population" : baseAnomaly;
|
|
72874
73152
|
const currentSeries = current.find((s5) => {
|
|
72875
73153
|
const keys = Object.keys(cmp.labels);
|
|
72876
73154
|
return keys.every((k3) => s5.labels[k3] === cmp.labels[k3]);
|
|
@@ -72901,6 +73179,8 @@ function anomalyLabel(anomaly) {
|
|
|
72901
73179
|
return "Queue Growth";
|
|
72902
73180
|
case "change":
|
|
72903
73181
|
return "Change";
|
|
73182
|
+
case "bimodal-population":
|
|
73183
|
+
return "Bimodal Population (possible per-segment variance)";
|
|
72904
73184
|
case "none":
|
|
72905
73185
|
return "No Anomaly";
|
|
72906
73186
|
}
|
|
@@ -72915,6 +73195,11 @@ function anomalyRelevance(anomaly) {
|
|
|
72915
73195
|
return 0.8;
|
|
72916
73196
|
case "change":
|
|
72917
73197
|
return 0.55;
|
|
73198
|
+
// Lower than a confident single-cause spike: a two-population pattern is a
|
|
73199
|
+
// hypothesis ("per-segment variance"), not a uniform regression — keep the
|
|
73200
|
+
// engine honest by NOT presenting it with spike-level weight (HOR-435).
|
|
73201
|
+
case "bimodal-population":
|
|
73202
|
+
return 0.6;
|
|
72918
73203
|
case "none":
|
|
72919
73204
|
return 0;
|
|
72920
73205
|
}
|
|
@@ -74045,6 +74330,37 @@ var AxiomProvider = class {
|
|
|
74045
74330
|
return [];
|
|
74046
74331
|
}
|
|
74047
74332
|
}
|
|
74333
|
+
/**
|
|
74334
|
+
* INFO-level duration coverage (HOR-434). Query NON-error completion/duration rows
|
|
74335
|
+
* (e.g. `Completed MANAGE_SALES:KSA ~2m10s`) and aggregate duration by an extracted
|
|
74336
|
+
* dimension (region / market / tenant) — from a structured field or a regex over the
|
|
74337
|
+
* job id / message. Returns per-dimension stats `{ region: { KSA: {avg,p95,count,…} } }`
|
|
74338
|
+
* so the engine sees per-segment variance the error-biased `collect()` path misses.
|
|
74339
|
+
*
|
|
74340
|
+
* Graceful: no completion rows, no parseable duration, or no extractable dimension
|
|
74341
|
+
* → `null`. Never throws.
|
|
74342
|
+
*/
|
|
74343
|
+
async analyzeDurations(opts) {
|
|
74344
|
+
try {
|
|
74345
|
+
const to = opts.to ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
74346
|
+
const from = opts.from ?? new Date(
|
|
74347
|
+
Date.parse(to) - (this.opts.windowMs ?? DEFAULT_WINDOW_MS)
|
|
74348
|
+
).toISOString();
|
|
74349
|
+
const apl = buildApl(
|
|
74350
|
+
this.opts.dataset,
|
|
74351
|
+
opts.completionText !== void 0 ? [opts.completionText] : [],
|
|
74352
|
+
opts.limit ?? 500
|
|
74353
|
+
);
|
|
74354
|
+
const rows = await this.client.query(apl, from, to);
|
|
74355
|
+
const normalized = rows.map((rec) => ({
|
|
74356
|
+
message: pickField2(rec.fields, MESSAGE_FIELDS) ?? "",
|
|
74357
|
+
fields: rec.fields
|
|
74358
|
+
}));
|
|
74359
|
+
return durationsByDimension(normalized, opts);
|
|
74360
|
+
} catch {
|
|
74361
|
+
return null;
|
|
74362
|
+
}
|
|
74363
|
+
}
|
|
74048
74364
|
async health() {
|
|
74049
74365
|
return this.client.health();
|
|
74050
74366
|
}
|
|
@@ -92806,6 +93122,36 @@ function generateHypotheses(evidence2, _correlation, ctx) {
|
|
|
92806
93122
|
contradictingEvidenceIds: [],
|
|
92807
93123
|
missingEvidence: infraSupport.length > 0 ? [] : ["Infrastructure metrics + datastore/cache state (`horus metrics`)"]
|
|
92808
93124
|
});
|
|
93125
|
+
const benignSupport = [
|
|
93126
|
+
.../* @__PURE__ */ new Set([
|
|
93127
|
+
...ctx.perDimensionDurationEvIds ?? [],
|
|
93128
|
+
...ctx.bimodalMetricEvIds ?? []
|
|
93129
|
+
])
|
|
93130
|
+
];
|
|
93131
|
+
if (benignSupport.length > 0 || ctx.benignVarianceApplicable === true) {
|
|
93132
|
+
hyps.push({
|
|
93133
|
+
id: globalThis.crypto.randomUUID(),
|
|
93134
|
+
category: "benign-variance",
|
|
93135
|
+
statement: "The anomaly may reflect expected per-segment/per-region variance (or a skewed average), not a failure.",
|
|
93136
|
+
confidence: 0.1,
|
|
93137
|
+
supportingEvidenceIds: benignSupport,
|
|
93138
|
+
contradictingEvidenceIds: [],
|
|
93139
|
+
missingEvidence: benignSupport.length > 0 ? [] : [
|
|
93140
|
+
"Per-segment duration breakdown (region/market/tenant) or a bimodal metric distribution (`horus logs` / `horus metrics`)"
|
|
93141
|
+
]
|
|
93142
|
+
});
|
|
93143
|
+
}
|
|
93144
|
+
const suggested = new Set(ctx.alertSuggestedCategories ?? []);
|
|
93145
|
+
if (suggested.size > 0) {
|
|
93146
|
+
for (const h3 of hyps) {
|
|
93147
|
+
if (suggested.has(h3.category) && h3.supportingEvidenceIds.length === 0) {
|
|
93148
|
+
h3.missingEvidence = [
|
|
93149
|
+
...h3.missingEvidence,
|
|
93150
|
+
"Suggested by the alert/hint text \u2014 confidence requires independent evidence, not the alert wording itself"
|
|
93151
|
+
];
|
|
93152
|
+
}
|
|
93153
|
+
}
|
|
93154
|
+
}
|
|
92809
93155
|
hyps.sort((a2, b5) => b5.confidence - a2.confidence);
|
|
92810
93156
|
return hyps;
|
|
92811
93157
|
}
|
|
@@ -92853,6 +93199,26 @@ function validateHypotheses(hyps, evidence2) {
|
|
|
92853
93199
|
rationale
|
|
92854
93200
|
};
|
|
92855
93201
|
});
|
|
93202
|
+
const SHARED_EVIDENCE_CEILING = 0.75;
|
|
93203
|
+
const bySupport = /* @__PURE__ */ new Map();
|
|
93204
|
+
for (const v4 of validated) {
|
|
93205
|
+
if (v4.contradictingPresent > 0) continue;
|
|
93206
|
+
const presentSupport = v4.supportingEvidenceIds.filter((id) => present.has(id)).sort();
|
|
93207
|
+
if (presentSupport.length === 0) continue;
|
|
93208
|
+
const key = presentSupport.join("|");
|
|
93209
|
+
const list = bySupport.get(key);
|
|
93210
|
+
if (list === void 0) bySupport.set(key, [v4]);
|
|
93211
|
+
else list.push(v4);
|
|
93212
|
+
}
|
|
93213
|
+
for (const cohort of bySupport.values()) {
|
|
93214
|
+
if (cohort.length < 2) continue;
|
|
93215
|
+
for (const v4 of cohort) {
|
|
93216
|
+
if (v4.confidence > SHARED_EVIDENCE_CEILING) {
|
|
93217
|
+
v4.confidence = SHARED_EVIDENCE_CEILING;
|
|
93218
|
+
v4.rationale = v4.rationale.replace(/\.$/, "") + `; capped at ${SHARED_EVIDENCE_CEILING} \u2014 shares all supporting evidence with ${cohort.length - 1} other hypothesis(es) and nothing distinguishes them.`;
|
|
93219
|
+
}
|
|
93220
|
+
}
|
|
93221
|
+
}
|
|
92856
93222
|
validated.sort((a2, b5) => {
|
|
92857
93223
|
const aElim = a2.verdict === "eliminated" ? 1 : 0;
|
|
92858
93224
|
const bElim = b5.verdict === "eliminated" ? 1 : 0;
|
|
@@ -95004,6 +95370,46 @@ function looksPerformance(hint) {
|
|
|
95004
95370
|
hint
|
|
95005
95371
|
);
|
|
95006
95372
|
}
|
|
95373
|
+
function looksDurationThemed(hint) {
|
|
95374
|
+
return looksPerformance(hint) || /\b(duration|took|elapsed|anomal(?:y|ies|ous)|spike|variance|regress(?:ion|ed)?|p\d{2})\b/i.test(
|
|
95375
|
+
hint
|
|
95376
|
+
);
|
|
95377
|
+
}
|
|
95378
|
+
function parseSuggestedCauses(hint) {
|
|
95379
|
+
const SUGGESTIVE = /\b(could indicate|may be|might be|maybe|possibly|suggest(?:s|ing|ed)?|likely (?:caused|due)|points? to|consistent with|indicat(?:es|ive)|appears? to be|seems? to be|probably)\b/i;
|
|
95380
|
+
if (!SUGGESTIVE.test(hint)) return [];
|
|
95381
|
+
const CATEGORY_CUES = [
|
|
95382
|
+
["deployment-regression", /\b(deploy(?:ment|ed)?|release|rollout|regression|recent change|shipped)\b/i],
|
|
95383
|
+
["infrastructure", /\b(database|db|infra(?:structure)?|network|cache|disk|memory|cpu|oom|out of memory|connection pool|datastore|redis|mongo|postgres)\b/i],
|
|
95384
|
+
["external-api-latency", /\b(upstream|external api|third[- ]party|downstream|api latency|timeout|slow (?:api|service)|gateway)\b/i],
|
|
95385
|
+
["retry-storm", /\b(retr(?:y|ies)|retry storm|thundering herd|amplif)/i],
|
|
95386
|
+
["queue-backlog", /\b(queue|backlog|consumer lag|enqueue)\b/i],
|
|
95387
|
+
["worker-slowdown", /\b(worker|consumer)\b/i],
|
|
95388
|
+
["benign-variance", /\b(per[- ]segment|per[- ]region|skew(?:ed)?|variance|expected variation|noise)\b/i]
|
|
95389
|
+
];
|
|
95390
|
+
const out2 = [];
|
|
95391
|
+
for (const [category, cue] of CATEGORY_CUES) {
|
|
95392
|
+
if (cue.test(hint)) out2.push(category);
|
|
95393
|
+
}
|
|
95394
|
+
return out2;
|
|
95395
|
+
}
|
|
95396
|
+
function formatDurationMs(ms) {
|
|
95397
|
+
if (!Number.isFinite(ms) || ms < 0) return "?";
|
|
95398
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
95399
|
+
const totalSec = ms / 1e3;
|
|
95400
|
+
if (totalSec < 60) {
|
|
95401
|
+
const s5 = Math.round(totalSec * 10) / 10;
|
|
95402
|
+
return `${s5}s`;
|
|
95403
|
+
}
|
|
95404
|
+
const totalMin = Math.floor(totalSec / 60);
|
|
95405
|
+
const remSec = Math.round(totalSec % 60);
|
|
95406
|
+
if (totalMin < 60) {
|
|
95407
|
+
return remSec > 0 ? `${totalMin}m${remSec}s` : `${totalMin}m`;
|
|
95408
|
+
}
|
|
95409
|
+
const h3 = Math.floor(totalMin / 60);
|
|
95410
|
+
const remMin = totalMin % 60;
|
|
95411
|
+
return remMin > 0 ? `${h3}h${remMin}m` : `${h3}h`;
|
|
95412
|
+
}
|
|
95007
95413
|
var SOURCE_IMPACT_RE = [
|
|
95008
95414
|
/\bwhat depends on\b/i,
|
|
95009
95415
|
/\bblast[- ]?radius\b/i,
|
|
@@ -95154,6 +95560,7 @@ async function investigate(input, deps) {
|
|
|
95154
95560
|
const hint = input.hint.trim();
|
|
95155
95561
|
const intent = classifyIntent(hint, input.since);
|
|
95156
95562
|
const namedSymbols = parseNamedSymbols(hint);
|
|
95563
|
+
const alertSuggestedCategories = parseSuggestedCauses(hint);
|
|
95157
95564
|
const sourceImpactHint = intent === "source-impact";
|
|
95158
95565
|
const preferNamed = sourceImpactHint && namedSymbols.length > 0 ? namedSymbols[0] : void 0;
|
|
95159
95566
|
const evidence2 = [];
|
|
@@ -95478,6 +95885,7 @@ async function investigate(input, deps) {
|
|
|
95478
95885
|
const logEvIds = [];
|
|
95479
95886
|
const directLogEvIds = [];
|
|
95480
95887
|
const ambientLogEvIds = [];
|
|
95888
|
+
const perDimensionDurationEvIds = [];
|
|
95481
95889
|
const directSignatures = [];
|
|
95482
95890
|
const directEvIdByKey = /* @__PURE__ */ new Map();
|
|
95483
95891
|
const seedEmittedJoins = [];
|
|
@@ -95896,6 +96304,55 @@ async function investigate(input, deps) {
|
|
|
95896
96304
|
axiomCollected = false;
|
|
95897
96305
|
}
|
|
95898
96306
|
}
|
|
96307
|
+
if (looksDurationThemed(hint)) {
|
|
96308
|
+
const from = logWindowFrom(input.logsSince ?? input.since);
|
|
96309
|
+
const baseDurationOpts = input.durationDimension ?? {
|
|
96310
|
+
// Conservative default for the common `Completed <JOB>:<REGION> ~2m10s` shape: extract an
|
|
96311
|
+
// alphanumeric token after a colon as the dimension, scoped to completion lines, parsing
|
|
96312
|
+
// the duration out of the message. A non-matching corpus yields null (graceful).
|
|
96313
|
+
dimension: { name: "region", pattern: ":\\s*([A-Za-z][\\w-]+)" },
|
|
96314
|
+
completionText: "Completed"
|
|
96315
|
+
};
|
|
96316
|
+
const durationOpts = {
|
|
96317
|
+
...baseDurationOpts,
|
|
96318
|
+
from: baseDurationOpts.from ?? from,
|
|
96319
|
+
...input.service !== void 0 && baseDurationOpts.service === void 0 ? { service: input.service } : {},
|
|
96320
|
+
level: baseDurationOpts.level ?? "info"
|
|
96321
|
+
};
|
|
96322
|
+
const durationProviders = [];
|
|
96323
|
+
if (deps.logs && typeof deps.logs.analyzeDurations === "function") {
|
|
96324
|
+
durationProviders.push({ source: "elasticsearch", analyze: (o5) => deps.logs.analyzeDurations(o5) });
|
|
96325
|
+
}
|
|
96326
|
+
if (deps.axiom && typeof deps.axiom.analyzeDurations === "function") {
|
|
96327
|
+
durationProviders.push({ source: "axiom", analyze: (o5) => deps.axiom.analyzeDurations(o5) });
|
|
96328
|
+
}
|
|
96329
|
+
for (const provider of durationProviders) {
|
|
96330
|
+
try {
|
|
96331
|
+
const result = await provider.analyze(durationOpts);
|
|
96332
|
+
if (result === null) continue;
|
|
96333
|
+
const values2 = Object.entries(result.byValue);
|
|
96334
|
+
if (values2.length === 0) continue;
|
|
96335
|
+
const summary2 = values2.sort((a2, b5) => b5[1].avg - a2[1].avg).map(([val, st3]) => `${val} ${formatDurationMs(st3.avg)}`).join(", ");
|
|
96336
|
+
const ev = mkEv(
|
|
96337
|
+
"log",
|
|
96338
|
+
`Duration by ${result.dimension}: ${summary2}`.slice(0, 220),
|
|
96339
|
+
{
|
|
96340
|
+
source: provider.source,
|
|
96341
|
+
kind: "duration-by-dimension",
|
|
96342
|
+
dimension: result.dimension,
|
|
96343
|
+
unit: result.unit,
|
|
96344
|
+
byValue: result.byValue,
|
|
96345
|
+
sampleCount: result.sampleCount
|
|
96346
|
+
},
|
|
96347
|
+
{},
|
|
96348
|
+
void 0,
|
|
96349
|
+
0.6
|
|
96350
|
+
);
|
|
96351
|
+
perDimensionDurationEvIds.push(ev.id);
|
|
96352
|
+
} catch {
|
|
96353
|
+
}
|
|
96354
|
+
}
|
|
96355
|
+
}
|
|
95899
96356
|
let stateAnalysis = null;
|
|
95900
96357
|
const stateEvIds = [];
|
|
95901
96358
|
const stateCollections = /* @__PURE__ */ new Set();
|
|
@@ -95986,6 +96443,7 @@ async function investigate(input, deps) {
|
|
|
95986
96443
|
const METRICS_TIMEOUT_MS = 3e4;
|
|
95987
96444
|
const metricEvIds = [];
|
|
95988
96445
|
const latencyMetricEvIds = [];
|
|
96446
|
+
const bimodalMetricEvIds = [];
|
|
95989
96447
|
const queueMetricEvIds = [];
|
|
95990
96448
|
const queueMetricEvIdsByQueue = /* @__PURE__ */ new Map();
|
|
95991
96449
|
const nominalMetricEvIds = [];
|
|
@@ -96037,6 +96495,8 @@ async function investigate(input, deps) {
|
|
|
96037
96495
|
);
|
|
96038
96496
|
const genericPerf = serviceFilter.length === 0 && hintIsPerf && f5.anomaly === "latency-spike";
|
|
96039
96497
|
if (matchesService || matchesHint || genericPerf) latencyMetricEvIds.push(ev.id);
|
|
96498
|
+
} else if (f5.anomaly === "bimodal-population") {
|
|
96499
|
+
bimodalMetricEvIds.push(ev.id);
|
|
96040
96500
|
} else if (f5.anomaly === "queue-growth") {
|
|
96041
96501
|
const panelNorm = f5.panelTitle.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
96042
96502
|
const labelNorms = Object.values(f5.labels).map(
|
|
@@ -96107,7 +96567,13 @@ async function investigate(input, deps) {
|
|
|
96107
96567
|
sinceProvided: input.since !== void 0,
|
|
96108
96568
|
suppressRuntimeHypotheses: sourceImpactHint,
|
|
96109
96569
|
recentChangeRelevant,
|
|
96110
|
-
graph
|
|
96570
|
+
graph,
|
|
96571
|
+
// HOR-435: per-segment duration breakdown (#2) + bimodal-population metric (#3) support
|
|
96572
|
+
// the benign-variance hypothesis (#4); the suggested categories de-anchor alert text (#1).
|
|
96573
|
+
perDimensionDurationEvIds,
|
|
96574
|
+
bimodalMetricEvIds,
|
|
96575
|
+
benignVarianceApplicable: looksDurationThemed(hint) || perDimensionDurationEvIds.length > 0 || bimodalMetricEvIds.length > 0 || latencyMetricEvIds.length > 0,
|
|
96576
|
+
alertSuggestedCategories
|
|
96111
96577
|
});
|
|
96112
96578
|
const validated = validateHypotheses(hyps, evidence2);
|
|
96113
96579
|
const causeChains = buildCauseChains(validated, evidence2, graph, label);
|
|
@@ -119235,6 +119701,8 @@ function anomalyLabel2(anomaly) {
|
|
|
119235
119701
|
return "QUEUE-GROWTH";
|
|
119236
119702
|
case "change":
|
|
119237
119703
|
return "CHANGE";
|
|
119704
|
+
case "bimodal-population":
|
|
119705
|
+
return "BIMODAL-POPULATION";
|
|
119238
119706
|
case "none":
|
|
119239
119707
|
return "OK";
|
|
119240
119708
|
}
|
|
@@ -121374,6 +121842,120 @@ function extractPort2(hostUrl) {
|
|
|
121374
121842
|
init_cjs_shims();
|
|
121375
121843
|
var import_picocolors44 = __toESM(require_picocolors(), 1);
|
|
121376
121844
|
var DEFAULT_DB_URL4 = "postgresql://horus:horus@localhost:5433/horus";
|
|
121845
|
+
var CONNECTOR_CHECKS = {
|
|
121846
|
+
// Elasticsearch (runtime logs)
|
|
121847
|
+
elasticsearch: {
|
|
121848
|
+
label: "Elasticsearch",
|
|
121849
|
+
absentNext: "add connectors.elasticsearch to an environment in your Horus config for runtime log evidence",
|
|
121850
|
+
check: (es, ctx) => !es.indexPattern ? {
|
|
121851
|
+
label: "Elasticsearch",
|
|
121852
|
+
status: "warn",
|
|
121853
|
+
detail: `${ctx} \u2014 indexPattern not set`,
|
|
121854
|
+
next: "set indexPattern in connectors.elasticsearch"
|
|
121855
|
+
} : {
|
|
121856
|
+
label: "Elasticsearch",
|
|
121857
|
+
status: "pass",
|
|
121858
|
+
detail: `${ctx} \u2014 ${es.indexPattern} [runtime ingestion pending]`
|
|
121859
|
+
}
|
|
121860
|
+
},
|
|
121861
|
+
// Grafana (metrics)
|
|
121862
|
+
grafana: {
|
|
121863
|
+
label: "Grafana",
|
|
121864
|
+
absentNext: "add connectors.grafana to an environment for metric evidence",
|
|
121865
|
+
check: (g4, ctx) => !g4.url ? {
|
|
121866
|
+
label: "Grafana",
|
|
121867
|
+
status: "warn",
|
|
121868
|
+
detail: `${ctx} \u2014 URL not set`,
|
|
121869
|
+
next: "set grafana.url or grafana.urlEnv in your Horus config"
|
|
121870
|
+
} : {
|
|
121871
|
+
label: "Grafana",
|
|
121872
|
+
status: "pass",
|
|
121873
|
+
detail: `${ctx} \u2014 URL configured${g4.dashboard ? ` (${g4.dashboard})` : ""} [runtime ingestion pending]`
|
|
121874
|
+
}
|
|
121875
|
+
},
|
|
121876
|
+
// MongoDB (state)
|
|
121877
|
+
mongodb: {
|
|
121878
|
+
label: "MongoDB",
|
|
121879
|
+
absentNext: "add connectors.mongodb to an environment for database state evidence",
|
|
121880
|
+
check: (m5, ctx) => !m5.url ? {
|
|
121881
|
+
label: "MongoDB",
|
|
121882
|
+
status: "warn",
|
|
121883
|
+
detail: `${ctx} \u2014 URL not set`,
|
|
121884
|
+
next: "set mongodb.url or mongodb.urlEnv in your Horus config"
|
|
121885
|
+
} : {
|
|
121886
|
+
label: "MongoDB",
|
|
121887
|
+
status: "pass",
|
|
121888
|
+
detail: `${ctx} \u2014 ${m5.database} [runtime ingestion pending]`
|
|
121889
|
+
}
|
|
121890
|
+
},
|
|
121891
|
+
// Postgres (state)
|
|
121892
|
+
postgres: {
|
|
121893
|
+
label: "Postgres",
|
|
121894
|
+
absentNext: "add connectors.postgres to an environment for database state evidence",
|
|
121895
|
+
check: (p5, ctx) => !p5.url ? {
|
|
121896
|
+
label: "Postgres",
|
|
121897
|
+
status: "warn",
|
|
121898
|
+
detail: `${ctx} \u2014 URL not set`,
|
|
121899
|
+
next: "set postgres.url or postgres.urlEnv in your Horus config"
|
|
121900
|
+
} : {
|
|
121901
|
+
label: "Postgres",
|
|
121902
|
+
status: "pass",
|
|
121903
|
+
detail: `${ctx} \u2014 ${p5.database ?? p5.schema ?? "postgres"} [runtime ingestion pending]`
|
|
121904
|
+
}
|
|
121905
|
+
},
|
|
121906
|
+
// Sentry (error tracking)
|
|
121907
|
+
sentry: {
|
|
121908
|
+
label: "Sentry",
|
|
121909
|
+
absentNext: "add connectors.sentry to an environment for error-tracking evidence",
|
|
121910
|
+
check: (s5, ctx) => {
|
|
121911
|
+
const tokenSet = s5.authToken || process.env[s5.authTokenEnv ?? "SENTRY_AUTH_TOKEN"];
|
|
121912
|
+
return !tokenSet ? {
|
|
121913
|
+
label: "Sentry",
|
|
121914
|
+
status: "warn",
|
|
121915
|
+
detail: `${ctx} \u2014 auth token not set`,
|
|
121916
|
+
next: "set sentry.authToken or sentry.authTokenEnv (default SENTRY_AUTH_TOKEN) in your Horus config"
|
|
121917
|
+
} : {
|
|
121918
|
+
label: "Sentry",
|
|
121919
|
+
status: "pass",
|
|
121920
|
+
detail: `${ctx} \u2014 ${s5.org}/${s5.project} [runtime ingestion pending]`
|
|
121921
|
+
};
|
|
121922
|
+
}
|
|
121923
|
+
},
|
|
121924
|
+
// Axiom (structured logs) — HOR-437: previously omitted from doctor entirely.
|
|
121925
|
+
axiom: {
|
|
121926
|
+
label: "Axiom",
|
|
121927
|
+
absentNext: "add connectors.axiom to an environment for structured log evidence",
|
|
121928
|
+
check: (a2, ctx) => {
|
|
121929
|
+
const tokenSet = a2.token || process.env[a2.tokenEnv ?? "AXIOM_TOKEN"];
|
|
121930
|
+
return !tokenSet ? {
|
|
121931
|
+
label: "Axiom",
|
|
121932
|
+
status: "warn",
|
|
121933
|
+
detail: `${ctx} \u2014 API token not set`,
|
|
121934
|
+
next: "set axiom.token or axiom.tokenEnv (default AXIOM_TOKEN) in your Horus config"
|
|
121935
|
+
} : {
|
|
121936
|
+
label: "Axiom",
|
|
121937
|
+
status: "pass",
|
|
121938
|
+
detail: `${ctx} \u2014 ${a2.dataset} [runtime ingestion pending]`
|
|
121939
|
+
};
|
|
121940
|
+
}
|
|
121941
|
+
},
|
|
121942
|
+
// Redis / BullMQ (queue + cache/state)
|
|
121943
|
+
redis: {
|
|
121944
|
+
label: "Redis",
|
|
121945
|
+
absentNext: "add connectors.redis to an environment for queue state evidence",
|
|
121946
|
+
check: (r, ctx) => !r.url ? {
|
|
121947
|
+
label: "Redis",
|
|
121948
|
+
status: "warn",
|
|
121949
|
+
detail: `${ctx} \u2014 URL not set`,
|
|
121950
|
+
next: "set redis.url or redis.urlEnv in your Horus config"
|
|
121951
|
+
} : {
|
|
121952
|
+
label: "Redis",
|
|
121953
|
+
status: "pass",
|
|
121954
|
+
detail: `${ctx} \u2014 URL configured [runtime ingestion pending]`
|
|
121955
|
+
}
|
|
121956
|
+
}
|
|
121957
|
+
};
|
|
121958
|
+
var DOCTOR_CONNECTOR_KEYS = Object.keys(CONNECTOR_CHECKS);
|
|
121377
121959
|
function mark2(status) {
|
|
121378
121960
|
if (status === "pass") return import_picocolors44.default.green("\u2713");
|
|
121379
121961
|
if (status === "warn") return import_picocolors44.default.yellow("~");
|
|
@@ -121475,168 +122057,28 @@ async function runDoctor(opts) {
|
|
|
121475
122057
|
}
|
|
121476
122058
|
try {
|
|
121477
122059
|
const globalConfig2 = await loadConfig(opts?.config, { cwd });
|
|
121478
|
-
|
|
121479
|
-
let anyGrafana = false;
|
|
121480
|
-
let anyMongo = false;
|
|
121481
|
-
let anyPostgres = false;
|
|
121482
|
-
let anySentry = false;
|
|
121483
|
-
let anyRedis = false;
|
|
122060
|
+
const configured = /* @__PURE__ */ new Set();
|
|
121484
122061
|
for (const project of globalConfig2.projects) {
|
|
121485
122062
|
for (const env of project.environments) {
|
|
121486
122063
|
const ctx = `${project.name}/${env.name}`;
|
|
121487
122064
|
const c4 = env.connectors;
|
|
121488
|
-
|
|
121489
|
-
|
|
121490
|
-
if (
|
|
121491
|
-
|
|
121492
|
-
|
|
121493
|
-
|
|
121494
|
-
detail: `${ctx} \u2014 indexPattern not set`,
|
|
121495
|
-
next: "set indexPattern in connectors.elasticsearch"
|
|
121496
|
-
});
|
|
121497
|
-
} else {
|
|
121498
|
-
checks.push({
|
|
121499
|
-
label: "Elasticsearch",
|
|
121500
|
-
status: "pass",
|
|
121501
|
-
detail: `${ctx} \u2014 ${c4.elasticsearch.indexPattern} [runtime ingestion pending]`
|
|
121502
|
-
});
|
|
121503
|
-
}
|
|
121504
|
-
}
|
|
121505
|
-
if (c4.grafana) {
|
|
121506
|
-
anyGrafana = true;
|
|
121507
|
-
if (!c4.grafana.url) {
|
|
121508
|
-
checks.push({
|
|
121509
|
-
label: "Grafana",
|
|
121510
|
-
status: "warn",
|
|
121511
|
-
detail: `${ctx} \u2014 URL not set`,
|
|
121512
|
-
next: "set grafana.url or grafana.urlEnv in your Horus config"
|
|
121513
|
-
});
|
|
121514
|
-
} else {
|
|
121515
|
-
const dashHint = c4.grafana.dashboard ? ` (${c4.grafana.dashboard})` : "";
|
|
121516
|
-
checks.push({
|
|
121517
|
-
label: "Grafana",
|
|
121518
|
-
status: "pass",
|
|
121519
|
-
detail: `${ctx} \u2014 URL configured${dashHint} [runtime ingestion pending]`
|
|
121520
|
-
});
|
|
121521
|
-
}
|
|
121522
|
-
}
|
|
121523
|
-
if (c4.mongodb) {
|
|
121524
|
-
anyMongo = true;
|
|
121525
|
-
if (!c4.mongodb.url) {
|
|
121526
|
-
checks.push({
|
|
121527
|
-
label: "MongoDB",
|
|
121528
|
-
status: "warn",
|
|
121529
|
-
detail: `${ctx} \u2014 URL not set`,
|
|
121530
|
-
next: "set mongodb.url or mongodb.urlEnv in your Horus config"
|
|
121531
|
-
});
|
|
121532
|
-
} else {
|
|
121533
|
-
checks.push({
|
|
121534
|
-
label: "MongoDB",
|
|
121535
|
-
status: "pass",
|
|
121536
|
-
detail: `${ctx} \u2014 ${c4.mongodb.database} [runtime ingestion pending]`
|
|
121537
|
-
});
|
|
121538
|
-
}
|
|
121539
|
-
}
|
|
121540
|
-
if (c4.postgres) {
|
|
121541
|
-
anyPostgres = true;
|
|
121542
|
-
if (!c4.postgres.url) {
|
|
121543
|
-
checks.push({
|
|
121544
|
-
label: "Postgres",
|
|
121545
|
-
status: "warn",
|
|
121546
|
-
detail: `${ctx} \u2014 URL not set`,
|
|
121547
|
-
next: "set postgres.url or postgres.urlEnv in your Horus config"
|
|
121548
|
-
});
|
|
121549
|
-
} else {
|
|
121550
|
-
checks.push({
|
|
121551
|
-
label: "Postgres",
|
|
121552
|
-
status: "pass",
|
|
121553
|
-
detail: `${ctx} \u2014 ${c4.postgres.database ?? c4.postgres.schema ?? "postgres"} [runtime ingestion pending]`
|
|
121554
|
-
});
|
|
121555
|
-
}
|
|
121556
|
-
}
|
|
121557
|
-
if (c4.sentry) {
|
|
121558
|
-
anySentry = true;
|
|
121559
|
-
const tokenSet = c4.sentry.authToken || process.env[c4.sentry.authTokenEnv ?? "SENTRY_AUTH_TOKEN"];
|
|
121560
|
-
if (!tokenSet) {
|
|
121561
|
-
checks.push({
|
|
121562
|
-
label: "Sentry",
|
|
121563
|
-
status: "warn",
|
|
121564
|
-
detail: `${ctx} \u2014 auth token not set`,
|
|
121565
|
-
next: "set sentry.authToken or sentry.authTokenEnv (default SENTRY_AUTH_TOKEN) in your Horus config"
|
|
121566
|
-
});
|
|
121567
|
-
} else {
|
|
121568
|
-
checks.push({
|
|
121569
|
-
label: "Sentry",
|
|
121570
|
-
status: "pass",
|
|
121571
|
-
detail: `${ctx} \u2014 ${c4.sentry.org}/${c4.sentry.project} [runtime ingestion pending]`
|
|
121572
|
-
});
|
|
121573
|
-
}
|
|
121574
|
-
}
|
|
121575
|
-
if (c4.redis) {
|
|
121576
|
-
anyRedis = true;
|
|
121577
|
-
if (!c4.redis.url) {
|
|
121578
|
-
checks.push({
|
|
121579
|
-
label: "Redis",
|
|
121580
|
-
status: "warn",
|
|
121581
|
-
detail: `${ctx} \u2014 URL not set`,
|
|
121582
|
-
next: "set redis.url or redis.urlEnv in your Horus config"
|
|
121583
|
-
});
|
|
121584
|
-
} else {
|
|
121585
|
-
checks.push({
|
|
121586
|
-
label: "Redis",
|
|
121587
|
-
status: "pass",
|
|
121588
|
-
detail: `${ctx} \u2014 URL configured [runtime ingestion pending]`
|
|
121589
|
-
});
|
|
121590
|
-
}
|
|
122065
|
+
for (const key of DOCTOR_CONNECTOR_KEYS) {
|
|
122066
|
+
const cfg = c4[key];
|
|
122067
|
+
if (cfg === void 0) continue;
|
|
122068
|
+
configured.add(key);
|
|
122069
|
+
const run3 = CONNECTOR_CHECKS[key].check;
|
|
122070
|
+
checks.push(run3(cfg, ctx));
|
|
121591
122071
|
}
|
|
121592
122072
|
}
|
|
121593
122073
|
}
|
|
121594
|
-
|
|
122074
|
+
for (const key of DOCTOR_CONNECTOR_KEYS) {
|
|
122075
|
+
if (configured.has(key)) continue;
|
|
122076
|
+
const desc2 = CONNECTOR_CHECKS[key];
|
|
121595
122077
|
checks.push({
|
|
121596
|
-
label:
|
|
121597
|
-
status: "warn",
|
|
121598
|
-
detail: "not configured",
|
|
121599
|
-
next: "add connectors.elasticsearch to an environment in your Horus config for runtime log evidence"
|
|
121600
|
-
});
|
|
121601
|
-
}
|
|
121602
|
-
if (!anyGrafana) {
|
|
121603
|
-
checks.push({
|
|
121604
|
-
label: "Grafana",
|
|
121605
|
-
status: "warn",
|
|
121606
|
-
detail: "not configured",
|
|
121607
|
-
next: "add connectors.grafana to an environment for metric evidence"
|
|
121608
|
-
});
|
|
121609
|
-
}
|
|
121610
|
-
if (!anyMongo) {
|
|
121611
|
-
checks.push({
|
|
121612
|
-
label: "MongoDB",
|
|
121613
|
-
status: "warn",
|
|
121614
|
-
detail: "not configured",
|
|
121615
|
-
next: "add connectors.mongodb to an environment for database state evidence"
|
|
121616
|
-
});
|
|
121617
|
-
}
|
|
121618
|
-
if (!anyPostgres) {
|
|
121619
|
-
checks.push({
|
|
121620
|
-
label: "Postgres",
|
|
121621
|
-
status: "warn",
|
|
121622
|
-
detail: "not configured",
|
|
121623
|
-
next: "add connectors.postgres to an environment for database state evidence"
|
|
121624
|
-
});
|
|
121625
|
-
}
|
|
121626
|
-
if (!anySentry) {
|
|
121627
|
-
checks.push({
|
|
121628
|
-
label: "Sentry",
|
|
121629
|
-
status: "warn",
|
|
121630
|
-
detail: "not configured",
|
|
121631
|
-
next: "add connectors.sentry to an environment for error-tracking evidence"
|
|
121632
|
-
});
|
|
121633
|
-
}
|
|
121634
|
-
if (!anyRedis) {
|
|
121635
|
-
checks.push({
|
|
121636
|
-
label: "Redis",
|
|
122078
|
+
label: desc2.label,
|
|
121637
122079
|
status: "warn",
|
|
121638
122080
|
detail: "not configured",
|
|
121639
|
-
next:
|
|
122081
|
+
next: desc2.absentNext
|
|
121640
122082
|
});
|
|
121641
122083
|
}
|
|
121642
122084
|
} catch {
|