@nocville/npc 0.2.2 → 0.3.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/README.md +55 -17
- package/dist/cjs/cli.js +94 -39
- package/dist/cjs/core/base-npc.js +15 -0
- package/dist/cjs/core/config.js +19 -2
- package/dist/cjs/core/errors.js +23 -0
- package/dist/cjs/core/http.js +121 -0
- package/dist/cjs/core/pixel-art.js +110 -0
- package/dist/cjs/core/polling-npc.js +99 -14
- package/dist/cjs/core/text.js +13 -0
- package/dist/cjs/core/thresholds.js +29 -0
- package/dist/cjs/index.js +21 -2
- package/dist/cjs/npcs/agent-common/session.js +1 -0
- package/dist/cjs/npcs/agent-common/shim.js +8 -2
- package/dist/cjs/npcs/github-watcher/defaults.js +7 -1
- package/dist/cjs/npcs/github-watcher/github-api.js +82 -45
- package/dist/cjs/npcs/github-watcher/index.js +82 -36
- package/dist/cjs/npcs/internet-connectivity/checks.js +70 -67
- package/dist/cjs/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/cjs/npcs/internet-connectivity/index.js +134 -131
- package/dist/cjs/npcs/log-watcher/buffer.js +61 -0
- package/dist/cjs/npcs/log-watcher/index.js +109 -103
- package/dist/cjs/npcs/log-watcher/tail.js +100 -53
- package/dist/cjs/npcs/office-characters/index.js +92 -0
- package/dist/cjs/npcs/process-watcher/index.js +60 -51
- package/dist/cjs/npcs/process-watcher/process-scanner.js +100 -68
- package/dist/cjs/npcs/receptionist/defaults.js +4 -0
- package/dist/cjs/npcs/receptionist/index.js +6 -93
- package/dist/cjs/npcs/receptionist/weather.js +42 -47
- package/dist/cjs/npcs/security/index.js +26 -0
- package/dist/cjs/npcs/system-monitor/collectors.js +66 -17
- package/dist/cjs/npcs/system-monitor/index.js +52 -28
- package/dist/cjs/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/cjs/npcs/web-page-watcher/capture.js +23 -1
- package/dist/cjs/npcs/web-page-watcher/defaults.js +44 -2
- package/dist/cjs/npcs/web-page-watcher/index.js +119 -68
- package/dist/cjs/npcs/web-page-watcher/pixelate.js +2 -1
- package/dist/cli.js +95 -40
- package/dist/core/base-npc.d.ts +5 -0
- package/dist/core/base-npc.js +15 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +18 -2
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +18 -0
- package/dist/core/http.d.ts +52 -0
- package/dist/core/http.js +111 -0
- package/dist/core/pixel-art.d.ts +38 -0
- package/dist/core/pixel-art.js +105 -0
- package/dist/core/polling-npc.d.ts +22 -1
- package/dist/core/polling-npc.js +98 -13
- package/dist/core/text.d.ts +6 -0
- package/dist/core/text.js +12 -0
- package/dist/core/thresholds.d.ts +9 -1
- package/dist/core/thresholds.js +27 -0
- package/dist/core/types.d.ts +5 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +6 -2
- package/dist/npcs/agent-common/session.js +1 -0
- package/dist/npcs/agent-common/shim.js +8 -2
- package/dist/npcs/github-watcher/defaults.d.ts +6 -0
- package/dist/npcs/github-watcher/defaults.js +7 -1
- package/dist/npcs/github-watcher/github-api.d.ts +21 -7
- package/dist/npcs/github-watcher/github-api.js +80 -45
- package/dist/npcs/github-watcher/index.d.ts +6 -0
- package/dist/npcs/github-watcher/index.js +83 -37
- package/dist/npcs/internet-connectivity/checks.d.ts +15 -18
- package/dist/npcs/internet-connectivity/checks.js +70 -67
- package/dist/npcs/internet-connectivity/defaults.d.ts +3 -0
- package/dist/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/npcs/internet-connectivity/index.d.ts +19 -6
- package/dist/npcs/internet-connectivity/index.js +135 -133
- package/dist/npcs/log-watcher/buffer.d.ts +22 -0
- package/dist/npcs/log-watcher/buffer.js +58 -0
- package/dist/npcs/log-watcher/index.d.ts +22 -21
- package/dist/npcs/log-watcher/index.js +112 -106
- package/dist/npcs/log-watcher/tail.d.ts +23 -3
- package/dist/npcs/log-watcher/tail.js +101 -54
- package/dist/npcs/office-characters/index.d.ts +50 -0
- package/dist/npcs/office-characters/index.js +82 -0
- package/dist/npcs/process-watcher/index.d.ts +1 -1
- package/dist/npcs/process-watcher/index.js +61 -52
- package/dist/npcs/process-watcher/process-scanner.d.ts +31 -1
- package/dist/npcs/process-watcher/process-scanner.js +97 -70
- package/dist/npcs/receptionist/defaults.d.ts +4 -0
- package/dist/npcs/receptionist/defaults.js +4 -0
- package/dist/npcs/receptionist/index.d.ts +3 -12
- package/dist/npcs/receptionist/index.js +6 -93
- package/dist/npcs/receptionist/weather.d.ts +8 -6
- package/dist/npcs/receptionist/weather.js +39 -45
- package/dist/npcs/security/index.d.ts +12 -0
- package/dist/npcs/security/index.js +22 -0
- package/dist/npcs/system-monitor/collectors.d.ts +15 -3
- package/dist/npcs/system-monitor/collectors.js +64 -17
- package/dist/npcs/system-monitor/defaults.d.ts +1 -1
- package/dist/npcs/system-monitor/index.d.ts +2 -1
- package/dist/npcs/system-monitor/index.js +54 -30
- package/dist/npcs/web-page-watcher/basic-mode.d.ts +2 -2
- package/dist/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/npcs/web-page-watcher/capture.d.ts +18 -0
- package/dist/npcs/web-page-watcher/capture.js +21 -1
- package/dist/npcs/web-page-watcher/defaults.d.ts +23 -0
- package/dist/npcs/web-page-watcher/defaults.js +41 -1
- package/dist/npcs/web-page-watcher/index.d.ts +18 -1
- package/dist/npcs/web-page-watcher/index.js +121 -70
- package/dist/npcs/web-page-watcher/pixelate.js +2 -1
- package/package.json +2 -2
|
@@ -1,84 +1,87 @@
|
|
|
1
1
|
import { promises as dns } from 'node:dns';
|
|
2
2
|
import net from 'node:net';
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { NPC_USER_AGENT, discardBody, displayUrl } from '../../core/http.js';
|
|
4
|
+
function errorText(err) {
|
|
5
|
+
return err instanceof Error ? err.message : String(err);
|
|
5
6
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
export async function checkHttp(urls, timeout) {
|
|
22
|
-
return Promise.all(urls.map((url) => fetchSingleUrl(url, timeout)));
|
|
23
|
-
}
|
|
24
|
-
async function fetchSingleUrl(url, timeout) {
|
|
25
|
-
const controller = new AbortController();
|
|
26
|
-
const timer = setTimeout(() => controller.abort(), timeout);
|
|
27
|
-
const start = performance.now();
|
|
7
|
+
// ---------- DNS ----------
|
|
8
|
+
/**
|
|
9
|
+
* Resolve each host's A records through a dedicated resolver with a bounded
|
|
10
|
+
* timeout (the default resolver retries for tens of seconds when DNS is down).
|
|
11
|
+
*/
|
|
12
|
+
export async function checkDns(hosts, timeoutMs) {
|
|
13
|
+
const resolver = new dns.Resolver({
|
|
14
|
+
timeout: Math.max(250, Math.floor(timeoutMs / 2)),
|
|
15
|
+
tries: 2,
|
|
16
|
+
});
|
|
17
|
+
let timer;
|
|
18
|
+
const deadline = new Promise((resolve) => {
|
|
19
|
+
timer = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
20
|
+
});
|
|
28
21
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
url,
|
|
42
|
-
ok: false,
|
|
43
|
-
timeMs: performance.now() - start,
|
|
44
|
-
error: err instanceof Error ? err.message : String(err),
|
|
45
|
-
};
|
|
22
|
+
return await Promise.all(hosts.map(async (host) => {
|
|
23
|
+
const start = performance.now();
|
|
24
|
+
try {
|
|
25
|
+
const outcome = await Promise.race([resolver.resolve4(host), deadline]);
|
|
26
|
+
if (outcome === 'timeout')
|
|
27
|
+
return { target: host, ok: false, timeMs: 0, error: 'timeout' };
|
|
28
|
+
return { target: host, ok: true, timeMs: performance.now() - start };
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
return { target: host, ok: false, timeMs: 0, error: errorText(err) };
|
|
32
|
+
}
|
|
33
|
+
}));
|
|
46
34
|
}
|
|
47
35
|
finally {
|
|
48
36
|
clearTimeout(timer);
|
|
37
|
+
resolver.cancel();
|
|
49
38
|
}
|
|
50
39
|
}
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
// ---------- HTTP ----------
|
|
41
|
+
/**
|
|
42
|
+
* Time to response headers for each URL. The body is discarded unread, so a
|
|
43
|
+
* large or endless response costs nothing. Only 2xx counts as reachable.
|
|
44
|
+
*/
|
|
45
|
+
export async function checkHttp(urls, timeoutMs) {
|
|
46
|
+
return Promise.all(urls.map(async (url) => {
|
|
47
|
+
const target = displayUrl(url); // never echo credentials or query tokens
|
|
48
|
+
const start = performance.now();
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
52
|
+
headers: { 'User-Agent': NPC_USER_AGENT },
|
|
53
|
+
});
|
|
54
|
+
const timeMs = performance.now() - start;
|
|
55
|
+
await discardBody(res);
|
|
56
|
+
return res.ok
|
|
57
|
+
? { target, ok: true, timeMs }
|
|
58
|
+
: { target, ok: false, timeMs: 0, error: `HTTP ${res.status}` };
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return { target, ok: false, timeMs: 0, error: errorText(err) };
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
// ---------- Latency (TCP connect) ----------
|
|
66
|
+
/** Time to establish a TCP connection to each host:port. */
|
|
67
|
+
export async function checkLatency(hosts, timeoutMs) {
|
|
68
|
+
return Promise.all(hosts.map((h) => measureTcpConnect(h, timeoutMs)));
|
|
53
69
|
}
|
|
54
|
-
function measureTcpConnect(target) {
|
|
70
|
+
function measureTcpConnect(target, timeoutMs) {
|
|
71
|
+
const label = target.host.includes(':')
|
|
72
|
+
? `[${target.host}]:${target.port}`
|
|
73
|
+
: `${target.host}:${target.port}`;
|
|
55
74
|
return new Promise((resolve) => {
|
|
56
75
|
const start = performance.now();
|
|
57
76
|
const socket = net.connect({ host: target.host, port: target.port });
|
|
58
|
-
const
|
|
77
|
+
const finish = (result) => {
|
|
78
|
+
clearTimeout(timer);
|
|
59
79
|
socket.destroy();
|
|
60
|
-
resolve(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
});
|
|
66
|
-
}, 5000);
|
|
67
|
-
socket.on('connect', () => {
|
|
68
|
-
clearTimeout(timeout);
|
|
69
|
-
const timeMs = performance.now() - start;
|
|
70
|
-
socket.destroy();
|
|
71
|
-
resolve({ host: target.host, ok: true, timeMs });
|
|
72
|
-
});
|
|
73
|
-
socket.on('error', (err) => {
|
|
74
|
-
clearTimeout(timeout);
|
|
75
|
-
resolve({
|
|
76
|
-
host: target.host,
|
|
77
|
-
ok: false,
|
|
78
|
-
timeMs: performance.now() - start,
|
|
79
|
-
error: err.message,
|
|
80
|
-
});
|
|
81
|
-
});
|
|
80
|
+
resolve(result);
|
|
81
|
+
};
|
|
82
|
+
const timer = setTimeout(() => finish({ target: label, ok: false, timeMs: 0, error: 'Connection timed out' }), timeoutMs);
|
|
83
|
+
socket.once('connect', () => finish({ target: label, ok: true, timeMs: performance.now() - start }));
|
|
84
|
+
socket.once('error', (err) => finish({ target: label, ok: false, timeMs: 0, error: err.message }));
|
|
82
85
|
});
|
|
83
86
|
}
|
|
84
87
|
//# sourceMappingURL=checks.js.map
|
|
@@ -8,7 +8,10 @@ export interface InternetConnectivityDefaults {
|
|
|
8
8
|
dnsHosts: string[];
|
|
9
9
|
httpUrls: string[];
|
|
10
10
|
latencyHosts: LatencyHost[];
|
|
11
|
+
/** Port for a `--latency-hosts` entry given without one. */
|
|
12
|
+
latencyPort: number;
|
|
11
13
|
slowThreshold: number;
|
|
14
|
+
/** Per-check timeout (DNS, HTTP and TCP) in ms. */
|
|
12
15
|
httpTimeout: number;
|
|
13
16
|
thresholds: {
|
|
14
17
|
dnsTime: ThresholdConfig;
|
|
@@ -4,10 +4,13 @@ export const DEFAULTS = {
|
|
|
4
4
|
// gstatic generate_204 is a stable, low-latency 204 endpoint used by many
|
|
5
5
|
// OSes for connectivity checks (replaces the flaky httpbin.org default).
|
|
6
6
|
httpUrls: ['https://1.1.1.1/cdn-cgi/trace', 'https://www.gstatic.com/generate_204'],
|
|
7
|
+
// 443, not 80: Google's 8.8.8.8 doesn't listen on port 80, so that default
|
|
8
|
+
// timed out on every poll and pinned the NPC at warning.
|
|
7
9
|
latencyHosts: [
|
|
8
|
-
{ host: '8.8.8.8', port:
|
|
9
|
-
{ host: '1.1.1.1', port:
|
|
10
|
+
{ host: '8.8.8.8', port: 443 },
|
|
11
|
+
{ host: '1.1.1.1', port: 443 },
|
|
10
12
|
],
|
|
13
|
+
latencyPort: 80,
|
|
11
14
|
slowThreshold: 1000, // ms
|
|
12
15
|
httpTimeout: 5000, // ms
|
|
13
16
|
thresholds: {
|
|
@@ -10,17 +10,20 @@ export interface ConnectivityThresholds {
|
|
|
10
10
|
export interface InternetConnectivityConfig extends PollingNpcConfig {
|
|
11
11
|
dnsHosts?: string[];
|
|
12
12
|
httpUrls?: string[];
|
|
13
|
+
/** `{host, port}` pairs, or "host" / "host:port" / "[v6]:port" strings. */
|
|
13
14
|
latencyHosts?: LatencyHost[] | string[];
|
|
14
15
|
slowThreshold?: number;
|
|
16
|
+
/** Per-check timeout in ms (DNS, HTTP and TCP). */
|
|
15
17
|
httpTimeout?: number;
|
|
16
18
|
thresholds?: ConnectivityThresholds;
|
|
17
19
|
}
|
|
18
20
|
interface ConnectivitySample {
|
|
19
21
|
allFailed: boolean;
|
|
20
22
|
anyFailed: boolean;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
/** Mean time of the successful checks of each kind; null when none succeeded. */
|
|
24
|
+
dnsAvg: number | null;
|
|
25
|
+
httpAvg: number | null;
|
|
26
|
+
latencyAvg: number | null;
|
|
24
27
|
}
|
|
25
28
|
/**
|
|
26
29
|
* Derive overall status from live per-check thresholds. All checks failing is
|
|
@@ -28,6 +31,11 @@ interface ConnectivitySample {
|
|
|
28
31
|
* escalate per their own thresholds.
|
|
29
32
|
*/
|
|
30
33
|
export declare function evaluateConnectivityStatus(sample: ConnectivitySample, thresholds: ConnectivityThresholds): Status;
|
|
34
|
+
/**
|
|
35
|
+
* Parse a latency target: "host", "host:port", "[ipv6]:port" or a bare IPv6
|
|
36
|
+
* address. Throws NpcConfigError on an out-of-range port.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseLatencyHost(input: string, defaultPort?: number): LatencyHost;
|
|
31
39
|
export declare class InternetConnectivityNPC extends PollingNPC {
|
|
32
40
|
private dnsHosts;
|
|
33
41
|
private httpUrls;
|
|
@@ -44,10 +52,15 @@ export declare class InternetConnectivityNPC extends PollingNPC {
|
|
|
44
52
|
get behavior(): BehaviorHints;
|
|
45
53
|
get companion(): CompanionDecl;
|
|
46
54
|
protected collect(): Promise<CollectedMetrics>;
|
|
47
|
-
private buildOnlineMessage;
|
|
48
|
-
private buildOfflineMessage;
|
|
49
55
|
/**
|
|
50
|
-
*
|
|
56
|
+
* One dashboard panel. A category whose checks all failed shows FAIL in red
|
|
57
|
+
* (and a 0 sparkline sample) rather than a misleading latency; a partial
|
|
58
|
+
* failure is amber.
|
|
59
|
+
*/
|
|
60
|
+
private panel;
|
|
61
|
+
/**
|
|
62
|
+
* Build config from CLI flags. List flags are comma-separated; latency
|
|
63
|
+
* hosts accept "host", "host:port" or "[ipv6]:port".
|
|
51
64
|
*/
|
|
52
65
|
static configFromFlags(flags: Record<string, string | boolean>): Partial<InternetConnectivityConfig>;
|
|
53
66
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
-
import { evaluateThreshold, worstStatus } from '../../core/thresholds.js';
|
|
2
|
+
import { evaluateThreshold, worstStatus, moodForStatus } from '../../core/thresholds.js';
|
|
3
3
|
import { statusColor, pushHistory } from '../../core/screen.js';
|
|
4
|
-
import {
|
|
4
|
+
import { listFlag, numberFlag } from '../../core/config.js';
|
|
5
|
+
import { NpcConfigError } from '../../core/errors.js';
|
|
6
|
+
import { isHttpUrl } from '../../core/http.js';
|
|
5
7
|
import { checkDns, checkHttp, checkLatency } from './checks.js';
|
|
6
8
|
import { DEFAULTS } from './defaults.js';
|
|
7
9
|
/**
|
|
@@ -12,13 +14,15 @@ import { DEFAULTS } from './defaults.js';
|
|
|
12
14
|
export function evaluateConnectivityStatus(sample, thresholds) {
|
|
13
15
|
if (sample.allFailed)
|
|
14
16
|
return 'critical';
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
evaluateThreshold(sample.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
const statuses = [sample.anyFailed ? 'warning' : 'healthy'];
|
|
18
|
+
if (sample.dnsAvg !== null)
|
|
19
|
+
statuses.push(evaluateThreshold(sample.dnsAvg, thresholds.dnsTime));
|
|
20
|
+
if (sample.httpAvg !== null)
|
|
21
|
+
statuses.push(evaluateThreshold(sample.httpAvg, thresholds.httpTime));
|
|
22
|
+
if (sample.latencyAvg !== null) {
|
|
23
|
+
statuses.push(evaluateThreshold(sample.latencyAvg, thresholds.latencyTime));
|
|
24
|
+
}
|
|
25
|
+
return worstStatus(statuses);
|
|
22
26
|
}
|
|
23
27
|
/** Scale a threshold set by a ratio, preserving direction. */
|
|
24
28
|
function scaleThresholds(base, ratio) {
|
|
@@ -33,6 +37,30 @@ function scaleThresholds(base, ratio) {
|
|
|
33
37
|
latencyTime: scale(base.latencyTime),
|
|
34
38
|
};
|
|
35
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse a latency target: "host", "host:port", "[ipv6]:port" or a bare IPv6
|
|
42
|
+
* address. Throws NpcConfigError on an out-of-range port.
|
|
43
|
+
*/
|
|
44
|
+
export function parseLatencyHost(input, defaultPort = DEFAULTS.latencyPort) {
|
|
45
|
+
const s = input.trim();
|
|
46
|
+
const toPort = (raw) => {
|
|
47
|
+
const port = Number(raw);
|
|
48
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
49
|
+
throw new NpcConfigError(`Invalid port in latency host "${s}"`);
|
|
50
|
+
}
|
|
51
|
+
return port;
|
|
52
|
+
};
|
|
53
|
+
const bracketed = s.match(/^\[([^\]]+)\](?::(\d*))?$/);
|
|
54
|
+
if (bracketed) {
|
|
55
|
+
return { host: bracketed[1], port: bracketed[2] ? toPort(bracketed[2]) : defaultPort };
|
|
56
|
+
}
|
|
57
|
+
const colons = s.split(':').length - 1;
|
|
58
|
+
if (colons === 1) {
|
|
59
|
+
const [host, port] = s.split(':');
|
|
60
|
+
return { host, port: toPort(port) };
|
|
61
|
+
}
|
|
62
|
+
return { host: s, port: defaultPort }; // plain host, or a bare IPv6 address
|
|
63
|
+
}
|
|
36
64
|
export class InternetConnectivityNPC extends PollingNPC {
|
|
37
65
|
dnsHosts;
|
|
38
66
|
httpUrls;
|
|
@@ -49,10 +77,16 @@ export class InternetConnectivityNPC extends PollingNPC {
|
|
|
49
77
|
});
|
|
50
78
|
this.dnsHosts = config.dnsHosts ?? DEFAULTS.dnsHosts;
|
|
51
79
|
this.httpUrls = config.httpUrls ?? DEFAULTS.httpUrls;
|
|
52
|
-
this.latencyHosts = config.latencyHosts
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
80
|
+
this.latencyHosts = (config.latencyHosts ?? DEFAULTS.latencyHosts).map((h) => typeof h === 'string' ? parseLatencyHost(h) : h);
|
|
81
|
+
// Every check must finish well inside the poll interval.
|
|
82
|
+
this.httpTimeout = Math.min(Math.max(500, config.httpTimeout ?? DEFAULTS.httpTimeout), this.pollInterval / 2);
|
|
83
|
+
const badUrl = this.httpUrls.find((u) => !isHttpUrl(u));
|
|
84
|
+
if (badUrl !== undefined) {
|
|
85
|
+
throw new NpcConfigError(`--http-urls: "${badUrl}" is not an http(s) URL`);
|
|
86
|
+
}
|
|
87
|
+
if (this.dnsHosts.length === 0 && this.httpUrls.length === 0) {
|
|
88
|
+
throw new NpcConfigError('internet-connectivity needs at least one DNS host or HTTP URL');
|
|
89
|
+
}
|
|
56
90
|
// Threshold resolution: explicit thresholds win; else if the legacy
|
|
57
91
|
// --slow-threshold differs from the default, scale the live thresholds by
|
|
58
92
|
// that ratio for back-compat; else use the defaults.
|
|
@@ -88,115 +122,91 @@ export class InternetConnectivityNPC extends PollingNPC {
|
|
|
88
122
|
}
|
|
89
123
|
async collect() {
|
|
90
124
|
const [dnsResults, httpResults, latencyResults] = await Promise.all([
|
|
91
|
-
checkDns(this.dnsHosts),
|
|
125
|
+
checkDns(this.dnsHosts, this.httpTimeout),
|
|
92
126
|
checkHttp(this.httpUrls, this.httpTimeout),
|
|
93
|
-
checkLatency(this.latencyHosts),
|
|
127
|
+
checkLatency(this.latencyHosts, this.httpTimeout),
|
|
94
128
|
]);
|
|
95
|
-
const dnsOk = dnsResults
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const latencyAvg = avgTime(latencyResults);
|
|
129
|
+
const dnsOk = countOk(dnsResults);
|
|
130
|
+
const httpOk = countOk(httpResults);
|
|
131
|
+
const latencyOk = countOk(latencyResults);
|
|
132
|
+
const dnsAvg = avgOkTime(dnsResults);
|
|
133
|
+
const httpAvg = avgOkTime(httpResults);
|
|
134
|
+
const latencyAvg = avgOkTime(latencyResults);
|
|
102
135
|
const isOnline = dnsOk > 0 || httpOk > 0;
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const message = allFailed
|
|
110
|
-
? this.buildOfflineMessage(dnsResults, httpResults)
|
|
111
|
-
: this.buildOnlineMessage(dnsAvg, httpAvg, latencyAvg);
|
|
112
|
-
pushHistory(this.dnsHistory, round(dnsAvg));
|
|
113
|
-
pushHistory(this.httpHistory, round(httpAvg));
|
|
114
|
-
pushHistory(this.latencyHistory, round(latencyAvg));
|
|
115
|
-
const screenData = {
|
|
116
|
-
layout: 'multi-metric',
|
|
117
|
-
values: [
|
|
118
|
-
{
|
|
119
|
-
label: 'DNS',
|
|
120
|
-
value: `${round(dnsAvg)}ms`,
|
|
121
|
-
color: statusColor(evaluateThreshold(dnsAvg, this.thresholds.dnsTime)),
|
|
122
|
-
sparkline: [...this.dnsHistory],
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
label: 'HTTP',
|
|
126
|
-
value: `${round(httpAvg)}ms`,
|
|
127
|
-
color: statusColor(evaluateThreshold(httpAvg, this.thresholds.httpTime)),
|
|
128
|
-
sparkline: [...this.httpHistory],
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
label: 'TCP',
|
|
132
|
-
value: `${round(latencyAvg)}ms`,
|
|
133
|
-
color: statusColor(evaluateThreshold(latencyAvg, this.thresholds.latencyTime)),
|
|
134
|
-
sparkline: [...this.latencyHistory],
|
|
135
|
-
},
|
|
136
|
-
{ label: 'NET', value: isOnline ? 'ONLINE' : 'OFFLINE', color: isOnline ? 'green' : 'red' },
|
|
137
|
-
],
|
|
138
|
-
};
|
|
136
|
+
const anyFailed = dnsOk < dnsResults.length || httpOk < httpResults.length || latencyOk < latencyResults.length;
|
|
137
|
+
const status = evaluateConnectivityStatus({ allFailed: !isOnline, anyFailed, dnsAvg, httpAvg, latencyAvg }, this.thresholds);
|
|
138
|
+
const message = isOnline
|
|
139
|
+
? `Online | DNS: ${fmtMs(dnsAvg)} | HTTP: ${fmtMs(httpAvg)} | Latency: ${fmtMs(latencyAvg)}` +
|
|
140
|
+
(anyFailed ? ` | ${failureSummary(dnsResults, httpResults, latencyResults)}` : '')
|
|
141
|
+
: offlineMessage(dnsResults, httpResults);
|
|
139
142
|
return {
|
|
140
143
|
status,
|
|
141
|
-
mood,
|
|
144
|
+
mood: moodForStatus(status),
|
|
142
145
|
message,
|
|
143
146
|
subStatus: isOnline ? undefined : 'offline',
|
|
144
|
-
screenData
|
|
147
|
+
screenData: {
|
|
148
|
+
layout: 'multi-metric',
|
|
149
|
+
values: [
|
|
150
|
+
this.panel('DNS', dnsResults, dnsAvg, this.dnsHistory, this.thresholds.dnsTime),
|
|
151
|
+
this.panel('HTTP', httpResults, httpAvg, this.httpHistory, this.thresholds.httpTime),
|
|
152
|
+
this.panel('TCP', latencyResults, latencyAvg, this.latencyHistory, this.thresholds.latencyTime),
|
|
153
|
+
{
|
|
154
|
+
label: 'NET',
|
|
155
|
+
value: isOnline ? 'ONLINE' : 'OFFLINE',
|
|
156
|
+
color: isOnline ? 'green' : 'red',
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
145
160
|
metrics: {
|
|
146
|
-
dns_avg_ms: round(dnsAvg),
|
|
147
|
-
http_avg_ms: round(httpAvg),
|
|
148
|
-
latency_avg_ms: round(latencyAvg),
|
|
161
|
+
dns_avg_ms: round(dnsAvg ?? 0),
|
|
162
|
+
http_avg_ms: round(httpAvg ?? 0),
|
|
163
|
+
latency_avg_ms: round(latencyAvg ?? 0),
|
|
149
164
|
dns_ok_count: dnsOk,
|
|
150
|
-
dns_fail_count:
|
|
165
|
+
dns_fail_count: dnsResults.length - dnsOk,
|
|
151
166
|
http_ok_count: httpOk,
|
|
152
|
-
http_fail_count:
|
|
167
|
+
http_fail_count: httpResults.length - httpOk,
|
|
168
|
+
latency_ok_count: latencyOk,
|
|
169
|
+
latency_fail_count: latencyResults.length - latencyOk,
|
|
153
170
|
is_online: isOnline ? 1 : 0,
|
|
154
171
|
},
|
|
155
172
|
};
|
|
156
173
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
174
|
+
/**
|
|
175
|
+
* One dashboard panel. A category whose checks all failed shows FAIL in red
|
|
176
|
+
* (and a 0 sparkline sample) rather than a misleading latency; a partial
|
|
177
|
+
* failure is amber.
|
|
178
|
+
*/
|
|
179
|
+
panel(label, results, avg, history, threshold) {
|
|
180
|
+
if (results.length === 0)
|
|
181
|
+
return { label, value: 'off', color: 'gray' };
|
|
182
|
+
pushHistory(history, round(avg ?? 0));
|
|
183
|
+
const sparkline = [...history];
|
|
184
|
+
if (avg === null)
|
|
185
|
+
return { label, value: 'FAIL', color: 'red', sparkline };
|
|
186
|
+
const color = countOk(results) < results.length ? 'amber' : statusColor(evaluateThreshold(avg, threshold));
|
|
187
|
+
return { label, value: fmtMs(avg), color, sparkline };
|
|
169
188
|
}
|
|
170
189
|
/**
|
|
171
|
-
* Build config from CLI flags.
|
|
190
|
+
* Build config from CLI flags. List flags are comma-separated; latency
|
|
191
|
+
* hosts accept "host", "host:port" or "[ipv6]:port".
|
|
172
192
|
*/
|
|
173
193
|
static configFromFlags(flags) {
|
|
174
194
|
const config = {};
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
177
|
-
config.dnsHosts =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (latencyHostsRaw) {
|
|
185
|
-
config.latencyHosts = latencyHostsRaw.split(',').map((s) => {
|
|
186
|
-
const trimmed = s.trim();
|
|
187
|
-
const colonIndex = trimmed.lastIndexOf(':');
|
|
188
|
-
if (colonIndex !== -1) {
|
|
189
|
-
const host = trimmed.slice(0, colonIndex);
|
|
190
|
-
const port = Number(trimmed.slice(colonIndex + 1));
|
|
191
|
-
if (!Number.isNaN(port))
|
|
192
|
-
return { host, port };
|
|
193
|
-
}
|
|
194
|
-
return { host: trimmed, port: 80 };
|
|
195
|
-
});
|
|
196
|
-
}
|
|
195
|
+
const dnsHosts = listFlag(flags, 'dns-hosts');
|
|
196
|
+
if (dnsHosts)
|
|
197
|
+
config.dnsHosts = dnsHosts;
|
|
198
|
+
const httpUrls = listFlag(flags, 'http-urls');
|
|
199
|
+
if (httpUrls)
|
|
200
|
+
config.httpUrls = httpUrls;
|
|
201
|
+
const latencyHosts = listFlag(flags, 'latency-hosts');
|
|
202
|
+
if (latencyHosts)
|
|
203
|
+
config.latencyHosts = latencyHosts.map((h) => parseLatencyHost(h));
|
|
197
204
|
const slowThreshold = numberFlag(flags, 'slow-threshold');
|
|
198
205
|
if (slowThreshold !== undefined)
|
|
199
206
|
config.slowThreshold = slowThreshold;
|
|
207
|
+
const timeout = numberFlag(flags, 'timeout');
|
|
208
|
+
if (timeout !== undefined)
|
|
209
|
+
config.httpTimeout = timeout;
|
|
200
210
|
const pollInterval = numberFlag(flags, 'poll-interval');
|
|
201
211
|
if (pollInterval !== undefined)
|
|
202
212
|
config.pollInterval = pollInterval;
|
|
@@ -204,43 +214,35 @@ export class InternetConnectivityNPC extends PollingNPC {
|
|
|
204
214
|
}
|
|
205
215
|
}
|
|
206
216
|
// ---------- Helpers ----------
|
|
207
|
-
function
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
function countOk(results) {
|
|
218
|
+
return results.filter((r) => r.ok).length;
|
|
219
|
+
}
|
|
220
|
+
/** Mean time of the successful checks only — a 5s timeout is a failure, not a latency. */
|
|
221
|
+
function avgOkTime(results) {
|
|
222
|
+
const ok = results.filter((r) => r.ok);
|
|
223
|
+
if (ok.length === 0)
|
|
224
|
+
return null;
|
|
225
|
+
return ok.reduce((acc, r) => acc + r.timeMs, 0) / ok.length;
|
|
212
226
|
}
|
|
213
227
|
function round(n) {
|
|
214
228
|
return Math.round(n * 100) / 100;
|
|
215
229
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
case 'healthy':
|
|
219
|
-
return 'happy';
|
|
220
|
-
case 'warning':
|
|
221
|
-
return 'concerned';
|
|
222
|
-
case 'critical':
|
|
223
|
-
return 'stressed';
|
|
224
|
-
default:
|
|
225
|
-
return 'idle';
|
|
226
|
-
}
|
|
230
|
+
function fmtMs(n) {
|
|
231
|
+
return n === null ? 'FAIL' : `${Math.round(n)}ms`;
|
|
227
232
|
}
|
|
228
|
-
/**
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
return { host: item, port: 80 };
|
|
244
|
-
});
|
|
233
|
+
/** "failing: 8.8.8.8:443, dns.example" — names the targets, never error text. */
|
|
234
|
+
function failureSummary(...groups) {
|
|
235
|
+
const failed = groups.flat().filter((r) => !r.ok);
|
|
236
|
+
return `failing: ${failed.map((r) => r.target).join(', ')}`;
|
|
237
|
+
}
|
|
238
|
+
function offlineMessage(dnsResults, httpResults) {
|
|
239
|
+
const problems = [];
|
|
240
|
+
if (dnsResults.length > 0 && dnsResults.every((r) => !r.ok)) {
|
|
241
|
+
problems.push('DNS resolution failed');
|
|
242
|
+
}
|
|
243
|
+
if (httpResults.length > 0 && httpResults.every((r) => !r.ok)) {
|
|
244
|
+
problems.push('no HTTP connectivity');
|
|
245
|
+
}
|
|
246
|
+
return `OFFLINE: ${problems.join(', ') || 'all checks failed'}`;
|
|
245
247
|
}
|
|
246
248
|
//# sourceMappingURL=index.js.map
|
|
@@ -6,6 +6,13 @@ export interface RecentLine {
|
|
|
6
6
|
}
|
|
7
7
|
/** Classify a raw log line against the error/warning patterns. */
|
|
8
8
|
export declare function classifyLine(line: string, errorRe: RegExp, warningRe: RegExp): LogLineLevel;
|
|
9
|
+
/**
|
|
10
|
+
* Prepare a log line for display to the whole team: strip terminal escapes,
|
|
11
|
+
* redact anything secret-looking, drop control characters and truncate.
|
|
12
|
+
* Log lines routinely contain tokens and passwords; only redacted text ever
|
|
13
|
+
* leaves the machine.
|
|
14
|
+
*/
|
|
15
|
+
export declare function cleanLogLine(line: string, max?: number): string;
|
|
9
16
|
/**
|
|
10
17
|
* Push a line into the bounded ring buffer: each entry is capped at `cap`
|
|
11
18
|
* characters and the buffer keeps only the last `max` entries.
|
|
@@ -15,4 +22,19 @@ export declare function pushRecent(buffer: RecentLine[], line: string, level: Lo
|
|
|
15
22
|
export declare function buildLogScreen(buffer: RecentLine[]): ScreenData;
|
|
16
23
|
/** True when enough time has elapsed since the last burst push to emit again. */
|
|
17
24
|
export declare function shouldEmitBurst(lastEmit: number, now: number, windowMs?: number): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Sliding-window error/warning counter in one-second buckets: memory is
|
|
27
|
+
* bounded by the window length, not by how many lines a runaway logger writes.
|
|
28
|
+
*/
|
|
29
|
+
export declare class WindowCounter {
|
|
30
|
+
private readonly windowMs;
|
|
31
|
+
private buckets;
|
|
32
|
+
constructor(windowMs: number);
|
|
33
|
+
add(level: 'error' | 'warning', now?: number): void;
|
|
34
|
+
counts(now?: number): {
|
|
35
|
+
errors: number;
|
|
36
|
+
warnings: number;
|
|
37
|
+
};
|
|
38
|
+
private prune;
|
|
39
|
+
}
|
|
18
40
|
//# sourceMappingURL=buffer.d.ts.map
|