@merittdev/horus 0.1.9 → 0.1.11
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 +611 -51
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -49547,7 +49547,7 @@ var require_Redis = __commonJS({
|
|
|
49547
49547
|
var lodash_1 = require_lodash();
|
|
49548
49548
|
var Deque = require_denque();
|
|
49549
49549
|
var debug = (0, utils_1.Debug)("redis");
|
|
49550
|
-
var
|
|
49550
|
+
var Redis3 = class _Redis extends Commander_1.default {
|
|
49551
49551
|
constructor(arg1, arg2, arg3) {
|
|
49552
49552
|
super();
|
|
49553
49553
|
this.status = "wait";
|
|
@@ -50212,12 +50212,12 @@ var require_Redis = __commonJS({
|
|
|
50212
50212
|
}).catch(lodash_1.noop);
|
|
50213
50213
|
}
|
|
50214
50214
|
};
|
|
50215
|
-
|
|
50216
|
-
|
|
50217
|
-
|
|
50218
|
-
(0, applyMixin_1.default)(
|
|
50219
|
-
(0, transaction_1.addTransactionSupport)(
|
|
50220
|
-
exports2.default =
|
|
50215
|
+
Redis3.Cluster = cluster_1.default;
|
|
50216
|
+
Redis3.Command = Command_1.default;
|
|
50217
|
+
Redis3.defaultOptions = RedisOptions_1.DEFAULT_REDIS_OPTIONS;
|
|
50218
|
+
(0, applyMixin_1.default)(Redis3, events_1.EventEmitter);
|
|
50219
|
+
(0, transaction_1.addTransactionSupport)(Redis3.prototype);
|
|
50220
|
+
exports2.default = Redis3;
|
|
50221
50221
|
}
|
|
50222
50222
|
});
|
|
50223
50223
|
|
|
@@ -50314,7 +50314,7 @@ init_cjs_shims();
|
|
|
50314
50314
|
|
|
50315
50315
|
// ../../packages/core/src/version.ts
|
|
50316
50316
|
init_cjs_shims();
|
|
50317
|
-
var HORUS_VERSION = true ? "0.1.
|
|
50317
|
+
var HORUS_VERSION = true ? "0.1.11" : "dev";
|
|
50318
50318
|
var PINNED_AXON_VERSION = "1.0.7";
|
|
50319
50319
|
var PINNED_SOURCE_VERSION = PINNED_AXON_VERSION;
|
|
50320
50320
|
|
|
@@ -54541,6 +54541,37 @@ var repositorySchema = external_exports.object({
|
|
|
54541
54541
|
*/
|
|
54542
54542
|
axon: external_exports.object({ hostUrl: external_exports.string().url() }).optional()
|
|
54543
54543
|
});
|
|
54544
|
+
var REDIS_ROLES = [
|
|
54545
|
+
"cache",
|
|
54546
|
+
"state",
|
|
54547
|
+
"locks",
|
|
54548
|
+
"rate-limit",
|
|
54549
|
+
"session",
|
|
54550
|
+
"dedupe",
|
|
54551
|
+
"bullmq",
|
|
54552
|
+
"queues"
|
|
54553
|
+
];
|
|
54554
|
+
var redisRoleSchema = external_exports.enum(REDIS_ROLES);
|
|
54555
|
+
var REDIS_QUEUE_ROLES = ["bullmq", "queues"];
|
|
54556
|
+
function redisDbFromUrl(url) {
|
|
54557
|
+
if (!url) return 0;
|
|
54558
|
+
try {
|
|
54559
|
+
const seg = new URL(url).pathname.replace(/^\//, "");
|
|
54560
|
+
const n = Number.parseInt(seg, 10);
|
|
54561
|
+
return Number.isInteger(n) && n >= 0 ? n : 0;
|
|
54562
|
+
} catch {
|
|
54563
|
+
return 0;
|
|
54564
|
+
}
|
|
54565
|
+
}
|
|
54566
|
+
function redisUrlForDb(url, db) {
|
|
54567
|
+
try {
|
|
54568
|
+
const u = new URL(url);
|
|
54569
|
+
u.pathname = `/${db}`;
|
|
54570
|
+
return u.toString();
|
|
54571
|
+
} catch {
|
|
54572
|
+
return url;
|
|
54573
|
+
}
|
|
54574
|
+
}
|
|
54544
54575
|
var connectorsSchema = external_exports.object({
|
|
54545
54576
|
elasticsearch: external_exports.object({
|
|
54546
54577
|
indexPattern: external_exports.string().optional(),
|
|
@@ -54619,7 +54650,33 @@ var connectorsSchema = external_exports.object({
|
|
|
54619
54650
|
/** Direct URL value (takes priority over urlEnv). */
|
|
54620
54651
|
url: external_exports.string().optional(),
|
|
54621
54652
|
/** Name of the env var holding the Redis URL. Defaults to "REDIS_URL". */
|
|
54622
|
-
urlEnv: external_exports.string().optional()
|
|
54653
|
+
urlEnv: external_exports.string().optional(),
|
|
54654
|
+
/**
|
|
54655
|
+
* Logical databases on the same Redis server (HOR-201). Redis is a general
|
|
54656
|
+
* runtime-evidence connector — a single server commonly holds BullMQ queues in
|
|
54657
|
+
* one DB and cache/locks/rate-limit/session keys in others. Declaring them here
|
|
54658
|
+
* (with roles) lets `queues --live` target the queue DB and lets investigation
|
|
54659
|
+
* collectors target cache/state/locks DBs. When omitted, the DB index in `url`
|
|
54660
|
+
* (default 0) is treated as a single configured database (backward compatible).
|
|
54661
|
+
*/
|
|
54662
|
+
databases: external_exports.array(
|
|
54663
|
+
external_exports.object({
|
|
54664
|
+
/** Logical DB index on the server (0–15 by default Redis config). */
|
|
54665
|
+
db: external_exports.number().int().min(0).max(15),
|
|
54666
|
+
/** Friendly name for display (e.g. "cache", "queues"). */
|
|
54667
|
+
name: external_exports.string().optional(),
|
|
54668
|
+
/** What this DB holds — drives which collectors/commands use it. */
|
|
54669
|
+
roles: external_exports.array(redisRoleSchema).default([]),
|
|
54670
|
+
/** BullMQ settings when this DB has the bullmq/queues role. */
|
|
54671
|
+
bullmq: external_exports.object({ prefix: external_exports.string().default("bull") }).optional(),
|
|
54672
|
+
/** Sampled-scan settings for cache/state/locks DBs. */
|
|
54673
|
+
scan: external_exports.object({
|
|
54674
|
+
enabled: external_exports.boolean().default(true),
|
|
54675
|
+
sampleLimit: external_exports.number().int().positive().default(500),
|
|
54676
|
+
patterns: external_exports.array(external_exports.string()).default([])
|
|
54677
|
+
}).optional()
|
|
54678
|
+
})
|
|
54679
|
+
).optional()
|
|
54623
54680
|
}).optional()
|
|
54624
54681
|
}).default({});
|
|
54625
54682
|
var environmentSchema = external_exports.object({
|
|
@@ -54762,8 +54819,28 @@ function resolveEnvironment(config, opts) {
|
|
|
54762
54819
|
}
|
|
54763
54820
|
if (c.redis !== void 0) {
|
|
54764
54821
|
const r = c.redis;
|
|
54822
|
+
const url = r.url ?? process.env[r.urlEnv ?? "REDIS_URL"];
|
|
54823
|
+
let databases;
|
|
54824
|
+
if (r.databases !== void 0 && r.databases.length > 0) {
|
|
54825
|
+
databases = r.databases.map((d) => ({
|
|
54826
|
+
db: d.db,
|
|
54827
|
+
...d.name !== void 0 ? { name: d.name } : {},
|
|
54828
|
+
roles: d.roles,
|
|
54829
|
+
bullmqPrefix: d.bullmq?.prefix ?? "bull",
|
|
54830
|
+
...d.scan !== void 0 ? {
|
|
54831
|
+
scan: {
|
|
54832
|
+
enabled: d.scan.enabled,
|
|
54833
|
+
sampleLimit: d.scan.sampleLimit,
|
|
54834
|
+
patterns: d.scan.patterns
|
|
54835
|
+
}
|
|
54836
|
+
} : {}
|
|
54837
|
+
}));
|
|
54838
|
+
} else {
|
|
54839
|
+
databases = [{ db: redisDbFromUrl(url), roles: [], bullmqPrefix: "bull" }];
|
|
54840
|
+
}
|
|
54765
54841
|
resolved.redis = {
|
|
54766
|
-
url
|
|
54842
|
+
...url !== void 0 ? { url } : {},
|
|
54843
|
+
databases
|
|
54767
54844
|
};
|
|
54768
54845
|
}
|
|
54769
54846
|
return {
|
|
@@ -57084,12 +57161,16 @@ var BullMQRedisClient = class {
|
|
|
57084
57161
|
);
|
|
57085
57162
|
}
|
|
57086
57163
|
/**
|
|
57087
|
-
* Discover queue names by scanning for BullMQ
|
|
57164
|
+
* Discover queue names by scanning for BullMQ `:meta` keys.
|
|
57165
|
+
*
|
|
57166
|
+
* `:meta` is written when a Queue is instantiated and persists regardless of job
|
|
57167
|
+
* state, so it finds idle queues too. Scanning `:wait` (the previous approach) only
|
|
57168
|
+
* surfaced queues with pending jobs, so idle-but-real queues were invisible.
|
|
57088
57169
|
* Returns at most `limit` queue names.
|
|
57089
57170
|
*/
|
|
57090
57171
|
async discoverQueues(limit = 50) {
|
|
57091
|
-
const pattern = `${this.prefix}:*:
|
|
57092
|
-
const suffixLen = ":
|
|
57172
|
+
const pattern = `${this.prefix}:*:meta`;
|
|
57173
|
+
const suffixLen = ":meta".length;
|
|
57093
57174
|
const prefixLen = this.prefix.length + 1;
|
|
57094
57175
|
const names = [];
|
|
57095
57176
|
let cursor = "0";
|
|
@@ -57272,6 +57353,10 @@ var BullMQRuntimeProvider = class {
|
|
|
57272
57353
|
const queues = await Promise.all(names.map((name) => this.inspectQueue(name)));
|
|
57273
57354
|
return { prefix: this.client.prefix, collectedAt, queues };
|
|
57274
57355
|
}
|
|
57356
|
+
/** Discover queue names present in Redis under the configured prefix. */
|
|
57357
|
+
discoverQueues() {
|
|
57358
|
+
return this.client.discoverQueues();
|
|
57359
|
+
}
|
|
57275
57360
|
async inspectQueue(name) {
|
|
57276
57361
|
const [waiting, active, failed, delayed, completed, paused] = await Promise.all([
|
|
57277
57362
|
this.client.listLen(this.client.queueKey(name, "wait")),
|
|
@@ -57331,6 +57416,174 @@ var BullMQRuntimeProvider = class {
|
|
|
57331
57416
|
}
|
|
57332
57417
|
};
|
|
57333
57418
|
|
|
57419
|
+
// ../../packages/connectors/src/redis/state-provider.ts
|
|
57420
|
+
init_cjs_shims();
|
|
57421
|
+
|
|
57422
|
+
// ../../packages/connectors/src/redis/scan-client.ts
|
|
57423
|
+
init_cjs_shims();
|
|
57424
|
+
var import_ioredis2 = __toESM(require_built3(), 1);
|
|
57425
|
+
var SAMPLE_DEFAULT = 500;
|
|
57426
|
+
var SCAN_BATCH = 200;
|
|
57427
|
+
var RedisScanClient = class {
|
|
57428
|
+
redis;
|
|
57429
|
+
db;
|
|
57430
|
+
constructor(opts) {
|
|
57431
|
+
this.db = opts.db ?? 0;
|
|
57432
|
+
const redisOpts = {
|
|
57433
|
+
db: this.db,
|
|
57434
|
+
lazyConnect: true,
|
|
57435
|
+
enableReadyCheck: false,
|
|
57436
|
+
maxRetriesPerRequest: 1,
|
|
57437
|
+
connectTimeout: 5e3
|
|
57438
|
+
};
|
|
57439
|
+
this.redis = new import_ioredis2.Redis(opts.url, redisOpts);
|
|
57440
|
+
this.redis.on("error", () => {
|
|
57441
|
+
});
|
|
57442
|
+
}
|
|
57443
|
+
/** Ping the server (also exercises AUTH). */
|
|
57444
|
+
async health() {
|
|
57445
|
+
try {
|
|
57446
|
+
await this.redis.ping();
|
|
57447
|
+
return { ok: true, detail: `Redis reachable (db ${this.db})` };
|
|
57448
|
+
} catch (err) {
|
|
57449
|
+
return { ok: false, detail: err.message };
|
|
57450
|
+
}
|
|
57451
|
+
}
|
|
57452
|
+
/** Total number of keys in this DB (DBSIZE). */
|
|
57453
|
+
async dbSize() {
|
|
57454
|
+
return this.redis.dbsize();
|
|
57455
|
+
}
|
|
57456
|
+
/**
|
|
57457
|
+
* Sample up to `limit` keys and group them by their leading segment (before the
|
|
57458
|
+
* first ':'), returning the most common prefixes. Used to characterise a cache/
|
|
57459
|
+
* state DB without reading values. Optional `patterns` restrict the scan.
|
|
57460
|
+
*/
|
|
57461
|
+
async samplePrefixes(limit = SAMPLE_DEFAULT, patterns) {
|
|
57462
|
+
const counts = /* @__PURE__ */ new Map();
|
|
57463
|
+
let sampled = 0;
|
|
57464
|
+
const scanPatterns = patterns && patterns.length > 0 ? patterns : ["*"];
|
|
57465
|
+
for (const pattern of scanPatterns) {
|
|
57466
|
+
let cursor = "0";
|
|
57467
|
+
do {
|
|
57468
|
+
const [next, batch] = await this.redis.scan(
|
|
57469
|
+
cursor,
|
|
57470
|
+
"MATCH",
|
|
57471
|
+
pattern,
|
|
57472
|
+
"COUNT",
|
|
57473
|
+
SCAN_BATCH
|
|
57474
|
+
);
|
|
57475
|
+
cursor = next;
|
|
57476
|
+
for (const key of batch) {
|
|
57477
|
+
const prefix = key.includes(":") ? key.slice(0, key.indexOf(":")) : key;
|
|
57478
|
+
counts.set(prefix, (counts.get(prefix) ?? 0) + 1);
|
|
57479
|
+
if (++sampled >= limit) break;
|
|
57480
|
+
}
|
|
57481
|
+
} while (cursor !== "0" && sampled < limit);
|
|
57482
|
+
if (sampled >= limit) break;
|
|
57483
|
+
}
|
|
57484
|
+
return [...counts.entries()].map(([prefix, count]) => ({ prefix, count })).sort((a, b2) => b2.count - a.count);
|
|
57485
|
+
}
|
|
57486
|
+
/**
|
|
57487
|
+
* Detect BullMQ queue names in this DB by scanning `{prefix}:*:meta`. `:meta` exists
|
|
57488
|
+
* for every instantiated queue (even idle ones), so this finds all queues, not just
|
|
57489
|
+
* those with pending jobs. Returns at most `limit` names.
|
|
57490
|
+
*/
|
|
57491
|
+
async detectBullmqQueues(prefix = "bull", limit = 200) {
|
|
57492
|
+
const pattern = `${prefix}:*:meta`;
|
|
57493
|
+
const suffixLen = ":meta".length;
|
|
57494
|
+
const prefixLen = prefix.length + 1;
|
|
57495
|
+
const names = [];
|
|
57496
|
+
let cursor = "0";
|
|
57497
|
+
do {
|
|
57498
|
+
const [next, batch] = await this.redis.scan(cursor, "MATCH", pattern, "COUNT", SCAN_BATCH);
|
|
57499
|
+
cursor = next;
|
|
57500
|
+
for (const key of batch) {
|
|
57501
|
+
names.push(key.slice(prefixLen, key.length - suffixLen));
|
|
57502
|
+
if (names.length >= limit) return names;
|
|
57503
|
+
}
|
|
57504
|
+
} while (cursor !== "0" && names.length < limit);
|
|
57505
|
+
return names;
|
|
57506
|
+
}
|
|
57507
|
+
async close() {
|
|
57508
|
+
this.redis.disconnect();
|
|
57509
|
+
}
|
|
57510
|
+
};
|
|
57511
|
+
|
|
57512
|
+
// ../../packages/connectors/src/redis/state-provider.ts
|
|
57513
|
+
var LOCK_RE = /lock/i;
|
|
57514
|
+
var RATELIMIT_RE = /rate.?limit|throttle/i;
|
|
57515
|
+
var RedisStateRuntimeProvider = class {
|
|
57516
|
+
constructor(dbs, mkClient = (d) => new RedisScanClient({ url: d.url, db: d.db })) {
|
|
57517
|
+
this.dbs = dbs;
|
|
57518
|
+
this.mkClient = mkClient;
|
|
57519
|
+
}
|
|
57520
|
+
dbs;
|
|
57521
|
+
mkClient;
|
|
57522
|
+
async analyzeRedisState() {
|
|
57523
|
+
const collectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
57524
|
+
const databases = [];
|
|
57525
|
+
const signals = [];
|
|
57526
|
+
for (const d of this.dbs) {
|
|
57527
|
+
const client = this.mkClient(d);
|
|
57528
|
+
try {
|
|
57529
|
+
const health = await client.health();
|
|
57530
|
+
if (!health.ok) continue;
|
|
57531
|
+
const keyCount = await client.dbSize();
|
|
57532
|
+
const prefixes = await client.samplePrefixes(d.scan?.sampleLimit ?? 500, d.scan?.patterns);
|
|
57533
|
+
databases.push({
|
|
57534
|
+
db: d.db,
|
|
57535
|
+
...d.name !== void 0 ? { name: d.name } : {},
|
|
57536
|
+
roles: d.roles,
|
|
57537
|
+
keyCount,
|
|
57538
|
+
prefixes
|
|
57539
|
+
});
|
|
57540
|
+
if (keyCount === 0) continue;
|
|
57541
|
+
const roleLabel = d.roles.length > 0 ? d.roles.join("/") : "state";
|
|
57542
|
+
const topPrefixes = prefixes.slice(0, 10);
|
|
57543
|
+
const topText = topPrefixes.map((p) => p.prefix).join(" ");
|
|
57544
|
+
signals.push({
|
|
57545
|
+
db: d.db,
|
|
57546
|
+
title: `Redis DB ${d.db} (${roleLabel}): ${keyCount} key(s), top prefixes ${topPrefixes.slice(0, 4).map((p) => `${p.prefix}:*`).join(", ")}`,
|
|
57547
|
+
payload: { db: d.db, roles: d.roles, keyCount, topPrefixes },
|
|
57548
|
+
relevance: 0.4,
|
|
57549
|
+
matchText: `${roleLabel} ${topText}`.toLowerCase()
|
|
57550
|
+
});
|
|
57551
|
+
const lockPrefixes = prefixes.filter((p) => LOCK_RE.test(p.prefix));
|
|
57552
|
+
if (d.roles.includes("locks") || lockPrefixes.length > 0) {
|
|
57553
|
+
const lockKeys = lockPrefixes.reduce((n, p) => n + p.count, 0);
|
|
57554
|
+
if (lockKeys > 0) {
|
|
57555
|
+
signals.push({
|
|
57556
|
+
db: d.db,
|
|
57557
|
+
title: `Redis DB ${d.db}: ${lockKeys} lock key(s) present (${lockPrefixes.map((p) => `${p.prefix}:*`).join(", ")})`,
|
|
57558
|
+
payload: { db: d.db, lockKeys, lockPrefixes },
|
|
57559
|
+
relevance: 0.6,
|
|
57560
|
+
matchText: `lock locks ${lockPrefixes.map((p) => p.prefix).join(" ")}`.toLowerCase()
|
|
57561
|
+
});
|
|
57562
|
+
}
|
|
57563
|
+
}
|
|
57564
|
+
const rlPrefixes = prefixes.filter((p) => RATELIMIT_RE.test(p.prefix));
|
|
57565
|
+
if (d.roles.includes("rate-limit") || rlPrefixes.length > 0) {
|
|
57566
|
+
const rlKeys = rlPrefixes.reduce((n, p) => n + p.count, 0);
|
|
57567
|
+
if (rlKeys > 0) {
|
|
57568
|
+
signals.push({
|
|
57569
|
+
db: d.db,
|
|
57570
|
+
title: `Redis DB ${d.db}: ${rlKeys} rate-limit key(s) present`,
|
|
57571
|
+
payload: { db: d.db, rateLimitKeys: rlKeys, prefixes: rlPrefixes },
|
|
57572
|
+
relevance: 0.6,
|
|
57573
|
+
matchText: `rate limit throttle ${rlPrefixes.map((p) => p.prefix).join(" ")}`.toLowerCase()
|
|
57574
|
+
});
|
|
57575
|
+
}
|
|
57576
|
+
}
|
|
57577
|
+
} finally {
|
|
57578
|
+
await client.close();
|
|
57579
|
+
}
|
|
57580
|
+
}
|
|
57581
|
+
return { collectedAt, databases, signals };
|
|
57582
|
+
}
|
|
57583
|
+
async close() {
|
|
57584
|
+
}
|
|
57585
|
+
};
|
|
57586
|
+
|
|
57334
57587
|
// ../../packages/connectors/src/factory.ts
|
|
57335
57588
|
function codeForEnv(renv) {
|
|
57336
57589
|
const repo = renv.repositories[0];
|
|
@@ -57372,10 +57625,43 @@ function metricsForEnv(renv) {
|
|
|
57372
57625
|
{ defaultStep: 60, dashboardUids: g.dashboards }
|
|
57373
57626
|
);
|
|
57374
57627
|
}
|
|
57628
|
+
function dbHasRole(db, roles) {
|
|
57629
|
+
return db.roles.some((r) => roles.includes(r));
|
|
57630
|
+
}
|
|
57631
|
+
function queueDatabaseForEnv(renv) {
|
|
57632
|
+
const dbs = renv.connectors.redis?.databases;
|
|
57633
|
+
if (!dbs || dbs.length === 0) return null;
|
|
57634
|
+
return dbs.find((d) => dbHasRole(d, REDIS_QUEUE_ROLES)) ?? (dbs.length === 1 ? dbs[0] : null);
|
|
57635
|
+
}
|
|
57636
|
+
var STATE_ROLES = ["cache", "state", "locks", "rate-limit", "session", "dedupe"];
|
|
57637
|
+
function stateDatabasesForEnv(renv) {
|
|
57638
|
+
const dbs = renv.connectors.redis?.databases ?? [];
|
|
57639
|
+
return dbs.filter((d) => dbHasRole(d, STATE_ROLES));
|
|
57640
|
+
}
|
|
57641
|
+
function redisStateForEnv(renv) {
|
|
57642
|
+
const r = renv.connectors.redis;
|
|
57643
|
+
if (!r?.url) return null;
|
|
57644
|
+
const stateDbs = stateDatabasesForEnv(renv);
|
|
57645
|
+
if (stateDbs.length === 0) return null;
|
|
57646
|
+
const baseUrl = r.url;
|
|
57647
|
+
return new RedisStateRuntimeProvider(
|
|
57648
|
+
stateDbs.map((d) => ({
|
|
57649
|
+
db: d.db,
|
|
57650
|
+
...d.name !== void 0 ? { name: d.name } : {},
|
|
57651
|
+
roles: d.roles,
|
|
57652
|
+
url: redisUrlForDb(baseUrl, d.db),
|
|
57653
|
+
...d.scan !== void 0 ? { scan: { sampleLimit: d.scan.sampleLimit, patterns: d.scan.patterns } } : {}
|
|
57654
|
+
}))
|
|
57655
|
+
);
|
|
57656
|
+
}
|
|
57375
57657
|
function queueForEnv(renv) {
|
|
57376
57658
|
const r = renv.connectors.redis;
|
|
57377
57659
|
if (!r?.url) return null;
|
|
57378
|
-
|
|
57660
|
+
const queueDb = queueDatabaseForEnv(renv);
|
|
57661
|
+
if (!queueDb) return null;
|
|
57662
|
+
return new BullMQRuntimeProvider(
|
|
57663
|
+
new BullMQRedisClient({ url: redisUrlForDb(r.url, queueDb.db), prefix: queueDb.bullmqPrefix })
|
|
57664
|
+
);
|
|
57379
57665
|
}
|
|
57380
57666
|
function mongoForEnv(renv) {
|
|
57381
57667
|
const m = renv.connectors.mongodb;
|
|
@@ -57545,6 +57831,130 @@ init_cjs_shims();
|
|
|
57545
57831
|
// ../../packages/connectors/src/bullmq/index.ts
|
|
57546
57832
|
init_cjs_shims();
|
|
57547
57833
|
|
|
57834
|
+
// ../../packages/connectors/src/redis/index.ts
|
|
57835
|
+
init_cjs_shims();
|
|
57836
|
+
|
|
57837
|
+
// ../../packages/connectors/src/redis/discovery.ts
|
|
57838
|
+
init_cjs_shims();
|
|
57839
|
+
function inferStateRoles(prefixes) {
|
|
57840
|
+
const roles = /* @__PURE__ */ new Set(["cache", "state"]);
|
|
57841
|
+
for (const { prefix } of prefixes) {
|
|
57842
|
+
const p = prefix.toLowerCase();
|
|
57843
|
+
if (/lock/.test(p)) roles.add("locks");
|
|
57844
|
+
if (/rate.?limit|throttle/.test(p)) roles.add("rate-limit");
|
|
57845
|
+
if (/session|sess/.test(p)) roles.add("session");
|
|
57846
|
+
if (/dedup/.test(p)) roles.add("dedupe");
|
|
57847
|
+
}
|
|
57848
|
+
return [...roles];
|
|
57849
|
+
}
|
|
57850
|
+
async function probeRedisDatabases(baseUrl, opts = {}) {
|
|
57851
|
+
const dbRange = opts.dbRange ?? Array.from({ length: 16 }, (_, i) => i);
|
|
57852
|
+
const sampleLimit = opts.sampleLimit ?? 500;
|
|
57853
|
+
const bullmqPrefix = opts.bullmqPrefix ?? "bull";
|
|
57854
|
+
const probes = [];
|
|
57855
|
+
for (const db of dbRange) {
|
|
57856
|
+
const client = new RedisScanClient({ url: baseUrl, db });
|
|
57857
|
+
try {
|
|
57858
|
+
const health = await client.health();
|
|
57859
|
+
if (!health.ok) {
|
|
57860
|
+
probes.push({
|
|
57861
|
+
db,
|
|
57862
|
+
reachable: false,
|
|
57863
|
+
detail: health.detail,
|
|
57864
|
+
keyCount: 0,
|
|
57865
|
+
prefixes: [],
|
|
57866
|
+
bullmqQueues: [],
|
|
57867
|
+
suggestedRoles: []
|
|
57868
|
+
});
|
|
57869
|
+
break;
|
|
57870
|
+
}
|
|
57871
|
+
const keyCount = await client.dbSize();
|
|
57872
|
+
if (keyCount === 0) {
|
|
57873
|
+
probes.push({ db, reachable: true, keyCount: 0, prefixes: [], bullmqQueues: [], suggestedRoles: [] });
|
|
57874
|
+
continue;
|
|
57875
|
+
}
|
|
57876
|
+
const bullmqQueues = await client.detectBullmqQueues(bullmqPrefix);
|
|
57877
|
+
if (bullmqQueues.length > 0) {
|
|
57878
|
+
probes.push({
|
|
57879
|
+
db,
|
|
57880
|
+
reachable: true,
|
|
57881
|
+
keyCount,
|
|
57882
|
+
prefixes: [],
|
|
57883
|
+
bullmqQueues,
|
|
57884
|
+
bullmqPrefix,
|
|
57885
|
+
suggestedRoles: ["bullmq", "queues"]
|
|
57886
|
+
});
|
|
57887
|
+
} else {
|
|
57888
|
+
const prefixes = await client.samplePrefixes(sampleLimit);
|
|
57889
|
+
probes.push({
|
|
57890
|
+
db,
|
|
57891
|
+
reachable: true,
|
|
57892
|
+
keyCount,
|
|
57893
|
+
prefixes,
|
|
57894
|
+
bullmqQueues: [],
|
|
57895
|
+
suggestedRoles: inferStateRoles(prefixes)
|
|
57896
|
+
});
|
|
57897
|
+
}
|
|
57898
|
+
} finally {
|
|
57899
|
+
await client.close();
|
|
57900
|
+
}
|
|
57901
|
+
}
|
|
57902
|
+
return probes;
|
|
57903
|
+
}
|
|
57904
|
+
|
|
57905
|
+
// ../../packages/connectors/src/redis/status.ts
|
|
57906
|
+
init_cjs_shims();
|
|
57907
|
+
async function redisServerStatus(renv) {
|
|
57908
|
+
const r = renv.connectors.redis;
|
|
57909
|
+
if (!r?.url) return null;
|
|
57910
|
+
const url = r.url;
|
|
57911
|
+
let reachable = false;
|
|
57912
|
+
let authFailed = false;
|
|
57913
|
+
const databases = [];
|
|
57914
|
+
for (const d of r.databases) {
|
|
57915
|
+
const client = new RedisScanClient({ url: redisUrlForDb(url, d.db), db: d.db });
|
|
57916
|
+
try {
|
|
57917
|
+
const health = await client.health();
|
|
57918
|
+
const isQueue = d.roles.some((role) => REDIS_QUEUE_ROLES.includes(role));
|
|
57919
|
+
if (!health.ok) {
|
|
57920
|
+
if (/WRONGPASS|NOAUTH|invalid password/i.test(health.detail)) authFailed = true;
|
|
57921
|
+
databases.push({
|
|
57922
|
+
db: d.db,
|
|
57923
|
+
...d.name !== void 0 ? { name: d.name } : {},
|
|
57924
|
+
roles: d.roles,
|
|
57925
|
+
reachable: false,
|
|
57926
|
+
detail: health.detail
|
|
57927
|
+
});
|
|
57928
|
+
continue;
|
|
57929
|
+
}
|
|
57930
|
+
reachable = true;
|
|
57931
|
+
if (isQueue) {
|
|
57932
|
+
const queues = await client.detectBullmqQueues(d.bullmqPrefix);
|
|
57933
|
+
databases.push({
|
|
57934
|
+
db: d.db,
|
|
57935
|
+
...d.name !== void 0 ? { name: d.name } : {},
|
|
57936
|
+
roles: d.roles,
|
|
57937
|
+
reachable: true,
|
|
57938
|
+
queueCount: queues.length,
|
|
57939
|
+
bullmqPrefix: d.bullmqPrefix
|
|
57940
|
+
});
|
|
57941
|
+
} else {
|
|
57942
|
+
const keyCount = await client.dbSize();
|
|
57943
|
+
databases.push({
|
|
57944
|
+
db: d.db,
|
|
57945
|
+
...d.name !== void 0 ? { name: d.name } : {},
|
|
57946
|
+
roles: d.roles,
|
|
57947
|
+
reachable: true,
|
|
57948
|
+
keyCount
|
|
57949
|
+
});
|
|
57950
|
+
}
|
|
57951
|
+
} finally {
|
|
57952
|
+
await client.close();
|
|
57953
|
+
}
|
|
57954
|
+
}
|
|
57955
|
+
return { reachable, authFailed, databases };
|
|
57956
|
+
}
|
|
57957
|
+
|
|
57548
57958
|
// ../../packages/db/src/index.ts
|
|
57549
57959
|
init_cjs_shims();
|
|
57550
57960
|
|
|
@@ -66794,10 +67204,32 @@ async function checkEnv(renv, deps) {
|
|
|
66794
67204
|
}
|
|
66795
67205
|
const redisCfg = renv.connectors.redis;
|
|
66796
67206
|
if (redisCfg?.url) {
|
|
66797
|
-
const
|
|
66798
|
-
|
|
66799
|
-
|
|
66800
|
-
);
|
|
67207
|
+
const server = redisServerLabel(redisCfg.url);
|
|
67208
|
+
const status = await (deps?.redisStatus ?? redisServerStatus)(renv);
|
|
67209
|
+
if (!status) {
|
|
67210
|
+
console.log(` ${mark("pending")} ${import_picocolors.default.bold("Redis")} ${import_picocolors.default.dim(`configured \xB7 ${server}`)}`);
|
|
67211
|
+
} else if (!status.reachable) {
|
|
67212
|
+
const state = status.authFailed ? "auth failed" : "unreachable";
|
|
67213
|
+
console.log(` ${mark(false)} ${import_picocolors.default.bold("Redis")} ${import_picocolors.default.dim(`${state} \xB7 ${server}`)}`);
|
|
67214
|
+
allOk = false;
|
|
67215
|
+
} else {
|
|
67216
|
+
console.log(` ${mark(true)} ${import_picocolors.default.bold("Redis")} ${import_picocolors.default.dim(`reachable \xB7 ${server}`)}`);
|
|
67217
|
+
for (const d of status.databases) {
|
|
67218
|
+
const roleLabel = d.roles.length > 0 ? d.roles.join("/") : "unrolled";
|
|
67219
|
+
const name = d.name ? ` ${d.name}` : "";
|
|
67220
|
+
let detail;
|
|
67221
|
+
if (!d.reachable) {
|
|
67222
|
+
detail = `${/WRONGPASS|NOAUTH/i.test(d.detail ?? "") ? "auth failed" : "unreachable"}`;
|
|
67223
|
+
} else if (d.queueCount !== void 0) {
|
|
67224
|
+
detail = `${d.queueCount} queue(s), prefix ${d.bullmqPrefix}`;
|
|
67225
|
+
} else {
|
|
67226
|
+
detail = `${d.keyCount ?? 0} key(s)`;
|
|
67227
|
+
}
|
|
67228
|
+
console.log(
|
|
67229
|
+
` ${mark(d.reachable)} ${import_picocolors.default.bold(`DB ${d.db}`)}${name} ${import_picocolors.default.dim(`${roleLabel} \xB7 ${detail}`)}`
|
|
67230
|
+
);
|
|
67231
|
+
}
|
|
67232
|
+
}
|
|
66801
67233
|
} else {
|
|
66802
67234
|
console.log(
|
|
66803
67235
|
` ${mark("pending")} ${import_picocolors.default.bold("Redis")} ${import_picocolors.default.dim("not configured")}`
|
|
@@ -66820,6 +67252,14 @@ function redactRedisUrl(raw) {
|
|
|
66820
67252
|
return raw.replace(/\/\/:?[^@]*@/, "//:***@");
|
|
66821
67253
|
}
|
|
66822
67254
|
}
|
|
67255
|
+
function redisServerLabel(raw) {
|
|
67256
|
+
try {
|
|
67257
|
+
const u = new URL(raw);
|
|
67258
|
+
return `${u.hostname}:${u.port || "6379"}`;
|
|
67259
|
+
} catch {
|
|
67260
|
+
return redactRedisUrl(raw);
|
|
67261
|
+
}
|
|
67262
|
+
}
|
|
66823
67263
|
async function runStatus(configPath, opts) {
|
|
66824
67264
|
console.log(import_picocolors.default.bold(`
|
|
66825
67265
|
Horus ${HORUS_VERSION}`));
|
|
@@ -66869,7 +67309,7 @@ Horus ${HORUS_VERSION}`));
|
|
|
66869
67309
|
console.error(import_picocolors.default.red(err.message));
|
|
66870
67310
|
return 1;
|
|
66871
67311
|
}
|
|
66872
|
-
const ok = await checkEnv(renv, { mongoFactory: opts?._mongoFactory });
|
|
67312
|
+
const ok = await checkEnv(renv, { mongoFactory: opts?._mongoFactory, redisStatus: opts?._redisStatus });
|
|
66873
67313
|
return ok ? 0 : 1;
|
|
66874
67314
|
}
|
|
66875
67315
|
let allHealthy = true;
|
|
@@ -66882,7 +67322,7 @@ Horus ${HORUS_VERSION}`));
|
|
|
66882
67322
|
allHealthy = false;
|
|
66883
67323
|
continue;
|
|
66884
67324
|
}
|
|
66885
|
-
const ok = await checkEnv(renv, { mongoFactory: opts?._mongoFactory });
|
|
67325
|
+
const ok = await checkEnv(renv, { mongoFactory: opts?._mongoFactory, redisStatus: opts?._redisStatus });
|
|
66886
67326
|
if (!ok) allHealthy = false;
|
|
66887
67327
|
}
|
|
66888
67328
|
return checks.some((c) => c.ok === false && c.fatal) ? 1 : 0;
|
|
@@ -67348,7 +67788,14 @@ async function runQueues(name, opts) {
|
|
|
67348
67788
|
const config = await loadConfig(opts.config, { name: opts.name });
|
|
67349
67789
|
const { db, sql: sql2 } = createDb(config.database.url);
|
|
67350
67790
|
try {
|
|
67351
|
-
|
|
67791
|
+
let project = opts.project;
|
|
67792
|
+
if (project === void 0) {
|
|
67793
|
+
try {
|
|
67794
|
+
project = resolveEnvironment(config, { project: opts.project }).project;
|
|
67795
|
+
} catch {
|
|
67796
|
+
}
|
|
67797
|
+
}
|
|
67798
|
+
const rows = await listQueueEdges(db, { project, queueName: name });
|
|
67352
67799
|
console.log(
|
|
67353
67800
|
import_picocolors4.default.bold("Queue topology") + import_picocolors4.default.dim(" \xB7 source: code / source intelligence \xB7 static (run horus index to refresh)")
|
|
67354
67801
|
);
|
|
@@ -67455,8 +67902,15 @@ async function runLiveMode(config, rows, nameFilter) {
|
|
|
67455
67902
|
console.log(import_picocolors4.default.red(` \u2717 Redis unreachable: ${health.detail}`));
|
|
67456
67903
|
return;
|
|
67457
67904
|
}
|
|
67458
|
-
const
|
|
67459
|
-
|
|
67905
|
+
const staticNames = new Set(buildQueueMap(rows).keys());
|
|
67906
|
+
let queueNames;
|
|
67907
|
+
if (nameFilter !== void 0) {
|
|
67908
|
+
queueNames = [nameFilter];
|
|
67909
|
+
} else {
|
|
67910
|
+
const discovered = await queueProvider.discoverQueues().catch(() => []);
|
|
67911
|
+
const union2 = /* @__PURE__ */ new Set([...staticNames, ...discovered]);
|
|
67912
|
+
queueNames = union2.size > 0 ? [...union2] : void 0;
|
|
67913
|
+
}
|
|
67460
67914
|
const state = await queueProvider.analyzeQueues({ queueNames });
|
|
67461
67915
|
const collectedAt = new Date(state.collectedAt).toLocaleTimeString();
|
|
67462
67916
|
console.log(
|
|
@@ -67473,7 +67927,7 @@ async function runLiveMode(config, rows, nameFilter) {
|
|
|
67473
67927
|
);
|
|
67474
67928
|
return;
|
|
67475
67929
|
}
|
|
67476
|
-
printLiveTable(state.queues);
|
|
67930
|
+
printLiveTable(state.queues, staticNames);
|
|
67477
67931
|
} catch (err) {
|
|
67478
67932
|
if (!headerPrinted) {
|
|
67479
67933
|
console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
|
|
@@ -67484,7 +67938,7 @@ async function runLiveMode(config, rows, nameFilter) {
|
|
|
67484
67938
|
});
|
|
67485
67939
|
}
|
|
67486
67940
|
}
|
|
67487
|
-
function printLiveTable(queues) {
|
|
67941
|
+
function printLiveTable(queues, staticNames = /* @__PURE__ */ new Set()) {
|
|
67488
67942
|
const nameWidth = Math.max(10, ...queues.map((q) => q.queueName.length));
|
|
67489
67943
|
const numWidth = 7;
|
|
67490
67944
|
const header = " " + "queue".padEnd(nameWidth) + " " + "waiting".padStart(numWidth) + " " + "active".padStart(numWidth) + " " + "failed".padStart(numWidth) + " " + "delayed".padStart(numWidth) + " " + "paused".padStart(numWidth);
|
|
@@ -67495,6 +67949,9 @@ function printLiveTable(queues) {
|
|
|
67495
67949
|
const color = hasIssue ? import_picocolors4.default.yellow : (s) => s;
|
|
67496
67950
|
const row = " " + q.queueName.padEnd(nameWidth) + " " + String(q.waiting).padStart(numWidth) + " " + String(q.active).padStart(numWidth) + " " + String(q.failed).padStart(numWidth) + " " + String(q.delayed).padStart(numWidth) + " " + (q.isPaused ? import_picocolors4.default.yellow("paused") : String(q.paused).padStart(numWidth));
|
|
67497
67951
|
console.log(color(row));
|
|
67952
|
+
if (!staticNames.has(q.queueName)) {
|
|
67953
|
+
console.log(import_picocolors4.default.dim(" runtime-only \xB7 no static producer/worker mapping"));
|
|
67954
|
+
}
|
|
67498
67955
|
if (q.oldestWaitingMs !== void 0) {
|
|
67499
67956
|
const age = formatAge(q.oldestWaitingMs);
|
|
67500
67957
|
console.log(import_picocolors4.default.dim(` oldest waiting: ${age}`));
|
|
@@ -69730,6 +70187,22 @@ async function investigate(input, deps) {
|
|
|
69730
70187
|
stateAnalysis = null;
|
|
69731
70188
|
}
|
|
69732
70189
|
}
|
|
70190
|
+
let redisStateAnalysis = null;
|
|
70191
|
+
const redisStateEvIds = [];
|
|
70192
|
+
if (deps.redisState) {
|
|
70193
|
+
try {
|
|
70194
|
+
redisStateAnalysis = await deps.redisState.analyzeRedisState();
|
|
70195
|
+
const redisTerms = [...new Set(tokenize(hint))];
|
|
70196
|
+
for (const s of redisStateAnalysis.signals) {
|
|
70197
|
+
const matched = redisTerms.some((t) => t.length > 2 && s.matchText.includes(t));
|
|
70198
|
+
const relevance = matched ? Math.min(1, s.relevance + 0.3) : s.relevance;
|
|
70199
|
+
const ev = mkEv("redis-key", s.title, s.payload, {}, redisStateAnalysis.collectedAt, relevance);
|
|
70200
|
+
redisStateEvIds.push(ev.id);
|
|
70201
|
+
}
|
|
70202
|
+
} catch {
|
|
70203
|
+
redisStateAnalysis = null;
|
|
70204
|
+
}
|
|
70205
|
+
}
|
|
69733
70206
|
let queueRuntimeState = null;
|
|
69734
70207
|
const queueRuntimeEvIds = [];
|
|
69735
70208
|
const queueRuntimeEvIdsByQueue = /* @__PURE__ */ new Map();
|
|
@@ -69963,6 +70436,15 @@ async function investigate(input, deps) {
|
|
|
69963
70436
|
evidenceIds: stateEvIds
|
|
69964
70437
|
});
|
|
69965
70438
|
}
|
|
70439
|
+
if (redisStateAnalysis !== null && redisStateEvIds.length > 0) {
|
|
70440
|
+
const dbCount = redisStateAnalysis.databases.filter((d) => d.keyCount > 0).length;
|
|
70441
|
+
findings2.push({
|
|
70442
|
+
kind: "observation",
|
|
70443
|
+
title: `Redis runtime state: ${redisStateEvIds.length} signal(s) across ${dbCount} DB(s)`,
|
|
70444
|
+
confidence: 0.5,
|
|
70445
|
+
evidenceIds: redisStateEvIds
|
|
70446
|
+
});
|
|
70447
|
+
}
|
|
69966
70448
|
if (queueRuntimeState !== null && queueRuntimeEvIds.length > 0) {
|
|
69967
70449
|
const starved = queueRuntimeState.queues.filter((q) => q.waiting >= 10 && q.active === 0);
|
|
69968
70450
|
const starvedNames = new Set(starved.map((q) => q.queueName));
|
|
@@ -70109,7 +70591,7 @@ async function investigate(input, deps) {
|
|
|
70109
70591
|
const providerReliability = {
|
|
70110
70592
|
code: 0.8,
|
|
70111
70593
|
...deps.logs != null ? { logs: 0.7 } : {},
|
|
70112
|
-
...deps.mongo != null ? { state: 0.85 } : {},
|
|
70594
|
+
...deps.mongo != null || deps.redisState != null ? { state: 0.85 } : {},
|
|
70113
70595
|
...deps.queue != null ? { queue: 0.9 } : {},
|
|
70114
70596
|
...deps.metrics != null ? { metrics: 0.75 } : {}
|
|
70115
70597
|
};
|
|
@@ -73148,6 +73630,7 @@ async function runInvestigate(hint, opts) {
|
|
|
73148
73630
|
const logs = logsForEnv(renv);
|
|
73149
73631
|
const mongo = mongoForEnv(renv);
|
|
73150
73632
|
const queue = queueForEnv(renv);
|
|
73633
|
+
const redisState = redisStateForEnv(renv);
|
|
73151
73634
|
const metrics = metricsForEnv(renv);
|
|
73152
73635
|
const service = opts.service ?? renv.connectors.elasticsearch?.serviceName;
|
|
73153
73636
|
const { db, sql: sql2 } = createDb(config.database.url);
|
|
@@ -73160,6 +73643,7 @@ async function runInvestigate(hint, opts) {
|
|
|
73160
73643
|
logs,
|
|
73161
73644
|
mongo,
|
|
73162
73645
|
queue,
|
|
73646
|
+
redisState,
|
|
73163
73647
|
metrics,
|
|
73164
73648
|
repoPath: renv.path,
|
|
73165
73649
|
connectors: {
|
|
@@ -74612,6 +75096,20 @@ async function checkboxSearch(opts) {
|
|
|
74612
75096
|
}
|
|
74613
75097
|
|
|
74614
75098
|
// ../../packages/cli/src/commands/connect.ts
|
|
75099
|
+
function parseDbSpec(spec) {
|
|
75100
|
+
const [dbPart, rolesPart] = spec.split(":");
|
|
75101
|
+
const db = Number.parseInt((dbPart ?? "").trim(), 10);
|
|
75102
|
+
if (!Number.isInteger(db) || db < 0 || db > 15) {
|
|
75103
|
+
throw new Error(`Invalid --db spec "${spec}": DB index must be 0\u201315 (e.g. 1:bullmq,queues)`);
|
|
75104
|
+
}
|
|
75105
|
+
const roles = (rolesPart ?? "").split(",").map((r) => r.trim()).filter(Boolean);
|
|
75106
|
+
for (const role of roles) {
|
|
75107
|
+
if (!REDIS_ROLES.includes(role)) {
|
|
75108
|
+
throw new Error(`Invalid role "${role}" in --db spec "${spec}". Valid roles: ${REDIS_ROLES.join(", ")}`);
|
|
75109
|
+
}
|
|
75110
|
+
}
|
|
75111
|
+
return { db, roles };
|
|
75112
|
+
}
|
|
74615
75113
|
var SUPPORTED = ["elasticsearch", "mongodb", "grafana", "redis"];
|
|
74616
75114
|
async function runConnect(type, opts) {
|
|
74617
75115
|
if (!SUPPORTED.includes(type)) {
|
|
@@ -74756,6 +75254,14 @@ ${import_picocolors27.default.bold(`Connect ${type}`)} ${import_picocolors27.def
|
|
|
74756
75254
|
filled.url = injectRedisPassword(filled.url, pw);
|
|
74757
75255
|
}
|
|
74758
75256
|
}
|
|
75257
|
+
if (filled.db && filled.db.length > 0) {
|
|
75258
|
+
filled.redisDatabases = filled.db.map(parseDbSpec);
|
|
75259
|
+
} else if (filled.url && filled.scanDbs !== false && isInteractive()) {
|
|
75260
|
+
const scan = await askScanDbs();
|
|
75261
|
+
if (scan) {
|
|
75262
|
+
filled.redisDatabases = await discoverAndSelectDbs(filled.url, filled.bullmqPrefix ?? "bull");
|
|
75263
|
+
}
|
|
75264
|
+
}
|
|
74759
75265
|
break;
|
|
74760
75266
|
}
|
|
74761
75267
|
}
|
|
@@ -74863,8 +75369,64 @@ function buildGrafanaPatch(opts) {
|
|
|
74863
75369
|
function buildRedisPatch(opts) {
|
|
74864
75370
|
const patch = {};
|
|
74865
75371
|
if (opts.url) patch["url"] = opts.url;
|
|
75372
|
+
const dbs = opts.redisDatabases ?? (opts.db ? opts.db.map(parseDbSpec) : void 0);
|
|
75373
|
+
if (dbs && dbs.length > 0) {
|
|
75374
|
+
const prefix = opts.bullmqPrefix ?? "bull";
|
|
75375
|
+
patch["databases"] = dbs.map((d) => {
|
|
75376
|
+
const isQueue = d.roles.some((r) => r === "bullmq" || r === "queues");
|
|
75377
|
+
return {
|
|
75378
|
+
db: d.db,
|
|
75379
|
+
...d.name ? { name: d.name } : {},
|
|
75380
|
+
roles: d.roles,
|
|
75381
|
+
...isQueue ? { bullmq: { prefix } } : {}
|
|
75382
|
+
};
|
|
75383
|
+
});
|
|
75384
|
+
}
|
|
74866
75385
|
return patch;
|
|
74867
75386
|
}
|
|
75387
|
+
async function askScanDbs() {
|
|
75388
|
+
const answer = (await ask("Scan Redis DBs 0-15 to detect queues vs cache?", "yes", false)).trim().toLowerCase();
|
|
75389
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
75390
|
+
}
|
|
75391
|
+
function describeProbe(p) {
|
|
75392
|
+
if (!p.reachable) return `unreachable (${p.detail ?? "error"})`;
|
|
75393
|
+
if (p.keyCount === 0) return "empty";
|
|
75394
|
+
if (p.bullmqQueues.length > 0) {
|
|
75395
|
+
const sample = p.bullmqQueues.slice(0, 3).join(", ");
|
|
75396
|
+
return `${p.bullmqQueues.length} BullMQ queue(s) \xB7 prefix ${p.bullmqPrefix} \xB7 ${sample}`;
|
|
75397
|
+
}
|
|
75398
|
+
const examples = p.prefixes.slice(0, 3).map((x) => `${x.prefix}:*`).join(", ");
|
|
75399
|
+
return `${p.keyCount} keys \xB7 ${p.suggestedRoles.join("/")}${examples ? ` \xB7 ${examples}` : ""}`;
|
|
75400
|
+
}
|
|
75401
|
+
async function discoverAndSelectDbs(url, bullmqPrefix) {
|
|
75402
|
+
console.log(import_picocolors27.default.dim("\n Scanning DBs 0-15 (read-only, sampled)\u2026"));
|
|
75403
|
+
const probes = await probeRedisDatabases(url, { bullmqPrefix });
|
|
75404
|
+
const nonEmpty = probes.filter((p) => p.reachable && p.keyCount > 0);
|
|
75405
|
+
if (nonEmpty.length === 0) {
|
|
75406
|
+
console.log(import_picocolors27.default.dim(" No populated DBs found."));
|
|
75407
|
+
return [];
|
|
75408
|
+
}
|
|
75409
|
+
for (const p of nonEmpty) {
|
|
75410
|
+
console.log(` ${import_picocolors27.default.bold(`DB ${p.db}`)} ${import_picocolors27.default.dim("\xB7 " + describeProbe(p))}`);
|
|
75411
|
+
}
|
|
75412
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
75413
|
+
const choices = nonEmpty.map((p) => {
|
|
75414
|
+
const label = `DB ${p.db} \u2014 ${p.suggestedRoles.join("/") || "unrolled"} (${describeProbe(p)})`;
|
|
75415
|
+
byLabel.set(label, p);
|
|
75416
|
+
return label;
|
|
75417
|
+
});
|
|
75418
|
+
let selected;
|
|
75419
|
+
try {
|
|
75420
|
+
selected = await checkboxSearch({ message: "Select DBs to save", choices, pageSize: 12 });
|
|
75421
|
+
} catch (err) {
|
|
75422
|
+
if (err instanceof ExitPromptError) throw err;
|
|
75423
|
+
selected = choices;
|
|
75424
|
+
}
|
|
75425
|
+
return selected.map((label) => {
|
|
75426
|
+
const p = byLabel.get(label);
|
|
75427
|
+
return { db: p.db, name: p.suggestedRoles.includes("bullmq") ? "queues" : "cache", roles: p.suggestedRoles };
|
|
75428
|
+
});
|
|
75429
|
+
}
|
|
74868
75430
|
async function probe(type, opts) {
|
|
74869
75431
|
try {
|
|
74870
75432
|
switch (type) {
|
|
@@ -74920,36 +75482,23 @@ async function probe(type, opts) {
|
|
|
74920
75482
|
}
|
|
74921
75483
|
case "redis": {
|
|
74922
75484
|
if (!opts.url) return { ok: true, detail: "skipped (no URL)" };
|
|
74923
|
-
const
|
|
74924
|
-
|
|
74925
|
-
|
|
74926
|
-
|
|
74927
|
-
|
|
75485
|
+
const client = new RedisScanClient({ url: opts.url });
|
|
75486
|
+
try {
|
|
75487
|
+
const h = await client.health();
|
|
75488
|
+
const host = new URL(opts.url).hostname;
|
|
75489
|
+
const port = new URL(opts.url).port || "6379";
|
|
75490
|
+
if (h.ok) return { ok: true, detail: `PING ok at ${host}:${port}` };
|
|
75491
|
+
const authFailed = /WRONGPASS|NOAUTH|invalid password/i.test(h.detail);
|
|
75492
|
+
return { ok: false, detail: authFailed ? `auth failed (${h.detail})` : h.detail };
|
|
75493
|
+
} finally {
|
|
75494
|
+
await client.close();
|
|
75495
|
+
}
|
|
74928
75496
|
}
|
|
74929
75497
|
}
|
|
74930
75498
|
} catch (err) {
|
|
74931
75499
|
return { ok: false, detail: err.message };
|
|
74932
75500
|
}
|
|
74933
75501
|
}
|
|
74934
|
-
function tcpProbe(host, port, timeoutMs = 3e3) {
|
|
74935
|
-
return new Promise((resolve8) => {
|
|
74936
|
-
const { createConnection } = require("net");
|
|
74937
|
-
const sock = createConnection({ host, port });
|
|
74938
|
-
const timer2 = setTimeout(() => {
|
|
74939
|
-
sock.destroy();
|
|
74940
|
-
resolve8(false);
|
|
74941
|
-
}, timeoutMs);
|
|
74942
|
-
sock.on("connect", () => {
|
|
74943
|
-
clearTimeout(timer2);
|
|
74944
|
-
sock.destroy();
|
|
74945
|
-
resolve8(true);
|
|
74946
|
-
});
|
|
74947
|
-
sock.on("error", () => {
|
|
74948
|
-
clearTimeout(timer2);
|
|
74949
|
-
resolve8(false);
|
|
74950
|
-
});
|
|
74951
|
-
});
|
|
74952
|
-
}
|
|
74953
75502
|
function printSummary(type, opts) {
|
|
74954
75503
|
const lines = [];
|
|
74955
75504
|
if (opts.url) lines.push(` url: ${redactUrl(opts.url)}`);
|
|
@@ -76025,7 +76574,15 @@ Examples:
|
|
|
76025
76574
|
});
|
|
76026
76575
|
program2.command("connect <type>").description(
|
|
76027
76576
|
"Add or update a runtime connector (elasticsearch / mongodb / grafana / redis) in .horus/config.json"
|
|
76028
|
-
).option("--env <name>", "target environment (default: first environment in config)").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("--dashboard <uid>", "default dashboard uid (grafana)").option(
|
|
76577
|
+
).option("--env <name>", "target environment (default: first environment in config)").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("--dashboard <uid>", "default dashboard uid (grafana)").option(
|
|
76578
|
+
"--db <spec>",
|
|
76579
|
+
"redis logical DB as db:role1,role2 (e.g. 0:cache,state or 1:bullmq,queues); repeatable",
|
|
76580
|
+
(val, acc) => {
|
|
76581
|
+
acc.push(val);
|
|
76582
|
+
return acc;
|
|
76583
|
+
},
|
|
76584
|
+
[]
|
|
76585
|
+
).option("--bullmq-prefix <prefix>", "BullMQ key prefix for redis queue DBs (default: bull)").option("--no-scan-dbs", "skip interactive Redis DB scan").option("--no-test", "skip live connection probe").action(
|
|
76029
76586
|
async (type, opts) => {
|
|
76030
76587
|
process.exitCode = await runConnect(type, {
|
|
76031
76588
|
env: opts.env,
|
|
@@ -76037,6 +76594,9 @@ Examples:
|
|
|
76037
76594
|
database: opts.database,
|
|
76038
76595
|
collections: opts.collections,
|
|
76039
76596
|
dashboard: opts.dashboard,
|
|
76597
|
+
db: opts.db,
|
|
76598
|
+
bullmqPrefix: opts.bullmqPrefix,
|
|
76599
|
+
scanDbs: opts.scanDbs,
|
|
76040
76600
|
noTest: opts.test === false
|
|
76041
76601
|
});
|
|
76042
76602
|
}
|