@nocville/npc 0.2.1 → 0.2.3
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/LICENSE +21 -0
- package/README.md +58 -120
- package/dist/cjs/cli.js +55 -34
- package/dist/cjs/core/base-npc.js +8 -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/session-filter.js +3 -1
- package/dist/cjs/core/text.js +13 -0
- package/dist/cjs/core/thresholds.js +29 -0
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/npcs/agent-common/session.js +1 -0
- 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/process-watcher/index.js +61 -51
- package/dist/cjs/npcs/process-watcher/process-scanner.js +63 -69
- package/dist/cjs/npcs/receptionist/defaults.js +4 -0
- package/dist/cjs/npcs/receptionist/index.js +45 -19
- package/dist/cjs/npcs/receptionist/weather.js +42 -47
- 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 +56 -35
- package/dist/core/base-npc.d.ts +5 -0
- package/dist/core/base-npc.js +8 -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/session-filter.d.ts +2 -2
- package/dist/core/session-filter.js +3 -1
- 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/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/npcs/agent-common/session.js +1 -0
- 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/process-watcher/index.d.ts +1 -1
- package/dist/npcs/process-watcher/index.js +62 -52
- package/dist/npcs/process-watcher/process-scanner.d.ts +29 -1
- package/dist/npcs/process-watcher/process-scanner.js +61 -71
- package/dist/npcs/receptionist/defaults.d.ts +4 -0
- package/dist/npcs/receptionist/defaults.js +4 -0
- package/dist/npcs/receptionist/index.d.ts +12 -2
- package/dist/npcs/receptionist/index.js +46 -20
- package/dist/npcs/receptionist/weather.d.ts +8 -6
- package/dist/npcs/receptionist/weather.js +39 -45
- 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 -7
|
@@ -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
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sanitizeText } from '../../core/text.js';
|
|
2
|
+
import { redactSecrets } from '../agent-common/activity.js';
|
|
1
3
|
/** Classify a raw log line against the error/warning patterns. */
|
|
2
4
|
export function classifyLine(line, errorRe, warningRe) {
|
|
3
5
|
if (errorRe.test(line))
|
|
@@ -6,6 +8,23 @@ export function classifyLine(line, errorRe, warningRe) {
|
|
|
6
8
|
return 'warning';
|
|
7
9
|
return 'info';
|
|
8
10
|
}
|
|
11
|
+
// ANSI CSI/OSC escape sequences (colours, cursor moves, hyperlinks).
|
|
12
|
+
// eslint-disable-next-line no-control-regex
|
|
13
|
+
const ANSI = /\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007\u001b]*(?:\u0007|\u001b\\))/g;
|
|
14
|
+
// `password=…`, `"token": "…"`, `Authorization: …` style pairs common in logs
|
|
15
|
+
// (redactSecrets covers CLI flags, bearer tokens, URL credentials and keys).
|
|
16
|
+
const SECRET_PAIR = /\b([\w-]*(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|authorization|cookie|session)[\w-]*)(["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;&]+)/gi;
|
|
17
|
+
/**
|
|
18
|
+
* Prepare a log line for display to the whole team: strip terminal escapes,
|
|
19
|
+
* redact anything secret-looking, drop control characters and truncate.
|
|
20
|
+
* Log lines routinely contain tokens and passwords; only redacted text ever
|
|
21
|
+
* leaves the machine.
|
|
22
|
+
*/
|
|
23
|
+
export function cleanLogLine(line, max = 200) {
|
|
24
|
+
const plain = line.replace(ANSI, '');
|
|
25
|
+
const redacted = redactSecrets(plain).replace(SECRET_PAIR, (_m, key, sep) => `${key}${sep}***`);
|
|
26
|
+
return sanitizeText(redacted, max);
|
|
27
|
+
}
|
|
9
28
|
/**
|
|
10
29
|
* Push a line into the bounded ring buffer: each entry is capped at `cap`
|
|
11
30
|
* characters and the buffer keeps only the last `max` entries.
|
|
@@ -31,4 +50,43 @@ export function buildLogScreen(buffer) {
|
|
|
31
50
|
export function shouldEmitBurst(lastEmit, now, windowMs = 5000) {
|
|
32
51
|
return now - lastEmit >= windowMs;
|
|
33
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Sliding-window error/warning counter in one-second buckets: memory is
|
|
55
|
+
* bounded by the window length, not by how many lines a runaway logger writes.
|
|
56
|
+
*/
|
|
57
|
+
export class WindowCounter {
|
|
58
|
+
windowMs;
|
|
59
|
+
buckets = new Map();
|
|
60
|
+
constructor(windowMs) {
|
|
61
|
+
this.windowMs = windowMs;
|
|
62
|
+
}
|
|
63
|
+
add(level, now = Date.now()) {
|
|
64
|
+
const key = Math.floor(now / 1000);
|
|
65
|
+
const bucket = this.buckets.get(key) ?? { errors: 0, warnings: 0 };
|
|
66
|
+
if (level === 'error')
|
|
67
|
+
bucket.errors++;
|
|
68
|
+
else
|
|
69
|
+
bucket.warnings++;
|
|
70
|
+
this.buckets.set(key, bucket);
|
|
71
|
+
if (this.buckets.size > this.windowMs / 1000 + 2)
|
|
72
|
+
this.prune(now);
|
|
73
|
+
}
|
|
74
|
+
counts(now = Date.now()) {
|
|
75
|
+
this.prune(now);
|
|
76
|
+
let errors = 0;
|
|
77
|
+
let warnings = 0;
|
|
78
|
+
for (const b of this.buckets.values()) {
|
|
79
|
+
errors += b.errors;
|
|
80
|
+
warnings += b.warnings;
|
|
81
|
+
}
|
|
82
|
+
return { errors, warnings };
|
|
83
|
+
}
|
|
84
|
+
prune(now) {
|
|
85
|
+
const oldest = Math.floor((now - this.windowMs) / 1000);
|
|
86
|
+
for (const key of this.buckets.keys()) {
|
|
87
|
+
if (key < oldest)
|
|
88
|
+
this.buckets.delete(key);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
34
92
|
//# sourceMappingURL=buffer.js.map
|
|
@@ -1,45 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
import type { PollingNpcConfig } from '../../core/polling-npc.js';
|
|
3
|
+
import type { CollectedMetrics, Archetype, BehaviorHints, CompanionDecl } from '../../core/types.js';
|
|
4
|
+
export interface LogWatcherConfig extends PollingNpcConfig {
|
|
4
5
|
files: string[];
|
|
5
6
|
errorPattern?: string;
|
|
6
7
|
warningPattern?: string;
|
|
7
8
|
windowSeconds?: number;
|
|
8
9
|
errorWarn?: number;
|
|
9
10
|
errorCrit?: number;
|
|
11
|
+
/** Report interval in ms (alias of pollInterval, kept for compatibility). */
|
|
10
12
|
reportInterval?: number;
|
|
11
13
|
}
|
|
12
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Tails log files and reports error bursts. A periodic report (every
|
|
16
|
+
* `reportInterval`) carries the error count over the sliding window and the
|
|
17
|
+
* last lines for the log terminal; a fresh error line also triggers an
|
|
18
|
+
* immediate report, debounced to one per 5s, so a burst shows up at once.
|
|
19
|
+
* Every report is one composed push, never a push per line.
|
|
20
|
+
*/
|
|
21
|
+
export declare class LogWatcherNPC extends PollingNPC {
|
|
13
22
|
private tailers;
|
|
14
|
-
private
|
|
15
|
-
private entries;
|
|
23
|
+
private counter;
|
|
16
24
|
private recentLines;
|
|
17
|
-
private
|
|
25
|
+
private lastError;
|
|
26
|
+
/** The error line that triggered an immediate report, consumed by the next collect(). */
|
|
27
|
+
private burstLine;
|
|
28
|
+
private lastBurstAt;
|
|
18
29
|
private errorRegex;
|
|
19
30
|
private warningRegex;
|
|
20
31
|
private windowMs;
|
|
21
|
-
private
|
|
22
|
-
private errorCrit;
|
|
23
|
-
private reportInterval;
|
|
32
|
+
private errorThreshold;
|
|
24
33
|
private files;
|
|
25
34
|
constructor(config: LogWatcherConfig);
|
|
26
35
|
get name(): string;
|
|
27
36
|
protected get defaultDisplayName(): string;
|
|
28
37
|
get archetype(): Archetype;
|
|
29
|
-
get heartbeatSeconds(): number;
|
|
30
38
|
get behavior(): BehaviorHints;
|
|
31
39
|
get companion(): CompanionDecl;
|
|
32
40
|
protected onStart(): Promise<void>;
|
|
33
41
|
protected onStop(): Promise<void>;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
private pruneWindow;
|
|
38
|
-
private report;
|
|
39
|
-
/**
|
|
40
|
-
* Build and push the current status. If `overrideMessage` is set (burst push),
|
|
41
|
-
* it drives the bubble and the status floors at warning.
|
|
42
|
-
*/
|
|
43
|
-
private pushReport;
|
|
42
|
+
/** Visible for tests: feed one raw line as if a tailer had read it. */
|
|
43
|
+
protected handleLine(line: string, now?: number): void;
|
|
44
|
+
protected collect(): Promise<CollectedMetrics>;
|
|
44
45
|
}
|
|
45
46
|
//# sourceMappingURL=index.d.ts.map
|