@merittdev/horus 0.10.0 → 0.11.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 +745 -19
- 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.
|
|
65883
|
+
var HORUS_VERSION = true ? "0.11.0" : "dev";
|
|
65884
65884
|
var PINNED_SOURCE_VERSION = "1.6.1";
|
|
65885
65885
|
|
|
65886
65886
|
// ../../packages/core/src/evidence.ts
|
|
@@ -70251,6 +70251,22 @@ var connectorsSchema = external_exports.object({
|
|
|
70251
70251
|
/** Env var holding the base URL. Defaults to "SENTRY_URL". */
|
|
70252
70252
|
urlEnv: external_exports.string().optional()
|
|
70253
70253
|
}).optional(),
|
|
70254
|
+
/**
|
|
70255
|
+
* Axiom logs connector (HOR-429). Structured log rows queried with APL become
|
|
70256
|
+
* `kind: 'log'` evidence (message + level + fields), alongside Elasticsearch.
|
|
70257
|
+
*/
|
|
70258
|
+
axiom: external_exports.object({
|
|
70259
|
+
/** Dataset to query (the Axiom analogue of an ES index). */
|
|
70260
|
+
dataset: external_exports.string(),
|
|
70261
|
+
/** Direct API token (takes priority over tokenEnv). */
|
|
70262
|
+
token: external_exports.string().optional(),
|
|
70263
|
+
/** Env var holding the API token. Defaults to "AXIOM_TOKEN". */
|
|
70264
|
+
tokenEnv: external_exports.string().optional(),
|
|
70265
|
+
/** Direct base URL (US default https://api.axiom.co; EU https://api.eu.axiom.co). Takes priority over urlEnv. */
|
|
70266
|
+
url: external_exports.string().optional(),
|
|
70267
|
+
/** Env var holding the base URL. Defaults to "AXIOM_URL". */
|
|
70268
|
+
urlEnv: external_exports.string().optional()
|
|
70269
|
+
}).optional(),
|
|
70254
70270
|
grafana: external_exports.object({
|
|
70255
70271
|
dashboard: external_exports.string().optional(),
|
|
70256
70272
|
/** Multiple dashboard UIDs to fetch. Takes precedence over `dashboard` when set. */
|
|
@@ -70469,6 +70485,16 @@ function resolveEnvironment(config2, opts) {
|
|
|
70469
70485
|
...url !== void 0 ? { url } : {}
|
|
70470
70486
|
};
|
|
70471
70487
|
}
|
|
70488
|
+
if (c4.axiom !== void 0) {
|
|
70489
|
+
const a2 = c4.axiom;
|
|
70490
|
+
const token = a2.token ?? process.env[a2.tokenEnv ?? "AXIOM_TOKEN"];
|
|
70491
|
+
const url = a2.url ?? process.env[a2.urlEnv ?? "AXIOM_URL"];
|
|
70492
|
+
resolved.axiom = {
|
|
70493
|
+
dataset: a2.dataset,
|
|
70494
|
+
...token !== void 0 ? { token } : {},
|
|
70495
|
+
...url !== void 0 ? { url } : {}
|
|
70496
|
+
};
|
|
70497
|
+
}
|
|
70472
70498
|
if (c4.grafana !== void 0) {
|
|
70473
70499
|
const g4 = c4.grafana;
|
|
70474
70500
|
resolved.grafana = {
|
|
@@ -73724,6 +73750,296 @@ function shortTs2(iso) {
|
|
|
73724
73750
|
return `${iso.slice(5, 10)} ${iso.slice(11, 16)}`;
|
|
73725
73751
|
}
|
|
73726
73752
|
|
|
73753
|
+
// ../../packages/connectors/src/axiom/client.ts
|
|
73754
|
+
init_cjs_shims();
|
|
73755
|
+
var REQUEST_TIMEOUT_MS2 = 8e3;
|
|
73756
|
+
var APL_PATH = "/v1/datasets/_apl?format=tabular";
|
|
73757
|
+
var AxiomClient = class {
|
|
73758
|
+
baseUrl;
|
|
73759
|
+
token;
|
|
73760
|
+
dataset;
|
|
73761
|
+
constructor(opts) {
|
|
73762
|
+
this.baseUrl = (opts.baseUrl ?? "https://api.axiom.co").replace(/\/$/, "");
|
|
73763
|
+
this.token = opts.token;
|
|
73764
|
+
this.dataset = opts.dataset;
|
|
73765
|
+
}
|
|
73766
|
+
async request(method, path, body2) {
|
|
73767
|
+
const url = `${this.baseUrl}${path}`;
|
|
73768
|
+
const init2 = {
|
|
73769
|
+
method,
|
|
73770
|
+
headers: {
|
|
73771
|
+
Authorization: `Bearer ${this.token}`,
|
|
73772
|
+
"Content-Type": "application/json"
|
|
73773
|
+
},
|
|
73774
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
|
|
73775
|
+
};
|
|
73776
|
+
if (body2 !== void 0) init2.body = JSON.stringify(body2);
|
|
73777
|
+
const res = await fetch(url, init2);
|
|
73778
|
+
if (!res.ok) {
|
|
73779
|
+
const text2 = await res.text().catch(() => "");
|
|
73780
|
+
throw new Error(`Axiom ${method} ${path} -> ${res.status}: ${text2.slice(0, 200)}`);
|
|
73781
|
+
}
|
|
73782
|
+
return res.json();
|
|
73783
|
+
}
|
|
73784
|
+
/**
|
|
73785
|
+
* List the datasets reachable with the configured token. Returns [] on any error so
|
|
73786
|
+
* callers degrade gracefully (never throws past here).
|
|
73787
|
+
*/
|
|
73788
|
+
async listDatasets() {
|
|
73789
|
+
try {
|
|
73790
|
+
const raw = await this.request("GET", "/v1/datasets");
|
|
73791
|
+
if (!Array.isArray(raw)) return [];
|
|
73792
|
+
return raw.map(parseDataset);
|
|
73793
|
+
} catch {
|
|
73794
|
+
return [];
|
|
73795
|
+
}
|
|
73796
|
+
}
|
|
73797
|
+
/**
|
|
73798
|
+
* Run an APL query over the configured time window. `startTime`/`endTime` are ISO
|
|
73799
|
+
* strings and bound the query server-side. Returns parsed log records, or [] on any
|
|
73800
|
+
* failure (never throws past here).
|
|
73801
|
+
*/
|
|
73802
|
+
async query(apl, startTime, endTime) {
|
|
73803
|
+
try {
|
|
73804
|
+
const raw = await this.request("POST", APL_PATH, { apl, startTime, endTime });
|
|
73805
|
+
return parseTabular(raw);
|
|
73806
|
+
} catch {
|
|
73807
|
+
return [];
|
|
73808
|
+
}
|
|
73809
|
+
}
|
|
73810
|
+
/**
|
|
73811
|
+
* Cheap reachability probe: list datasets. Returns a structured status; never throws.
|
|
73812
|
+
*/
|
|
73813
|
+
async health() {
|
|
73814
|
+
try {
|
|
73815
|
+
await this.request("GET", "/v1/datasets");
|
|
73816
|
+
return { ok: true, detail: `axiom reachable (${this.dataset})` };
|
|
73817
|
+
} catch (err2) {
|
|
73818
|
+
return { ok: false, detail: err2.message };
|
|
73819
|
+
}
|
|
73820
|
+
}
|
|
73821
|
+
};
|
|
73822
|
+
function parseDataset(raw) {
|
|
73823
|
+
const ds = { name: String(raw["name"] ?? "") };
|
|
73824
|
+
if (typeof raw["description"] === "string" && raw["description"] !== "") {
|
|
73825
|
+
ds.description = raw["description"];
|
|
73826
|
+
}
|
|
73827
|
+
return ds;
|
|
73828
|
+
}
|
|
73829
|
+
var AXIOM_ERROR_LEVELS = ["error", "warn", "fatal"];
|
|
73830
|
+
function datasetRef(dataset) {
|
|
73831
|
+
return `['${dataset.replace(/'/g, "")}']`;
|
|
73832
|
+
}
|
|
73833
|
+
function searchTerms(hintTerms) {
|
|
73834
|
+
return hintTerms.map((t2) => t2.trim()).filter((t2) => t2.length > 2);
|
|
73835
|
+
}
|
|
73836
|
+
function searchClause(hintTerms) {
|
|
73837
|
+
const terms = searchTerms(hintTerms);
|
|
73838
|
+
return terms.length > 0 ? ` | search ${terms.map((t2) => `"${t2.replace(/"/g, "")}"`).join(" or ")}` : "";
|
|
73839
|
+
}
|
|
73840
|
+
function errorLevelClause() {
|
|
73841
|
+
const levels = AXIOM_ERROR_LEVELS.map((l2) => `'${l2}'`).join(", ");
|
|
73842
|
+
return ` | where tolower(tostring(level)) in (${levels})`;
|
|
73843
|
+
}
|
|
73844
|
+
function buildApl(dataset, hintTerms = [], limit = 100) {
|
|
73845
|
+
const lim = Math.max(1, Math.min(limit, 1e3));
|
|
73846
|
+
return `${datasetRef(dataset)}${searchClause(hintTerms)} | sort by _time desc | limit ${lim}`;
|
|
73847
|
+
}
|
|
73848
|
+
function buildErrorSignatureApl(dataset, hintTerms = [], topN = 10) {
|
|
73849
|
+
const lim = Math.max(1, Math.min(topN, 1e3));
|
|
73850
|
+
return `${datasetRef(dataset)}${errorLevelClause()}${searchClause(hintTerms)} | summarize ['count'] = count(), ['_time'] = max(_time) by message, level | sort by ['count'] desc | limit ${lim}`;
|
|
73851
|
+
}
|
|
73852
|
+
function buildRecentErrorsApl(dataset, hintTerms = [], limit = 5) {
|
|
73853
|
+
const lim = Math.max(1, Math.min(limit, 1e3));
|
|
73854
|
+
return `${datasetRef(dataset)}${errorLevelClause()}${searchClause(hintTerms)} | sort by _time desc | limit ${lim}`;
|
|
73855
|
+
}
|
|
73856
|
+
function parseTabular(raw) {
|
|
73857
|
+
if (raw === null || raw === void 0 || typeof raw !== "object") return [];
|
|
73858
|
+
const tables = raw["tables"];
|
|
73859
|
+
if (!Array.isArray(tables)) return [];
|
|
73860
|
+
const records = [];
|
|
73861
|
+
for (const table of tables) {
|
|
73862
|
+
const fields = table["fields"];
|
|
73863
|
+
if (!Array.isArray(fields)) continue;
|
|
73864
|
+
const names = fields.map((f5) => String(f5["name"] ?? ""));
|
|
73865
|
+
const columns = table["columns"];
|
|
73866
|
+
const rows = table["rows"];
|
|
73867
|
+
if (Array.isArray(columns)) {
|
|
73868
|
+
const first = columns[0];
|
|
73869
|
+
const rowCount = Array.isArray(first) ? first.length : 0;
|
|
73870
|
+
for (let r = 0; r < rowCount; r++) {
|
|
73871
|
+
const row = {};
|
|
73872
|
+
for (let c4 = 0; c4 < names.length; c4++) {
|
|
73873
|
+
const col = columns[c4];
|
|
73874
|
+
if (Array.isArray(col)) row[names[c4]] = col[r];
|
|
73875
|
+
}
|
|
73876
|
+
records.push(toRecord(row));
|
|
73877
|
+
}
|
|
73878
|
+
} else if (Array.isArray(rows)) {
|
|
73879
|
+
for (const rowVals of rows) {
|
|
73880
|
+
if (!Array.isArray(rowVals)) continue;
|
|
73881
|
+
const row = {};
|
|
73882
|
+
for (let c4 = 0; c4 < names.length; c4++) {
|
|
73883
|
+
row[names[c4]] = rowVals[c4];
|
|
73884
|
+
}
|
|
73885
|
+
records.push(toRecord(row));
|
|
73886
|
+
}
|
|
73887
|
+
}
|
|
73888
|
+
}
|
|
73889
|
+
return records;
|
|
73890
|
+
}
|
|
73891
|
+
function toRecord(fields) {
|
|
73892
|
+
const rec = { fields };
|
|
73893
|
+
const t2 = fields["_time"];
|
|
73894
|
+
if (typeof t2 === "string") rec.timestamp = t2;
|
|
73895
|
+
return rec;
|
|
73896
|
+
}
|
|
73897
|
+
|
|
73898
|
+
// ../../packages/connectors/src/axiom/provider.ts
|
|
73899
|
+
init_cjs_shims();
|
|
73900
|
+
var DEFAULT_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
73901
|
+
var AxiomProvider = class {
|
|
73902
|
+
constructor(client, opts) {
|
|
73903
|
+
this.client = client;
|
|
73904
|
+
this.opts = opts;
|
|
73905
|
+
}
|
|
73906
|
+
client;
|
|
73907
|
+
opts;
|
|
73908
|
+
id = "axiom";
|
|
73909
|
+
kind = "logs";
|
|
73910
|
+
/**
|
|
73911
|
+
* Collect log rows over the window, INCIDENT-AWARE. Rather than a broad newest-first
|
|
73912
|
+
* scan (which on a noisy dataset returns the 15 freshest `info` rows and misses a
|
|
73913
|
+
* high-volume error storm), this runs two error-biased queries in parallel:
|
|
73914
|
+
*
|
|
73915
|
+
* 1. TOP ERROR SIGNATURES — `summarize count() + max(_time) by message, level` over
|
|
73916
|
+
* error/warn/fatal rows (AND the hint terms), highest-volume first. This is what
|
|
73917
|
+
* surfaces a 3k-row `level=error` signature that a newest-first scan buries.
|
|
73918
|
+
* 2. RECENT RAW ERRORS — a few newest error rows with full fields, for grounding.
|
|
73919
|
+
*
|
|
73920
|
+
* Signatures lead so high-volume errors aren't crowded out. When NO error-level row
|
|
73921
|
+
* matches (neither query returns anything) it broadens to the all-levels fallback so
|
|
73922
|
+
* non-error datasets still produce evidence. Best-effort: a failing query yields [].
|
|
73923
|
+
* Never throws.
|
|
73924
|
+
*/
|
|
73925
|
+
async collect(opts = {}) {
|
|
73926
|
+
const to = opts.to ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
73927
|
+
const from = opts.from ?? new Date(Date.parse(to) - (this.opts.windowMs ?? DEFAULT_WINDOW_MS)).toISOString();
|
|
73928
|
+
const hintTerms = opts.hintTerms ?? [];
|
|
73929
|
+
const limit = this.opts.limit ?? 100;
|
|
73930
|
+
const topN = Math.max(1, Math.min(limit, 10));
|
|
73931
|
+
const recentN = Math.max(1, Math.min(limit, 5));
|
|
73932
|
+
const [signatures, recent] = await Promise.all([
|
|
73933
|
+
this.client.query(buildErrorSignatureApl(this.opts.dataset, hintTerms, topN), from, to),
|
|
73934
|
+
this.client.query(buildRecentErrorsApl(this.opts.dataset, hintTerms, recentN), from, to)
|
|
73935
|
+
]);
|
|
73936
|
+
if (signatures.length === 0 && recent.length === 0) {
|
|
73937
|
+
return this.client.query(buildApl(this.opts.dataset, hintTerms, limit), from, to);
|
|
73938
|
+
}
|
|
73939
|
+
return [...signatures, ...recent];
|
|
73940
|
+
}
|
|
73941
|
+
/**
|
|
73942
|
+
* Synthesize Evidence from Axiom rows. One `kind: 'log'` Evidence per row, with the
|
|
73943
|
+
* row fields on the payload (under `source: 'axiom'`), a redacted message in the
|
|
73944
|
+
* title, and relevance weighted by hint-term match / recency / level.
|
|
73945
|
+
*/
|
|
73946
|
+
toEvidence(records, hintTerms, collectedAt) {
|
|
73947
|
+
const query = `axiom ${this.opts.dataset}`;
|
|
73948
|
+
return records.map(
|
|
73949
|
+
(rec, i3) => this.recordToEvidence(rec, hintTerms, query, collectedAt, i3)
|
|
73950
|
+
);
|
|
73951
|
+
}
|
|
73952
|
+
/**
|
|
73953
|
+
* One-shot evidence query: collect rows + convert to Evidence. Preferred entry point
|
|
73954
|
+
* for the investigation engine. Degrades to [] on any failure so a flaky Axiom never
|
|
73955
|
+
* aborts an investigation.
|
|
73956
|
+
*/
|
|
73957
|
+
async queryEvidence(opts = {}) {
|
|
73958
|
+
try {
|
|
73959
|
+
const hintTerms = opts.hintTerms ?? [];
|
|
73960
|
+
const records = await this.collect({
|
|
73961
|
+
...opts.from !== void 0 ? { from: opts.from } : {},
|
|
73962
|
+
...opts.to !== void 0 ? { to: opts.to } : {},
|
|
73963
|
+
hintTerms
|
|
73964
|
+
});
|
|
73965
|
+
return this.toEvidence(records, hintTerms, opts.collectedAt ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
73966
|
+
} catch {
|
|
73967
|
+
return [];
|
|
73968
|
+
}
|
|
73969
|
+
}
|
|
73970
|
+
async health() {
|
|
73971
|
+
return this.client.health();
|
|
73972
|
+
}
|
|
73973
|
+
recordToEvidence(rec, hintTerms, query, collectedAt, index2) {
|
|
73974
|
+
const title = buildTitle2(rec, this.opts.dataset);
|
|
73975
|
+
const relevance = computeRelevance2(rec, hintTerms);
|
|
73976
|
+
const payload = {
|
|
73977
|
+
source: "axiom",
|
|
73978
|
+
dataset: this.opts.dataset,
|
|
73979
|
+
...rec.fields
|
|
73980
|
+
};
|
|
73981
|
+
const ev = {
|
|
73982
|
+
id: `ev_axiom_${index2}`,
|
|
73983
|
+
source: "logs",
|
|
73984
|
+
kind: "log",
|
|
73985
|
+
title,
|
|
73986
|
+
relevance,
|
|
73987
|
+
payload,
|
|
73988
|
+
links: {},
|
|
73989
|
+
provenance: { query, collectedAt }
|
|
73990
|
+
};
|
|
73991
|
+
if (rec.timestamp !== void 0) ev.timestamp = rec.timestamp;
|
|
73992
|
+
return ev;
|
|
73993
|
+
}
|
|
73994
|
+
};
|
|
73995
|
+
var MESSAGE_FIELDS = ["message", "msg", "body", "event", "log", "_raw"];
|
|
73996
|
+
var LEVEL_FIELDS = ["level", "severity", "lvl", "loglevel", "status"];
|
|
73997
|
+
function pickField2(fields, candidates) {
|
|
73998
|
+
for (const key of candidates) {
|
|
73999
|
+
const v4 = fields[key];
|
|
74000
|
+
if (typeof v4 === "string" && v4 !== "") return v4;
|
|
74001
|
+
if (typeof v4 === "number") return String(v4);
|
|
74002
|
+
}
|
|
74003
|
+
return void 0;
|
|
74004
|
+
}
|
|
74005
|
+
function buildTitle2(rec, dataset) {
|
|
74006
|
+
const msg = pickField2(rec.fields, MESSAGE_FIELDS);
|
|
74007
|
+
const level = pickField2(rec.fields, LEVEL_FIELDS);
|
|
74008
|
+
const lvl = level ? `[${level}] ` : "";
|
|
74009
|
+
const body2 = msg ? redactSecrets(msg) : "(log event)";
|
|
74010
|
+
const rawCount = rec.fields["count"];
|
|
74011
|
+
const count = typeof rawCount === "number" && Number.isFinite(rawCount) && rawCount > 1 ? rawCount : void 0;
|
|
74012
|
+
if (count !== void 0) {
|
|
74013
|
+
const latest = rec.timestamp ? ` (latest ${shortTs3(rec.timestamp)})` : "";
|
|
74014
|
+
return `Axiom ${dataset}: ${lvl}${body2} \xD7${count.toLocaleString("en-US")}${latest}`.slice(0, 220);
|
|
74015
|
+
}
|
|
74016
|
+
const ts = rec.timestamp ? ` \xB7 ${shortTs3(rec.timestamp)}` : "";
|
|
74017
|
+
return `Axiom ${dataset}: ${lvl}${body2}${ts}`.slice(0, 220);
|
|
74018
|
+
}
|
|
74019
|
+
function computeRelevance2(rec, hintTerms, now = Date.now()) {
|
|
74020
|
+
let score = 0.6;
|
|
74021
|
+
const hay = JSON.stringify(rec.fields).toLowerCase();
|
|
74022
|
+
const domainTerms = hintTerms.filter((t2) => t2.length > 2);
|
|
74023
|
+
if (domainTerms.some((t2) => hay.includes(t2.toLowerCase()))) score += 0.2;
|
|
74024
|
+
if (rec.timestamp) {
|
|
74025
|
+
const ageMs = now - Date.parse(rec.timestamp);
|
|
74026
|
+
if (Number.isFinite(ageMs)) {
|
|
74027
|
+
if (ageMs <= 864e5) score += 0.1;
|
|
74028
|
+
else if (ageMs <= 7 * 864e5) score += 0.05;
|
|
74029
|
+
}
|
|
74030
|
+
}
|
|
74031
|
+
const level = (pickField2(rec.fields, LEVEL_FIELDS) ?? "").toLowerCase();
|
|
74032
|
+
if (level === "error" || level === "fatal" || level === "critical") score += 0.1;
|
|
74033
|
+
else if (level === "warn" || level === "warning") score += 0.05;
|
|
74034
|
+
const count = rec.fields["count"];
|
|
74035
|
+
if (typeof count === "number" && count >= 10) score += 0.1;
|
|
74036
|
+
return Math.min(0.95, Math.max(0.5, score));
|
|
74037
|
+
}
|
|
74038
|
+
function shortTs3(iso) {
|
|
74039
|
+
if (!iso || iso.length < 16) return iso || "\u2014";
|
|
74040
|
+
return `${iso.slice(5, 10)} ${iso.slice(11, 16)}`;
|
|
74041
|
+
}
|
|
74042
|
+
|
|
73727
74043
|
// ../../packages/connectors/src/bullmq/client.ts
|
|
73728
74044
|
init_cjs_shims();
|
|
73729
74045
|
var import_ioredis = __toESM(require_built3(), 1);
|
|
@@ -74388,6 +74704,18 @@ function sentryForEnv(renv) {
|
|
|
74388
74704
|
{ org: s5.org, project: s5.project }
|
|
74389
74705
|
);
|
|
74390
74706
|
}
|
|
74707
|
+
function axiomForEnv(renv) {
|
|
74708
|
+
const a2 = renv.connectors.axiom;
|
|
74709
|
+
if (!a2 || !a2.token || !a2.dataset) return null;
|
|
74710
|
+
return new AxiomProvider(
|
|
74711
|
+
new AxiomClient({
|
|
74712
|
+
token: a2.token,
|
|
74713
|
+
dataset: a2.dataset,
|
|
74714
|
+
...a2.url !== void 0 ? { baseUrl: a2.url } : {}
|
|
74715
|
+
}),
|
|
74716
|
+
{ dataset: a2.dataset }
|
|
74717
|
+
);
|
|
74718
|
+
}
|
|
74391
74719
|
function codeForRepo(config2, repoName) {
|
|
74392
74720
|
const renv = resolveEnvironment(config2, { project: repoName });
|
|
74393
74721
|
const c4 = codeForEnv(renv);
|
|
@@ -74563,6 +74891,9 @@ init_cjs_shims();
|
|
|
74563
74891
|
// ../../packages/connectors/src/sentry/index.ts
|
|
74564
74892
|
init_cjs_shims();
|
|
74565
74893
|
|
|
74894
|
+
// ../../packages/connectors/src/axiom/index.ts
|
|
74895
|
+
init_cjs_shims();
|
|
74896
|
+
|
|
74566
74897
|
// ../../packages/connectors/src/bullmq/index.ts
|
|
74567
74898
|
init_cjs_shims();
|
|
74568
74899
|
|
|
@@ -90486,6 +90817,32 @@ async function checkEnv(renv, deps) {
|
|
|
90486
90817
|
` ${mark("pending")} ${import_picocolors3.default.bold("Sentry")} ${import_picocolors3.default.dim("not configured")}`
|
|
90487
90818
|
);
|
|
90488
90819
|
}
|
|
90820
|
+
const axiomCfg = renv.connectors.axiom;
|
|
90821
|
+
if (axiomCfg) {
|
|
90822
|
+
const label = axiomCfg.dataset ?? "(no dataset)";
|
|
90823
|
+
const axiom = (deps?.axiomFactory ?? axiomForEnv)(renv);
|
|
90824
|
+
if (axiom) {
|
|
90825
|
+
const h3 = await axiom.health();
|
|
90826
|
+
if (!h3.ok) {
|
|
90827
|
+
console.log(
|
|
90828
|
+
` ${mark(false)} ${import_picocolors3.default.bold("Axiom")} ${import_picocolors3.default.dim(`unreachable \xB7 ${label}`)}`
|
|
90829
|
+
);
|
|
90830
|
+
allOk = false;
|
|
90831
|
+
} else {
|
|
90832
|
+
console.log(
|
|
90833
|
+
` ${mark(true)} ${import_picocolors3.default.bold("Axiom")} ${import_picocolors3.default.dim(`reachable \xB7 ${label}`)}`
|
|
90834
|
+
);
|
|
90835
|
+
}
|
|
90836
|
+
} else {
|
|
90837
|
+
console.log(
|
|
90838
|
+
` ${mark(false)} ${import_picocolors3.default.bold("Axiom")} ${import_picocolors3.default.dim(`configured (${label}) but API token not set`)}`
|
|
90839
|
+
);
|
|
90840
|
+
}
|
|
90841
|
+
} else {
|
|
90842
|
+
console.log(
|
|
90843
|
+
` ${mark("pending")} ${import_picocolors3.default.bold("Axiom")} ${import_picocolors3.default.dim("not configured")}`
|
|
90844
|
+
);
|
|
90845
|
+
}
|
|
90489
90846
|
const redisCfg = renv.connectors.redis;
|
|
90490
90847
|
if (redisCfg?.url) {
|
|
90491
90848
|
const server = redisServerLabel(redisCfg.url);
|
|
@@ -92915,12 +93272,15 @@ function detectMissingEvidence(r, connectors = {}, mode) {
|
|
|
92915
93272
|
if (!hasLog && !sourceImpact) {
|
|
92916
93273
|
let logWhy;
|
|
92917
93274
|
let logNextSource;
|
|
92918
|
-
if (!connectors.elasticsearch && !connectors.sentry) {
|
|
92919
|
-
logWhy = "No Elasticsearch connector (nor Sentry) configured for this environment \u2014 no runtime error evidence.";
|
|
92920
|
-
logNextSource = "Add an `elasticsearch` and/or `
|
|
93275
|
+
if (!connectors.elasticsearch && !connectors.sentry && !connectors.axiom) {
|
|
93276
|
+
logWhy = "No Elasticsearch connector (nor Sentry / Axiom) configured for this environment \u2014 no runtime error evidence.";
|
|
93277
|
+
logNextSource = "Add an `elasticsearch`, `sentry`, and/or `axiom` connector to the project/environment";
|
|
92921
93278
|
} else if (!connectors.elasticsearch && connectors.sentry) {
|
|
92922
93279
|
logWhy = connectors.sentryCollected ? "No open Sentry issues matched in the window \u2014 cannot confirm the actual error signatures." : "Sentry collection failed or timed out \u2014 cannot confirm the actual error signatures.";
|
|
92923
93280
|
logNextSource = connectors.sentryCollected ? "Widen the window (--since) or check the Sentry project for open issues" : "Check the Sentry auth token / project, then retry";
|
|
93281
|
+
} else if (!connectors.elasticsearch && connectors.axiom) {
|
|
93282
|
+
logWhy = connectors.axiomCollected ? "No Axiom log rows matched in the window \u2014 cannot confirm the actual error signatures." : "Axiom log collection failed or timed out \u2014 cannot confirm the actual error signatures.";
|
|
93283
|
+
logNextSource = connectors.axiomCollected ? "Widen the window (--since) or check the Axiom dataset for matching rows" : "Check the Axiom API token / dataset, then retry";
|
|
92924
93284
|
} else if (connectors.logsCompatibilityError) {
|
|
92925
93285
|
logWhy = `Elasticsearch field mapping is incompatible with the index \u2014 log collection was blocked. ` + connectors.logsCompatibilityError;
|
|
92926
93286
|
logNextSource = "Fix fields.* overrides in your connector config or choose the correct preset (meritt / ecs)";
|
|
@@ -92931,7 +93291,7 @@ function detectMissingEvidence(r, connectors = {}, mode) {
|
|
|
92931
93291
|
logWhy = "No error logs matched in the window \u2014 cannot confirm the actual error signatures.";
|
|
92932
93292
|
logNextSource = "Widen the window (--since) or inspect `horus logs <service>`";
|
|
92933
93293
|
}
|
|
92934
|
-
const logRouteHint = !connectors.elasticsearch && !connectors.sentry ? { nextTool: "connect", args: "elasticsearch", reason: logNextSource } : { nextTool: "logs", args: service, reason: logNextSource };
|
|
93294
|
+
const logRouteHint = !connectors.elasticsearch && !connectors.sentry && !connectors.axiom ? { nextTool: "connect", args: "elasticsearch", reason: logNextSource } : { nextTool: "logs", args: service, reason: logNextSource };
|
|
92935
93295
|
gaps.push({
|
|
92936
93296
|
dimension: "logs",
|
|
92937
93297
|
why: logWhy,
|
|
@@ -92958,8 +93318,10 @@ function detectMissingEvidence(r, connectors = {}, mode) {
|
|
|
92958
93318
|
if (hasQueueTopology && !hasQueueState && !sourceImpact) {
|
|
92959
93319
|
gaps.push({
|
|
92960
93320
|
dimension: "queue runtime state",
|
|
92961
|
-
why: connectors.redis ? "Queue topology is known but live depth + failed/delayed counts were not collected." : "Queue topology is known but there is no Redis
|
|
92962
|
-
|
|
93321
|
+
why: connectors.redis ? "Queue topology is known but live depth + failed/delayed counts were not collected." : "Queue topology is known but there is no Redis connector for live queue depth/failures.",
|
|
93322
|
+
// Stack-agnostic: the tip names Redis (the connector to add) but NOT a specific queue
|
|
93323
|
+
// library — "BullMQ" is Node-only and was leaking onto Python/Redis repos (HOR-428).
|
|
93324
|
+
nextSource: connectors.redis ? "Run `horus queues --live` to see real-time queue depths and failed-job counts" : "Add a `redis` connector to read live queue state",
|
|
92963
93325
|
confidenceImpact: 0.1,
|
|
92964
93326
|
// Redis wired → read live state with `queues --live`; otherwise `connect redis` first.
|
|
92965
93327
|
routeHint: connectors.redis ? {
|
|
@@ -92969,7 +93331,7 @@ function detectMissingEvidence(r, connectors = {}, mode) {
|
|
|
92969
93331
|
} : {
|
|
92970
93332
|
nextTool: "connect",
|
|
92971
93333
|
args: "redis",
|
|
92972
|
-
reason: "Add a `redis` connector to read live
|
|
93334
|
+
reason: "Add a `redis` connector to read live queue state"
|
|
92973
93335
|
}
|
|
92974
93336
|
});
|
|
92975
93337
|
blindSpots.push("Cannot determine if the queue is actually backed up.");
|
|
@@ -93047,10 +93409,11 @@ function buildEntry(source, configured, evidenceCount, failed, detail) {
|
|
|
93047
93409
|
}
|
|
93048
93410
|
function buildRuntimeSourceStatus(evidence2, connectors) {
|
|
93049
93411
|
const logsCount = evidence2.filter((e) => e.source === "logs").length;
|
|
93050
|
-
const logsConfigured = !!(connectors.elasticsearch || connectors.sentry);
|
|
93412
|
+
const logsConfigured = !!(connectors.elasticsearch || connectors.sentry || connectors.axiom);
|
|
93051
93413
|
const esFailed = !!connectors.elasticsearch && !connectors.logsCollected;
|
|
93052
93414
|
const sentryFailed = !!connectors.sentry && !connectors.sentryCollected;
|
|
93053
|
-
const
|
|
93415
|
+
const axiomFailed = !!connectors.axiom && !connectors.axiomCollected;
|
|
93416
|
+
const logsFailed = logsConfigured && logsCount === 0 && (esFailed || sentryFailed || axiomFailed);
|
|
93054
93417
|
const metricsCount = evidence2.filter((e) => e.source === "metrics").length;
|
|
93055
93418
|
const metricsConfigured = !!connectors.grafana;
|
|
93056
93419
|
const metricsFailed = metricsConfigured && !connectors.metricsCollected;
|
|
@@ -94016,6 +94379,17 @@ function classifyLogRelevance(sigKey, sigServices, seedTerms, inputService) {
|
|
|
94016
94379
|
relevanceReason: "no structural link to seed \u2014 ambient runtime noise"
|
|
94017
94380
|
};
|
|
94018
94381
|
}
|
|
94382
|
+
var AXIOM_MESSAGE_FIELDS = ["message", "msg", "body", "event", "log", "_raw"];
|
|
94383
|
+
var AXIOM_LEVEL_FIELDS = ["level", "severity", "lvl", "loglevel", "status"];
|
|
94384
|
+
var AXIOM_SERVICE_FIELDS = ["service", "app", "logger", "source", "namespace"];
|
|
94385
|
+
function pickAxiomField(fields, candidates) {
|
|
94386
|
+
for (const key of candidates) {
|
|
94387
|
+
const v4 = fields[key];
|
|
94388
|
+
if (typeof v4 === "string" && v4 !== "") return v4;
|
|
94389
|
+
if (typeof v4 === "number") return String(v4);
|
|
94390
|
+
}
|
|
94391
|
+
return void 0;
|
|
94392
|
+
}
|
|
94019
94393
|
function extractEventCodes(...texts) {
|
|
94020
94394
|
const RE = /\b(?=[A-Z0-9_]*[0-9_])[A-Z][A-Z0-9_]{3,}\b/g;
|
|
94021
94395
|
const out2 = [];
|
|
@@ -94965,6 +95339,61 @@ async function investigate(input, deps) {
|
|
|
94965
95339
|
sentryCollected = false;
|
|
94966
95340
|
}
|
|
94967
95341
|
}
|
|
95342
|
+
let axiomCollected = false;
|
|
95343
|
+
if (deps.axiom) {
|
|
95344
|
+
try {
|
|
95345
|
+
const from = logWindowFrom(input.logsSince ?? input.since);
|
|
95346
|
+
const to = (/* @__PURE__ */ new Date()).toISOString();
|
|
95347
|
+
const axiomSeedBase = top ? top.filePath.split("/").pop() ?? "" : "";
|
|
95348
|
+
const axiomTerms = top ? [.../* @__PURE__ */ new Set([...tokenize(hint), ...tokenize(top.name), ...tokenize(axiomSeedBase)])] : [...new Set(tokenize(hint))];
|
|
95349
|
+
const records = await deps.axiom.collect({ from, to, hintTerms: axiomTerms });
|
|
95350
|
+
axiomCollected = true;
|
|
95351
|
+
for (const rec of records.slice(0, 15)) {
|
|
95352
|
+
const msg = pickAxiomField(rec.fields, AXIOM_MESSAGE_FIELDS);
|
|
95353
|
+
const level = pickAxiomField(rec.fields, AXIOM_LEVEL_FIELDS);
|
|
95354
|
+
const svc = pickAxiomField(rec.fields, AXIOM_SERVICE_FIELDS) ?? "";
|
|
95355
|
+
const sigKey = [msg ?? "", svc].filter(Boolean).join(" ");
|
|
95356
|
+
const { relevanceClass, relevanceReason } = classifyLogRelevance(
|
|
95357
|
+
sigKey,
|
|
95358
|
+
svc ? [svc] : [],
|
|
95359
|
+
axiomTerms,
|
|
95360
|
+
input.service
|
|
95361
|
+
);
|
|
95362
|
+
const lvl = level ? `[${level}] ` : "";
|
|
95363
|
+
const body2 = msg ?? "(log event)";
|
|
95364
|
+
const tag = relevanceClass === "ambient" ? " [ambient]" : "";
|
|
95365
|
+
const title = `Axiom log: ${lvl}${body2}${tag}`.slice(0, 220);
|
|
95366
|
+
const isError = ["error", "fatal", "critical"].includes((level ?? "").toLowerCase());
|
|
95367
|
+
const ev = mkEv(
|
|
95368
|
+
"log",
|
|
95369
|
+
title,
|
|
95370
|
+
{
|
|
95371
|
+
source: "axiom",
|
|
95372
|
+
signature: msg ?? null,
|
|
95373
|
+
level: level ?? null,
|
|
95374
|
+
service: svc || null,
|
|
95375
|
+
relevanceClass,
|
|
95376
|
+
relevanceReason,
|
|
95377
|
+
fields: rec.fields
|
|
95378
|
+
},
|
|
95379
|
+
{},
|
|
95380
|
+
rec.timestamp,
|
|
95381
|
+
// Direct: weighted up for error-level rows. Ambient: demoted so unrelated
|
|
95382
|
+
// high-volume log lines don't inflate confidence.
|
|
95383
|
+
relevanceClass === "direct" ? isError ? 0.9 : 0.75 : 0.4
|
|
95384
|
+
);
|
|
95385
|
+
logEvIds.push(ev.id);
|
|
95386
|
+
if (relevanceClass === "direct") {
|
|
95387
|
+
directLogEvIds.push(ev.id);
|
|
95388
|
+
directSignatures.push({ key: msg ?? "(log event)", count: 1, message: msg ?? "" });
|
|
95389
|
+
} else {
|
|
95390
|
+
ambientLogEvIds.push(ev.id);
|
|
95391
|
+
}
|
|
95392
|
+
}
|
|
95393
|
+
} catch {
|
|
95394
|
+
axiomCollected = false;
|
|
95395
|
+
}
|
|
95396
|
+
}
|
|
94968
95397
|
let stateAnalysis = null;
|
|
94969
95398
|
const stateEvIds = [];
|
|
94970
95399
|
const stateCollections = /* @__PURE__ */ new Set();
|
|
@@ -95725,6 +96154,10 @@ async function investigate(input, deps) {
|
|
|
95725
96154
|
// its collection ran (distinguishes "no open issues" from "collection failed").
|
|
95726
96155
|
sentry: deps.sentry != null,
|
|
95727
96156
|
sentryCollected,
|
|
96157
|
+
// Axiom is configured iff a provider was built; axiomCollected tracks whether its
|
|
96158
|
+
// collection ran (distinguishes "no matching rows" from "collection failed").
|
|
96159
|
+
axiom: deps.axiom != null,
|
|
96160
|
+
axiomCollected,
|
|
95728
96161
|
metricsCollected,
|
|
95729
96162
|
metricsFailureReason,
|
|
95730
96163
|
logsCollected,
|
|
@@ -95736,6 +96169,8 @@ async function investigate(input, deps) {
|
|
|
95736
96169
|
postgres: deps.postgres != null,
|
|
95737
96170
|
sentry: deps.sentry != null,
|
|
95738
96171
|
sentryCollected,
|
|
96172
|
+
axiom: deps.axiom != null,
|
|
96173
|
+
axiomCollected,
|
|
95739
96174
|
grafana: deps.metrics != null,
|
|
95740
96175
|
queue: deps.queue != null,
|
|
95741
96176
|
metricsCollected,
|
|
@@ -114838,6 +115273,7 @@ async function buildInvestigationContext(renv, opts) {
|
|
|
114838
115273
|
const mongo = mongoForEnv(renv);
|
|
114839
115274
|
const postgres2 = postgresForEnv(renv);
|
|
114840
115275
|
const sentry = sentryForEnv(renv);
|
|
115276
|
+
const axiom = axiomForEnv(renv);
|
|
114841
115277
|
const queue = queueForEnv(renv);
|
|
114842
115278
|
const redisState = redisStateForEnv(renv);
|
|
114843
115279
|
const metrics = metricsForEnv(renv);
|
|
@@ -114854,6 +115290,7 @@ async function buildInvestigationContext(renv, opts) {
|
|
|
114854
115290
|
mongo,
|
|
114855
115291
|
postgres: postgres2,
|
|
114856
115292
|
sentry,
|
|
115293
|
+
axiom,
|
|
114857
115294
|
queue,
|
|
114858
115295
|
redisState,
|
|
114859
115296
|
metrics,
|
|
@@ -114882,6 +115319,7 @@ async function runOneInvestigation(input, ctx, opts = {}) {
|
|
|
114882
115319
|
mongo: ctx.mongo,
|
|
114883
115320
|
postgres: ctx.postgres,
|
|
114884
115321
|
sentry: ctx.sentry,
|
|
115322
|
+
axiom: ctx.axiom,
|
|
114885
115323
|
queue: ctx.queue,
|
|
114886
115324
|
redisState: ctx.redisState,
|
|
114887
115325
|
metrics: ctx.metrics,
|
|
@@ -114892,6 +115330,7 @@ async function runOneInvestigation(input, ctx, opts = {}) {
|
|
|
114892
115330
|
mongodb: !!renv.connectors.mongodb?.url,
|
|
114893
115331
|
postgres: !!renv.connectors.postgres?.url,
|
|
114894
115332
|
sentry: !!renv.connectors.sentry,
|
|
115333
|
+
axiom: !!renv.connectors.axiom,
|
|
114895
115334
|
redis: !!renv.connectors.redis?.url,
|
|
114896
115335
|
queue: !!ctx.queue
|
|
114897
115336
|
}
|
|
@@ -117828,6 +118267,8 @@ function outcomeLabelToJSON(l2) {
|
|
|
117828
118267
|
at: l2.at instanceof Date ? l2.at.toISOString() : l2.at
|
|
117829
118268
|
};
|
|
117830
118269
|
}
|
|
118270
|
+
var DEFAULT_ACCURACY_WINDOW_DAYS = 30;
|
|
118271
|
+
var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
|
|
117831
118272
|
async function runMemoryAccuracy(opts) {
|
|
117832
118273
|
try {
|
|
117833
118274
|
let source;
|
|
@@ -117839,12 +118280,29 @@ async function runMemoryAccuracy(opts) {
|
|
|
117839
118280
|
source = opts.source;
|
|
117840
118281
|
}
|
|
117841
118282
|
let since;
|
|
117842
|
-
|
|
118283
|
+
let window2;
|
|
118284
|
+
if (opts.all) {
|
|
118285
|
+
window2 = "all time";
|
|
118286
|
+
} else if (opts.since !== void 0) {
|
|
118287
|
+
const parsed = new Date(opts.since);
|
|
118288
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
118289
|
+
console.error(
|
|
118290
|
+
import_picocolors31.default.red("--since must be a parseable date (e.g. 2026-06-01 or an ISO timestamp).")
|
|
118291
|
+
);
|
|
118292
|
+
return 1;
|
|
118293
|
+
}
|
|
118294
|
+
since = parsed;
|
|
118295
|
+
window2 = `since ${parsed.toISOString().slice(0, 10)}`;
|
|
118296
|
+
} else if (opts.days !== void 0) {
|
|
117843
118297
|
if (!Number.isFinite(opts.days) || opts.days <= 0) {
|
|
117844
118298
|
console.error(import_picocolors31.default.red("--days must be a positive number."));
|
|
117845
118299
|
return 1;
|
|
117846
118300
|
}
|
|
117847
|
-
since = new Date(Date.now() - opts.days *
|
|
118301
|
+
since = new Date(Date.now() - opts.days * MS_PER_DAY2);
|
|
118302
|
+
window2 = `last ${opts.days} day(s)`;
|
|
118303
|
+
} else {
|
|
118304
|
+
since = new Date(Date.now() - DEFAULT_ACCURACY_WINDOW_DAYS * MS_PER_DAY2);
|
|
118305
|
+
window2 = `last ${DEFAULT_ACCURACY_WINDOW_DAYS} day(s) (default \u2014 pass --all for full history)`;
|
|
117848
118306
|
}
|
|
117849
118307
|
const config2 = await loadConfig(opts.config);
|
|
117850
118308
|
const project = resolveProject(config2, opts.repo);
|
|
@@ -117865,6 +118323,7 @@ async function runMemoryAccuracy(opts) {
|
|
|
117865
118323
|
project,
|
|
117866
118324
|
source: source ?? null,
|
|
117867
118325
|
since: since ? since.toISOString() : null,
|
|
118326
|
+
window: window2,
|
|
117868
118327
|
summary,
|
|
117869
118328
|
labels: labels.map(outcomeLabelToJSON)
|
|
117870
118329
|
},
|
|
@@ -117874,11 +118333,11 @@ async function runMemoryAccuracy(opts) {
|
|
|
117874
118333
|
);
|
|
117875
118334
|
} else {
|
|
117876
118335
|
const pct = (n4) => `${(n4 * 100).toFixed(0)}%`;
|
|
117877
|
-
console.log(import_picocolors31.default.bold(`Investigation accuracy \u2014 ${project}`));
|
|
118336
|
+
console.log(import_picocolors31.default.bold(`Investigation accuracy \u2014 ${project}`) + import_picocolors31.default.dim(` \xB7 window: ${window2}`));
|
|
117878
118337
|
if (summary.evaluated === 0) {
|
|
117879
118338
|
console.log(
|
|
117880
118339
|
import_picocolors31.default.dim(
|
|
117881
|
-
|
|
118340
|
+
`No outcome labels in this window (${window2}). Confirm an investigation (\`horus memory confirm <id>\`) or leave feedback (\`horus feedback <id> --resolved yes|partly|no\`) to add to the eval set` + (opts.all ? "." : ", or pass --all to include older labels.")
|
|
117882
118341
|
)
|
|
117883
118342
|
);
|
|
117884
118343
|
} else {
|
|
@@ -119060,6 +119519,148 @@ async function checkboxSearch(opts) {
|
|
|
119060
119519
|
render();
|
|
119061
119520
|
});
|
|
119062
119521
|
}
|
|
119522
|
+
async function selectSearch(opts) {
|
|
119523
|
+
const { message, choices } = opts;
|
|
119524
|
+
const pageSize = opts.pageSize ?? 10;
|
|
119525
|
+
const input = opts.input ?? process.stdin;
|
|
119526
|
+
const output = opts.output ?? process.stdout;
|
|
119527
|
+
if (!isInteractive(input)) {
|
|
119528
|
+
throw new Error("selectSearch requires an interactive TTY");
|
|
119529
|
+
}
|
|
119530
|
+
if (choices.length === 0) {
|
|
119531
|
+
throw new ExitPromptError("No choices to select from");
|
|
119532
|
+
}
|
|
119533
|
+
return new Promise((resolve12, reject) => {
|
|
119534
|
+
let filter = "";
|
|
119535
|
+
let cursor = 0;
|
|
119536
|
+
let filtered = choices;
|
|
119537
|
+
let visibleOffset = 0;
|
|
119538
|
+
let lastRenderLines = 0;
|
|
119539
|
+
const wasRaw = input.isRaw;
|
|
119540
|
+
function updateFiltered() {
|
|
119541
|
+
const f5 = filter.toLowerCase();
|
|
119542
|
+
filtered = f5 ? choices.filter((c4) => c4.toLowerCase().includes(f5)) : choices;
|
|
119543
|
+
cursor = Math.min(cursor, Math.max(0, filtered.length - 1));
|
|
119544
|
+
updateVisibleOffset();
|
|
119545
|
+
}
|
|
119546
|
+
function updateVisibleOffset() {
|
|
119547
|
+
if (cursor < visibleOffset) {
|
|
119548
|
+
visibleOffset = cursor;
|
|
119549
|
+
} else if (cursor >= visibleOffset + pageSize) {
|
|
119550
|
+
visibleOffset = cursor - pageSize + 1;
|
|
119551
|
+
}
|
|
119552
|
+
const maxOffset = Math.max(0, filtered.length - pageSize);
|
|
119553
|
+
visibleOffset = Math.max(0, Math.min(visibleOffset, maxOffset));
|
|
119554
|
+
}
|
|
119555
|
+
function clear() {
|
|
119556
|
+
output.write("\x1B[?25l");
|
|
119557
|
+
if (lastRenderLines > 0) {
|
|
119558
|
+
output.write(`\x1B[${lastRenderLines}A`);
|
|
119559
|
+
output.write("\x1B[0J");
|
|
119560
|
+
}
|
|
119561
|
+
}
|
|
119562
|
+
function render() {
|
|
119563
|
+
clear();
|
|
119564
|
+
const lines = [];
|
|
119565
|
+
lines.push(
|
|
119566
|
+
`? ${import_picocolors39.default.bold(message)} ${import_picocolors39.default.dim("(\u2191\u2193 navigate \u2022 enter confirm \u2022 type filter)")}`
|
|
119567
|
+
);
|
|
119568
|
+
const visible = filtered.slice(visibleOffset, visibleOffset + pageSize);
|
|
119569
|
+
for (let i3 = 0; i3 < visible.length; i3++) {
|
|
119570
|
+
const idx = visibleOffset + i3;
|
|
119571
|
+
const choice = visible[i3];
|
|
119572
|
+
const isCursor = idx === cursor;
|
|
119573
|
+
const pointer = isCursor ? import_picocolors39.default.cyan("\u276F") : " ";
|
|
119574
|
+
const label = isCursor ? import_picocolors39.default.cyan(choice) : choice;
|
|
119575
|
+
lines.push(` ${pointer} ${label}`);
|
|
119576
|
+
}
|
|
119577
|
+
if (filtered.length === 0) {
|
|
119578
|
+
lines.push(import_picocolors39.default.dim(" No matches"));
|
|
119579
|
+
}
|
|
119580
|
+
const statusParts = [`${filtered.length}/${choices.length} matches`];
|
|
119581
|
+
if (filter) statusParts.push(`filter: ${filter}`);
|
|
119582
|
+
lines.push(import_picocolors39.default.dim(` ${statusParts.join(" \xB7 ")}`));
|
|
119583
|
+
output.write(lines.join("\n"));
|
|
119584
|
+
lastRenderLines = lines.length;
|
|
119585
|
+
}
|
|
119586
|
+
function finish(value) {
|
|
119587
|
+
cleanup();
|
|
119588
|
+
output.write(`
|
|
119589
|
+
? ${import_picocolors39.default.bold(message)} ${value}
|
|
119590
|
+
`);
|
|
119591
|
+
resolve12(value);
|
|
119592
|
+
}
|
|
119593
|
+
function cancel() {
|
|
119594
|
+
cleanup();
|
|
119595
|
+
reject(new ExitPromptError());
|
|
119596
|
+
}
|
|
119597
|
+
function cleanup() {
|
|
119598
|
+
input.setRawMode(wasRaw);
|
|
119599
|
+
input.pause();
|
|
119600
|
+
input.removeListener("data", onData);
|
|
119601
|
+
output.write("\x1B[?25h");
|
|
119602
|
+
}
|
|
119603
|
+
function onData(chunk) {
|
|
119604
|
+
const s5 = chunk.toString();
|
|
119605
|
+
if (s5 === "") {
|
|
119606
|
+
cancel();
|
|
119607
|
+
return;
|
|
119608
|
+
}
|
|
119609
|
+
if (s5 === "\x1B") {
|
|
119610
|
+
if (filter) {
|
|
119611
|
+
filter = "";
|
|
119612
|
+
cursor = 0;
|
|
119613
|
+
updateFiltered();
|
|
119614
|
+
} else {
|
|
119615
|
+
cancel();
|
|
119616
|
+
}
|
|
119617
|
+
render();
|
|
119618
|
+
return;
|
|
119619
|
+
}
|
|
119620
|
+
if (s5 === "\r" || s5 === "\n") {
|
|
119621
|
+
const choice = filtered[cursor];
|
|
119622
|
+
if (choice !== void 0) finish(choice);
|
|
119623
|
+
return;
|
|
119624
|
+
}
|
|
119625
|
+
if (s5 === "\x7F" || s5 === "\b") {
|
|
119626
|
+
if (filter.length > 0) {
|
|
119627
|
+
filter = filter.slice(0, -1);
|
|
119628
|
+
cursor = 0;
|
|
119629
|
+
updateFiltered();
|
|
119630
|
+
}
|
|
119631
|
+
render();
|
|
119632
|
+
return;
|
|
119633
|
+
}
|
|
119634
|
+
if (s5 === "\x1B[A") {
|
|
119635
|
+
if (cursor > 0) {
|
|
119636
|
+
cursor -= 1;
|
|
119637
|
+
updateVisibleOffset();
|
|
119638
|
+
}
|
|
119639
|
+
render();
|
|
119640
|
+
return;
|
|
119641
|
+
}
|
|
119642
|
+
if (s5 === "\x1B[B") {
|
|
119643
|
+
if (cursor < filtered.length - 1) {
|
|
119644
|
+
cursor += 1;
|
|
119645
|
+
updateVisibleOffset();
|
|
119646
|
+
}
|
|
119647
|
+
render();
|
|
119648
|
+
return;
|
|
119649
|
+
}
|
|
119650
|
+
if (s5.length === 1 && s5.charCodeAt(0) >= 32 && s5.charCodeAt(0) <= 126) {
|
|
119651
|
+
filter += s5;
|
|
119652
|
+
cursor = 0;
|
|
119653
|
+
updateFiltered();
|
|
119654
|
+
render();
|
|
119655
|
+
}
|
|
119656
|
+
}
|
|
119657
|
+
input.setRawMode(true);
|
|
119658
|
+
input.resume();
|
|
119659
|
+
input.on("data", onData);
|
|
119660
|
+
updateFiltered();
|
|
119661
|
+
render();
|
|
119662
|
+
});
|
|
119663
|
+
}
|
|
119063
119664
|
|
|
119064
119665
|
// ../../packages/cli/src/commands/connect-ai.ts
|
|
119065
119666
|
init_cjs_shims();
|
|
@@ -119197,7 +119798,7 @@ function parseDbSpec(spec) {
|
|
|
119197
119798
|
}
|
|
119198
119799
|
return { db, roles };
|
|
119199
119800
|
}
|
|
119200
|
-
var SUPPORTED = ["elasticsearch", "mongodb", "postgres", "sentry", "grafana", "redis"];
|
|
119801
|
+
var SUPPORTED = ["elasticsearch", "mongodb", "postgres", "sentry", "axiom", "grafana", "redis"];
|
|
119201
119802
|
async function runConnect(type, opts) {
|
|
119202
119803
|
if (type === "ai") {
|
|
119203
119804
|
return runConnectAi({
|
|
@@ -119234,6 +119835,9 @@ async function runConnect(type, opts) {
|
|
|
119234
119835
|
case "sentry":
|
|
119235
119836
|
patch = buildSentryPatch(filled);
|
|
119236
119837
|
break;
|
|
119838
|
+
case "axiom":
|
|
119839
|
+
patch = buildAxiomPatch(filled);
|
|
119840
|
+
break;
|
|
119237
119841
|
case "grafana":
|
|
119238
119842
|
patch = buildGrafanaPatch(filled);
|
|
119239
119843
|
break;
|
|
@@ -119255,7 +119859,7 @@ ${import_picocolors41.default.red(`\u2717 Could not reach ${connectorType}:`)} $
|
|
|
119255
119859
|
${import_picocolors41.default.green("\u2713")} ${connectorType} reachable ${import_picocolors41.default.dim(`(${probeResult.detail})`)}`
|
|
119256
119860
|
);
|
|
119257
119861
|
}
|
|
119258
|
-
const hasLiteralCredentials = filled.url !== void 0 || filled.password !== void 0 || filled.username !== void 0 || filled.authToken !== void 0;
|
|
119862
|
+
const hasLiteralCredentials = filled.url !== void 0 || filled.password !== void 0 || filled.username !== void 0 || filled.authToken !== void 0 || filled.token !== void 0;
|
|
119259
119863
|
if (hasLiteralCredentials) {
|
|
119260
119864
|
if (isGitTracked(configPath, root)) {
|
|
119261
119865
|
console.error(
|
|
@@ -119332,6 +119936,35 @@ ${import_picocolors41.default.bold(`Connect ${type}`)} ${import_picocolors41.def
|
|
|
119332
119936
|
filled.authToken = filled.authToken ?? (await askPassword("Auth token") || void 0);
|
|
119333
119937
|
filled.url = filled.url ?? (await ask("Base URL (self-hosted)", "https://sentry.io", false) || void 0);
|
|
119334
119938
|
break;
|
|
119939
|
+
case "axiom": {
|
|
119940
|
+
filled.token = filled.token ?? (await askPassword("Axiom API token") || void 0);
|
|
119941
|
+
if (filled.url === void 0) {
|
|
119942
|
+
filled.url = await askAxiomRegion();
|
|
119943
|
+
}
|
|
119944
|
+
if (filled.dataset === void 0) {
|
|
119945
|
+
const baseUrl = filled.url ?? "https://api.axiom.co";
|
|
119946
|
+
let names = [];
|
|
119947
|
+
if (filled.token) {
|
|
119948
|
+
const client = new AxiomClient({ token: filled.token, dataset: "", baseUrl });
|
|
119949
|
+
names = (await client.listDatasets()).map((d3) => d3.name).filter(Boolean);
|
|
119950
|
+
}
|
|
119951
|
+
if (names.length > 0) {
|
|
119952
|
+
const picked = await askDatasetSelection(names);
|
|
119953
|
+
if (picked) filled.dataset = picked;
|
|
119954
|
+
}
|
|
119955
|
+
if (filled.dataset === void 0) {
|
|
119956
|
+
if (names.length === 0) {
|
|
119957
|
+
console.log(
|
|
119958
|
+
import_picocolors41.default.yellow(
|
|
119959
|
+
" Couldn't list datasets (check token / network / region) \u2014 enter the name manually."
|
|
119960
|
+
)
|
|
119961
|
+
);
|
|
119962
|
+
}
|
|
119963
|
+
filled.dataset = await ask("Dataset name", "logs");
|
|
119964
|
+
}
|
|
119965
|
+
}
|
|
119966
|
+
break;
|
|
119967
|
+
}
|
|
119335
119968
|
case "grafana":
|
|
119336
119969
|
filled.url = filled.url ?? await ask("URL", "https://grafana.example.com");
|
|
119337
119970
|
filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
|
|
@@ -119389,6 +120022,8 @@ function missingRequired(type, opts) {
|
|
|
119389
120022
|
return !opts.url;
|
|
119390
120023
|
case "sentry":
|
|
119391
120024
|
return !opts.org || !opts.project || !opts.authToken;
|
|
120025
|
+
case "axiom":
|
|
120026
|
+
return !opts.dataset || !opts.token;
|
|
119392
120027
|
case "grafana":
|
|
119393
120028
|
return !opts.url;
|
|
119394
120029
|
case "redis":
|
|
@@ -119487,6 +120122,13 @@ function buildSentryPatch(opts) {
|
|
|
119487
120122
|
if (opts.url && opts.url.replace(/\/$/, "") !== "https://sentry.io") patch["url"] = opts.url;
|
|
119488
120123
|
return patch;
|
|
119489
120124
|
}
|
|
120125
|
+
function buildAxiomPatch(opts) {
|
|
120126
|
+
if (!opts.dataset) throw new Error("Dataset is required for axiom");
|
|
120127
|
+
const patch = { dataset: opts.dataset };
|
|
120128
|
+
if (opts.token) patch["token"] = opts.token;
|
|
120129
|
+
if (opts.url && opts.url.replace(/\/$/, "") !== "https://api.axiom.co") patch["url"] = opts.url;
|
|
120130
|
+
return patch;
|
|
120131
|
+
}
|
|
119490
120132
|
function buildGrafanaPatch(opts) {
|
|
119491
120133
|
const patch = {};
|
|
119492
120134
|
if (opts.url) patch["url"] = opts.url;
|
|
@@ -119622,6 +120264,17 @@ async function probe(type, opts) {
|
|
|
119622
120264
|
});
|
|
119623
120265
|
return client.health();
|
|
119624
120266
|
}
|
|
120267
|
+
case "axiom": {
|
|
120268
|
+
if (!opts.token || !opts.dataset) {
|
|
120269
|
+
return { ok: true, detail: "skipped (missing dataset/token)" };
|
|
120270
|
+
}
|
|
120271
|
+
const client = new AxiomClient({
|
|
120272
|
+
token: opts.token,
|
|
120273
|
+
dataset: opts.dataset,
|
|
120274
|
+
...opts.url ? { baseUrl: opts.url } : {}
|
|
120275
|
+
});
|
|
120276
|
+
return client.health();
|
|
120277
|
+
}
|
|
119625
120278
|
case "grafana": {
|
|
119626
120279
|
if (!opts.url) return { ok: true, detail: "skipped (no URL)" };
|
|
119627
120280
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -119673,8 +120326,11 @@ function printSummary(type, opts) {
|
|
|
119673
120326
|
if (opts.collections) lines.push(` collections: ${opts.collections}`);
|
|
119674
120327
|
if (opts.org) lines.push(` org: ${opts.org}`);
|
|
119675
120328
|
if (opts.project) lines.push(` project: ${opts.project}`);
|
|
120329
|
+
if (opts.dataset) lines.push(` dataset: ${opts.dataset}`);
|
|
119676
120330
|
if (opts.authToken)
|
|
119677
120331
|
lines.push(` auth-token: ${"\u2022".repeat(Math.min(opts.authToken.length, 8))}`);
|
|
120332
|
+
if (opts.token)
|
|
120333
|
+
lines.push(` token: ${"\u2022".repeat(Math.min(opts.token.length, 8))}`);
|
|
119678
120334
|
if (opts.dashboardUids && opts.dashboardUids.length > 0) {
|
|
119679
120335
|
lines.push(` dashboards: ${opts.dashboardUids.join(", ")}`);
|
|
119680
120336
|
} else if (opts.dashboard) {
|
|
@@ -119785,6 +120441,63 @@ async function askDashboardSelection(dashboards) {
|
|
|
119785
120441
|
}
|
|
119786
120442
|
return [];
|
|
119787
120443
|
}
|
|
120444
|
+
async function askAxiomRegion() {
|
|
120445
|
+
const US = "US \u2014 api.axiom.co";
|
|
120446
|
+
const EU = "EU \u2014 api.eu.axiom.co";
|
|
120447
|
+
const CUSTOM = "Custom URL\u2026";
|
|
120448
|
+
const US_URL = "https://api.axiom.co";
|
|
120449
|
+
const EU_URL = "https://api.eu.axiom.co";
|
|
120450
|
+
let choice = US;
|
|
120451
|
+
if (isInteractive()) {
|
|
120452
|
+
try {
|
|
120453
|
+
choice = await selectSearch({
|
|
120454
|
+
message: "Axiom server / region",
|
|
120455
|
+
choices: [US, EU, CUSTOM],
|
|
120456
|
+
pageSize: 3
|
|
120457
|
+
});
|
|
120458
|
+
} catch (err2) {
|
|
120459
|
+
if (err2 instanceof ExitPromptError) throw err2;
|
|
120460
|
+
return US_URL;
|
|
120461
|
+
}
|
|
120462
|
+
} else {
|
|
120463
|
+
return US_URL;
|
|
120464
|
+
}
|
|
120465
|
+
if (choice === EU) return EU_URL;
|
|
120466
|
+
if (choice === CUSTOM) {
|
|
120467
|
+
return await ask("Custom Axiom base URL", US_URL) || US_URL;
|
|
120468
|
+
}
|
|
120469
|
+
return US_URL;
|
|
120470
|
+
}
|
|
120471
|
+
async function askDatasetSelection(names) {
|
|
120472
|
+
if (names.length === 0) return void 0;
|
|
120473
|
+
if (isInteractive()) {
|
|
120474
|
+
try {
|
|
120475
|
+
return await selectSearch({
|
|
120476
|
+
message: "Select an Axiom dataset",
|
|
120477
|
+
choices: names,
|
|
120478
|
+
pageSize: 12
|
|
120479
|
+
});
|
|
120480
|
+
} catch (err2) {
|
|
120481
|
+
if (err2 instanceof ExitPromptError) throw err2;
|
|
120482
|
+
}
|
|
120483
|
+
}
|
|
120484
|
+
const MAX_DISPLAY = 25;
|
|
120485
|
+
const shown = names.slice(0, MAX_DISPLAY);
|
|
120486
|
+
console.log("\n Available Axiom datasets:");
|
|
120487
|
+
shown.forEach((name2, i3) => {
|
|
120488
|
+
console.log(` ${import_picocolors41.default.dim(`[${i3 + 1}]`)} ${name2}`);
|
|
120489
|
+
});
|
|
120490
|
+
if (names.length > MAX_DISPLAY) {
|
|
120491
|
+
console.log(import_picocolors41.default.dim(` \u2026 and ${names.length - MAX_DISPLAY} more`));
|
|
120492
|
+
}
|
|
120493
|
+
const input = (await ask(" Select a dataset (e.g. 1 or Enter to type a name manually)", "", false)).trim();
|
|
120494
|
+
if (!input) return void 0;
|
|
120495
|
+
if (/^\d+$/.test(input)) {
|
|
120496
|
+
const idx = parseInt(input, 10) - 1;
|
|
120497
|
+
if (idx >= 0 && idx < shown.length) return shown[idx];
|
|
120498
|
+
}
|
|
120499
|
+
return input;
|
|
120500
|
+
}
|
|
119788
120501
|
function redisUrlHasPassword(raw) {
|
|
119789
120502
|
if (!raw) return false;
|
|
119790
120503
|
try {
|
|
@@ -120879,6 +121592,7 @@ async function runReadiness(opts) {
|
|
|
120879
121592
|
let anyMongo = false;
|
|
120880
121593
|
let anyPostgres = false;
|
|
120881
121594
|
let anySentry = false;
|
|
121595
|
+
let anyAxiom = false;
|
|
120882
121596
|
let anyRedis = false;
|
|
120883
121597
|
for (const project of globalConfig2.projects) {
|
|
120884
121598
|
for (const repo of project.repositories) {
|
|
@@ -120891,6 +121605,7 @@ async function runReadiness(opts) {
|
|
|
120891
121605
|
if (c4.mongodb) anyMongo = true;
|
|
120892
121606
|
if (c4.postgres) anyPostgres = true;
|
|
120893
121607
|
if (c4.sentry) anySentry = true;
|
|
121608
|
+
if (c4.axiom) anyAxiom = true;
|
|
120894
121609
|
if (c4.redis) anyRedis = true;
|
|
120895
121610
|
}
|
|
120896
121611
|
}
|
|
@@ -120948,6 +121663,15 @@ async function runReadiness(opts) {
|
|
|
120948
121663
|
next: "run `horus connect sentry`"
|
|
120949
121664
|
}
|
|
120950
121665
|
);
|
|
121666
|
+
checks.push(
|
|
121667
|
+
anyAxiom ? { label: "Axiom", status: "pass", blocking: false, detail: "configured" } : {
|
|
121668
|
+
label: "Axiom",
|
|
121669
|
+
status: "warn",
|
|
121670
|
+
blocking: false,
|
|
121671
|
+
detail: "not configured \u2014 no runtime log evidence",
|
|
121672
|
+
next: "run `horus connect axiom`"
|
|
121673
|
+
}
|
|
121674
|
+
);
|
|
120951
121675
|
checks.push(
|
|
120952
121676
|
anyRedis ? { label: "Redis", status: "pass", blocking: false, detail: "configured" } : {
|
|
120953
121677
|
label: "Redis",
|
|
@@ -122675,8 +123399,8 @@ Examples:
|
|
|
122675
123399
|
process.exitCode = await runReadiness({ config: opts.config, ai: opts.ai, aiModel: opts.aiModel });
|
|
122676
123400
|
});
|
|
122677
123401
|
program2.command("connect <type>").description(
|
|
122678
|
-
"Add or update a connector (elasticsearch / mongodb / postgres / sentry / grafana / redis / ai) in .horus/config.json"
|
|
122679
|
-
).option("--env <name>", "target environment (default: first environment in config)").option("--provider <name>", "AI provider for `connect ai` (anthropic / claude / codex / gemini)").option("--api-key <key>", "Anthropic API key for `connect ai`").option("--model <id>", "default model for `connect ai`").option("--url <url>", "connector URL or connection string (Redis with auth: redis://:password@host:6379)").option("--username <user>", "username (elasticsearch / grafana)").option("--password <pass>", "password (elasticsearch / grafana; for Redis embed in --url)").option("--index-pattern <pattern>", "Elasticsearch index pattern (required for elasticsearch)").option("--service <name>", "service name scope for log queries").option("--database <name>", "database name (required for mongodb)").option("--collections <list>", "comma-separated collection allowlist (mongodb)").option("--schema <name>", "schema to introspect (postgres; default public)").option("--tables <list>", "comma-separated table allowlist (postgres)").option("--auth-token <token>", "Sentry API auth token (required for sentry)").option("--org <slug>", "Sentry org slug (required for sentry)").option("--project <slug>", "Sentry project slug (required for sentry)").option("--dashboard <uid>", "default dashboard uid (grafana)").option(
|
|
123402
|
+
"Add or update a connector (elasticsearch / mongodb / postgres / sentry / axiom / grafana / redis / ai) in .horus/config.json"
|
|
123403
|
+
).option("--env <name>", "target environment (default: first environment in config)").option("--provider <name>", "AI provider for `connect ai` (anthropic / claude / codex / gemini)").option("--api-key <key>", "Anthropic API key for `connect ai`").option("--model <id>", "default model for `connect ai`").option("--url <url>", "connector URL or connection string (Redis with auth: redis://:password@host:6379)").option("--username <user>", "username (elasticsearch / grafana)").option("--password <pass>", "password (elasticsearch / grafana; for Redis embed in --url)").option("--index-pattern <pattern>", "Elasticsearch index pattern (required for elasticsearch)").option("--service <name>", "service name scope for log queries").option("--database <name>", "database name (required for mongodb)").option("--collections <list>", "comma-separated collection allowlist (mongodb)").option("--schema <name>", "schema to introspect (postgres; default public)").option("--tables <list>", "comma-separated table allowlist (postgres)").option("--auth-token <token>", "Sentry API auth token (required for sentry)").option("--org <slug>", "Sentry org slug (required for sentry)").option("--project <slug>", "Sentry project slug (required for sentry)").option("--token <token>", "Axiom API token (required for axiom)").option("--dataset <name>", "Axiom dataset to query (required for axiom)").option("--dashboard <uid>", "default dashboard uid (grafana)").option(
|
|
122680
123404
|
"--db <spec>",
|
|
122681
123405
|
"redis logical DB as db:role1,role2 (e.g. 0:cache,state or 1:bullmq,queues); repeatable",
|
|
122682
123406
|
(val, acc) => {
|
|
@@ -122700,6 +123424,8 @@ Examples:
|
|
|
122700
123424
|
authToken: opts.authToken,
|
|
122701
123425
|
org: opts.org,
|
|
122702
123426
|
project: opts.project,
|
|
123427
|
+
token: opts.token,
|
|
123428
|
+
dataset: opts.dataset,
|
|
122703
123429
|
dashboard: opts.dashboard,
|
|
122704
123430
|
db: opts.db,
|
|
122705
123431
|
bullmqPrefix: opts.bullmqPrefix,
|
|
@@ -122814,7 +123540,7 @@ Examples:
|
|
|
122814
123540
|
process.exitCode = await runMemoryDetect(opts);
|
|
122815
123541
|
}
|
|
122816
123542
|
);
|
|
122817
|
-
memory.command("accuracy").description("Report Horus's measured hit-rate from the converged outcome-label eval set").option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "project/repository to scope to (default: inferred from cwd)").option("--source <source>", "filter to one signal source: feedback | confirm").option("--days <n>", "only count labels from the last N days", (v4) => Number(v4)).option("--limit <n>", "max labels to scan", (v4) => Number(v4)).option("--json", "output JSON").action(
|
|
123543
|
+
memory.command("accuracy").description("Report Horus's measured hit-rate from the converged outcome-label eval set").option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "project/repository to scope to (default: inferred from cwd)").option("--source <source>", "filter to one signal source: feedback | confirm").option("--since <date>", "only count labels on/after this date (e.g. 2026-06-01)").option("--days <n>", "only count labels from the last N days", (v4) => Number(v4)).option("--all", "count the full retained history (disable the default recent window)").option("--limit <n>", "max labels to scan", (v4) => Number(v4)).option("--json", "output JSON").action(
|
|
122818
123544
|
async (opts) => {
|
|
122819
123545
|
process.exitCode = await runMemoryAccuracy(opts);
|
|
122820
123546
|
}
|