mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-26 09:07:59 +02:00
488 lines
15 KiB
JavaScript
488 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
const HELP_TEXT = `
|
|
ShadowBroker DM root health Prometheus exporter
|
|
|
|
Usage:
|
|
node scripts/mesh/export-dm-root-health-prometheus.mjs [--stdout] [--output PATH] [--base-url URL] [--health-path PATH]
|
|
|
|
Environment:
|
|
SB_DM_ROOT_BASE_URL=http://127.0.0.1:8000
|
|
SB_DM_ROOT_HEALTH_PATH=/api/wormhole/dm/root-health
|
|
SB_DM_ROOT_AUTH_HEADER=X-Admin-Key: change-me
|
|
SB_DM_ROOT_AUTH_COOKIE=operator_session=...
|
|
SB_DM_ROOT_TIMEOUT_MS=10000
|
|
SB_DM_ROOT_PROMETHEUS_OUTPUT=/var/lib/node_exporter/textfile_collector/shadowbroker_dm_root.prom
|
|
|
|
Flags:
|
|
--stdout Print Prometheus metrics to stdout
|
|
--output PATH Override SB_DM_ROOT_PROMETHEUS_OUTPUT
|
|
--base-url URL Override SB_DM_ROOT_BASE_URL
|
|
--health-path PATH Override SB_DM_ROOT_HEALTH_PATH
|
|
--help Show this text
|
|
|
|
Exit codes:
|
|
0 = export succeeded
|
|
2 = fetch or payload validation failed
|
|
`.trim();
|
|
|
|
function parseArgs(argv) {
|
|
const parsed = {};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const current = String(argv[index] || '').trim();
|
|
if (!current) continue;
|
|
if (current === '--stdout') {
|
|
parsed.stdout = true;
|
|
continue;
|
|
}
|
|
if (current === '--help' || current === '-h') {
|
|
parsed.help = true;
|
|
continue;
|
|
}
|
|
if (
|
|
(current === '--output' || current === '--base-url' || current === '--health-path') &&
|
|
index + 1 < argv.length
|
|
) {
|
|
parsed[current.slice(2).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())] =
|
|
String(argv[index + 1] || '').trim();
|
|
index += 1;
|
|
}
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function normalizeUrl(baseUrl, healthPath) {
|
|
const base = String(baseUrl || 'http://127.0.0.1:8000').trim().replace(/\/+$/, '');
|
|
const pathValue = String(healthPath || '/api/wormhole/dm/root-health').trim();
|
|
if (!pathValue) {
|
|
return `${base}/api/wormhole/dm/root-health`;
|
|
}
|
|
return pathValue.startsWith('http://') || pathValue.startsWith('https://')
|
|
? pathValue
|
|
: `${base}/${pathValue.replace(/^\/+/, '')}`;
|
|
}
|
|
|
|
function parseHeader(rawValue) {
|
|
const raw = String(rawValue || '').trim();
|
|
if (!raw) return null;
|
|
const separator = raw.indexOf(':');
|
|
if (separator <= 0) return null;
|
|
const name = raw.slice(0, separator).trim();
|
|
const value = raw.slice(separator + 1).trim();
|
|
if (!name || !value) return null;
|
|
return [name, value];
|
|
}
|
|
|
|
function safeInt(value, fallback = 0) {
|
|
const numeric = Number(value);
|
|
if (!Number.isFinite(numeric)) return fallback;
|
|
return Math.trunc(numeric);
|
|
}
|
|
|
|
function boolGauge(value) {
|
|
return value ? 1 : 0;
|
|
}
|
|
|
|
function metricEscape(value) {
|
|
return String(value ?? '')
|
|
.replace(/\\/g, '\\\\')
|
|
.replace(/\n/g, '\\n')
|
|
.replace(/"/g, '\\"');
|
|
}
|
|
|
|
function labelsText(labels) {
|
|
const entries = Object.entries(labels || {}).filter(([, value]) => String(value ?? '').length > 0);
|
|
if (!entries.length) return '';
|
|
return `{${entries.map(([key, value]) => `${key}="${metricEscape(value)}"`).join(',')}}`;
|
|
}
|
|
|
|
function appendMetric(lines, name, help, type, value, labels = undefined) {
|
|
lines.push(`# HELP ${name} ${help}`);
|
|
lines.push(`# TYPE ${name} ${type}`);
|
|
lines.push(`${name}${labelsText(labels)} ${value}`);
|
|
}
|
|
|
|
function stateCode(value, mapping, fallback) {
|
|
const key = String(value || '').trim().toLowerCase();
|
|
if (Object.prototype.hasOwnProperty.call(mapping, key)) {
|
|
return mapping[key];
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
function buildMetrics(payload, errorDetail = '') {
|
|
const checkedAt = safeInt(payload?.checked_at || Math.floor(Date.now() / 1000), Math.floor(Date.now() / 1000));
|
|
const summaryState = String(payload?.state || '').trim().toLowerCase();
|
|
const healthState = String(payload?.health_state || '').trim().toLowerCase();
|
|
const monitorState = String(payload?.monitoring?.state || '').trim().toLowerCase();
|
|
const witnessState = String(payload?.witness?.state || '').trim().toLowerCase();
|
|
const transparencyState = String(payload?.transparency?.state || '').trim().toLowerCase();
|
|
const nextAction = String(payload?.next_action || '').trim();
|
|
const alerts = Array.isArray(payload?.alerts) ? payload.alerts.filter((item) => item && typeof item === 'object') : [];
|
|
|
|
const summaryStateCode = stateCode(
|
|
summaryState,
|
|
{ local_cached_only: 0, current_external: 1, stale_external: 2 },
|
|
-1,
|
|
);
|
|
const healthStateCode = stateCode(
|
|
healthState,
|
|
{ ok: 0, warning: 1, stale: 2, error: 3 },
|
|
-1,
|
|
);
|
|
const monitorStateCode = stateCode(
|
|
monitorState,
|
|
{ ok: 0, warning: 1, critical: 2 },
|
|
-1,
|
|
);
|
|
const witnessStateCode = stateCode(
|
|
witnessState,
|
|
{ not_configured: 0, descriptors_only: 1, current: 2, stale: 3, error: 4 },
|
|
-1,
|
|
);
|
|
const transparencyStateCode = stateCode(
|
|
transparencyState,
|
|
{ not_configured: 0, current: 1, stale: 2, error: 3 },
|
|
-1,
|
|
);
|
|
|
|
const lines = [];
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_health_scrape_success',
|
|
'Whether the DM root health scrape succeeded.',
|
|
'gauge',
|
|
errorDetail ? 0 : 1,
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_checked_at_unixtime',
|
|
'Unix timestamp for the most recent DM root health check represented in this export.',
|
|
'gauge',
|
|
checkedAt,
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_summary_state_code',
|
|
'DM root operator summary state (0=local_cached_only, 1=current_external, 2=stale_external, -1=unknown).',
|
|
'gauge',
|
|
summaryStateCode,
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_health_state_code',
|
|
'DM root rolled-up health state (0=ok, 1=warning, 2=stale, 3=error, -1=unknown).',
|
|
'gauge',
|
|
healthStateCode,
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_monitor_state_code',
|
|
'Monitoring severity for DM root health (0=ok, 1=warning, 2=critical, -1=unknown).',
|
|
'gauge',
|
|
monitorStateCode,
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_strong_trust_blocked',
|
|
'Whether strong DM trust is currently blocked by external assurance state.',
|
|
'gauge',
|
|
boolGauge(Boolean(payload?.strong_trust_blocked)),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_external_assurance_current',
|
|
'Whether configured external witness and transparency assurances are both current.',
|
|
'gauge',
|
|
boolGauge(Boolean(payload?.external_assurance_current)),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_requires_attention',
|
|
'Whether DM root external assurance currently requires operator attention.',
|
|
'gauge',
|
|
boolGauge(Boolean(payload?.requires_attention)),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_independent_quorum_met',
|
|
'Whether the current witness state satisfies independent quorum.',
|
|
'gauge',
|
|
boolGauge(Boolean(payload?.independent_quorum_met)),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_alert_count',
|
|
'Number of active DM root health alerts.',
|
|
'gauge',
|
|
safeInt(payload?.alert_count, 0),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_blocking_alert_count',
|
|
'Number of active blocking DM root health alerts.',
|
|
'gauge',
|
|
safeInt(payload?.blocking_alert_count, 0),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_warning_alert_count',
|
|
'Number of active warning-level DM root health alerts.',
|
|
'gauge',
|
|
safeInt(payload?.warning_alert_count, 0),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_witness_state_code',
|
|
'Witness operator state (0=not_configured, 1=descriptors_only, 2=current, 3=stale, 4=error, -1=unknown).',
|
|
'gauge',
|
|
witnessStateCode,
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_witness_health_state_code',
|
|
'Witness health state (0=ok, 1=warning, 2=stale, 3=error, -1=unknown).',
|
|
'gauge',
|
|
stateCode(String(payload?.witness?.health_state || '').trim().toLowerCase(), { ok: 0, warning: 1, stale: 2, error: 3 }, -1),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_witness_age_seconds',
|
|
'Age in seconds of the current external witness package.',
|
|
'gauge',
|
|
safeInt(payload?.witness?.age_s, 0),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_witness_warning_window_seconds',
|
|
'Configured warning threshold for external witness freshness.',
|
|
'gauge',
|
|
safeInt(payload?.witness?.warning_window_s, 0),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_witness_freshness_window_seconds',
|
|
'Configured maximum freshness window for external witness material.',
|
|
'gauge',
|
|
safeInt(payload?.witness?.freshness_window_s, 0),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_witness_reacquire_required',
|
|
'Whether external witness receipt reacquisition is currently required.',
|
|
'gauge',
|
|
boolGauge(Boolean(payload?.witness?.reacquire_required)),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_witness_manifest_matches_current',
|
|
'Whether the external witness material matches the current manifest fingerprint.',
|
|
'gauge',
|
|
boolGauge(Boolean(payload?.witness?.manifest_matches_current)),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_witness_independent_quorum_met',
|
|
'Whether the witness side independently satisfies quorum.',
|
|
'gauge',
|
|
boolGauge(Boolean(payload?.witness?.independent_quorum_met)),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_transparency_state_code',
|
|
'Transparency operator state (0=not_configured, 1=current, 2=stale, 3=error, -1=unknown).',
|
|
'gauge',
|
|
transparencyStateCode,
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_transparency_health_state_code',
|
|
'Transparency health state (0=ok, 1=warning, 2=stale, 3=error, -1=unknown).',
|
|
'gauge',
|
|
stateCode(String(payload?.transparency?.health_state || '').trim().toLowerCase(), { ok: 0, warning: 1, stale: 2, error: 3 }, -1),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_transparency_age_seconds',
|
|
'Age in seconds of the current external transparency ledger readback.',
|
|
'gauge',
|
|
safeInt(payload?.transparency?.age_s, 0),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_transparency_warning_window_seconds',
|
|
'Configured warning threshold for external transparency freshness.',
|
|
'gauge',
|
|
safeInt(payload?.transparency?.warning_window_s, 0),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_transparency_freshness_window_seconds',
|
|
'Configured maximum freshness window for external transparency readback.',
|
|
'gauge',
|
|
safeInt(payload?.transparency?.freshness_window_s, 0),
|
|
);
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_transparency_verification_required',
|
|
'Whether transparency verification refresh is currently required.',
|
|
'gauge',
|
|
boolGauge(Boolean(payload?.transparency?.verification_required)),
|
|
);
|
|
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_summary_info',
|
|
'State labels for the current DM root operator summary.',
|
|
'gauge',
|
|
1,
|
|
{
|
|
summary_state: summaryState || 'unknown',
|
|
health_state: healthState || 'unknown',
|
|
monitor_state: monitorState || 'unknown',
|
|
witness_state: witnessState || 'unknown',
|
|
transparency_state: transparencyState || 'unknown',
|
|
},
|
|
);
|
|
if (nextAction) {
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_next_action_info',
|
|
'Suggested next DM root operator action.',
|
|
'gauge',
|
|
1,
|
|
{ action: nextAction },
|
|
);
|
|
}
|
|
if (errorDetail) {
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_health_scrape_error_info',
|
|
'Reason for the most recent DM root health scrape failure.',
|
|
'gauge',
|
|
1,
|
|
{ reason: errorDetail },
|
|
);
|
|
}
|
|
for (const alert of alerts) {
|
|
const code = String(alert?.code || '').trim();
|
|
if (!code) continue;
|
|
appendMetric(
|
|
lines,
|
|
'shadowbroker_dm_root_alert_active',
|
|
'Active DM root health alerts.',
|
|
'gauge',
|
|
1,
|
|
{
|
|
code,
|
|
severity: String(alert?.severity || '').trim().toLowerCase() || 'unknown',
|
|
target: String(alert?.target || '').trim().toLowerCase() || 'dm_root',
|
|
blocking: boolGauge(Boolean(alert?.blocking)).toString(),
|
|
},
|
|
);
|
|
}
|
|
return `${lines.join('\n')}\n`;
|
|
}
|
|
|
|
async function fetchHealth(config) {
|
|
const headers = { Accept: 'application/json' };
|
|
const authHeader = parseHeader(config.authHeader);
|
|
if (authHeader) {
|
|
headers[authHeader[0]] = authHeader[1];
|
|
}
|
|
if (config.authCookie) {
|
|
headers.Cookie = config.authCookie;
|
|
}
|
|
const controller = new AbortController();
|
|
const timeout = globalThis.setTimeout(() => controller.abort(), config.timeoutMs);
|
|
try {
|
|
const response = await fetch(config.url, {
|
|
method: 'GET',
|
|
headers,
|
|
signal: controller.signal,
|
|
});
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok || payload?.ok === false) {
|
|
const detail = String(payload?.detail || payload?.message || `http_${response.status}`).trim();
|
|
throw new Error(detail || 'dm_root_health_failed');
|
|
}
|
|
return payload;
|
|
} finally {
|
|
globalThis.clearTimeout(timeout);
|
|
}
|
|
}
|
|
|
|
async function writeMetrics(outputPath, text) {
|
|
const resolved = path.resolve(outputPath);
|
|
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
const tempPath = `${resolved}.tmp`;
|
|
await fs.writeFile(tempPath, text, 'utf8');
|
|
await fs.rename(tempPath, resolved);
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help) {
|
|
console.log(HELP_TEXT);
|
|
return;
|
|
}
|
|
|
|
const config = {
|
|
url: normalizeUrl(args.baseUrl || process.env.SB_DM_ROOT_BASE_URL, args.healthPath || process.env.SB_DM_ROOT_HEALTH_PATH),
|
|
authHeader: process.env.SB_DM_ROOT_AUTH_HEADER || '',
|
|
authCookie: process.env.SB_DM_ROOT_AUTH_COOKIE || '',
|
|
timeoutMs: Math.max(1000, safeInt(process.env.SB_DM_ROOT_TIMEOUT_MS, 10000)),
|
|
output: String(args.output || process.env.SB_DM_ROOT_PROMETHEUS_OUTPUT || '').trim(),
|
|
stdout: Boolean(args.stdout),
|
|
};
|
|
|
|
let metricsText = '';
|
|
let exitCode = 0;
|
|
try {
|
|
const payload = await fetchHealth(config);
|
|
metricsText = buildMetrics(payload);
|
|
} catch (error) {
|
|
const detail = String(error?.message || 'dm_root_health_fetch_failed').trim() || 'dm_root_health_fetch_failed';
|
|
metricsText = buildMetrics(
|
|
{
|
|
checked_at: Math.floor(Date.now() / 1000),
|
|
state: 'stale_external',
|
|
health_state: 'error',
|
|
monitoring: { state: 'critical' },
|
|
strong_trust_blocked: true,
|
|
requires_attention: true,
|
|
alert_count: 1,
|
|
blocking_alert_count: 1,
|
|
warning_alert_count: 0,
|
|
alerts: [
|
|
{
|
|
code: 'dm_root_health_scrape_failed',
|
|
severity: 'error',
|
|
target: 'dm_root',
|
|
blocking: true,
|
|
},
|
|
],
|
|
witness: {},
|
|
transparency: {},
|
|
},
|
|
detail,
|
|
);
|
|
exitCode = 2;
|
|
}
|
|
|
|
if (config.output) {
|
|
await writeMetrics(config.output, metricsText);
|
|
}
|
|
if (config.stdout || !config.output) {
|
|
process.stdout.write(metricsText);
|
|
}
|
|
if (exitCode) {
|
|
process.exit(exitCode);
|
|
}
|
|
}
|
|
|
|
await main();
|