next-observer 0.3.22 → 0.3.24

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 CHANGED
@@ -142,12 +142,61 @@ export default [
142
142
  Tools: `get_services`, `get_operation_stats`, `compare_versions`, `get_errors`, `search_traces`, `get_trace`.
143
143
  `next-observer` validates the file and lists the loaded specialists on start.
144
144
 
145
+ ## Scheduled checks
146
+
147
+ Put `observe.checks.ts` (or `.mts`, `.js`, `.mjs`) in the app root to have the observer ask the app on a timer: is it
148
+ up, is it fast enough, does it still refuse what it must refuse. The file is plain data — no imports:
149
+
150
+ ```ts
151
+ export default [
152
+ { name: 'catalog answers fast', url: '/api/products', expect: { status: 200, maxMs: 300 } },
153
+ { name: 'home page shows the shop', url: '/', expect: { bodyIncludes: 'Porto Shop' } },
154
+ { name: 'guests cannot see orders', url: '/api/orders', expect: { status: [401, 403] } },
155
+ { name: 'checkout refuses a bad quantity', url: '/api/checkout', method: 'POST', body: { quantity: 99 }, expect: { status: 400 } },
156
+ ]
157
+ ```
158
+
159
+ | Field | Default | |
160
+ |---|---|---|
161
+ | `name` | — | unique in the file |
162
+ | `url` | — | a path (sent to the app) or a full `http(s)` URL |
163
+ | `method`, `headers`, `body` | `GET` | a `body` that is not a string is sent as JSON |
164
+ | `everySeconds` | `60` | at least 5 |
165
+ | `timeoutMs` | `10000` | no answer within it is a failure |
166
+ | `expect.status` | any 2xx | one status or a list; redirects are not followed — a 302 is an answer |
167
+ | `expect.maxMs` | — | the whole answer, body included |
168
+ | `expect.bodyIncludes` | — | text the body must contain |
169
+
170
+ Paths go to `OBSERVE_APP_URL`; without it, to the host and port `next dev` was given (`-H`, `-p`, `PORT`), else
171
+ `http://localhost:3000`. That is a guess: when the port is busy, `next dev` moves to the next free one — compare the
172
+ address in the observer's banner with the one Next prints, and set `OBSERVE_APP_URL` if they differ.
173
+
174
+ - Every request carries the header `x-observe-check` (the check's name, URL-encoded) and a trace id of its own, so each
175
+ result links to the trace the app recorded for it.
176
+ - These requests are not traffic for the anomaly detector: an expected refusal is not an error, and a quiet app stays quiet.
177
+ - While the app is still starting (it has not answered yet, first minute), a refused connection is not a failure — the
178
+ check is tried again every 5 s.
179
+ - A body is read only when `maxMs` or `bodyIncludes` asks about it, and then its first megabyte; an event stream is
180
+ never read. So the status of a streaming route can be checked.
181
+ - A field that is misspelled, or an expectation that can never hold (a body text on `HEAD`), is an error on start.
182
+
183
+ Results: `GET /api/checks` — the last 50 per check and how many failed in a row.
184
+
185
+ **When a check keeps failing, the AI agents look into it on their own** — the same way as with an anomaly in real
186
+ traffic: it appears in the chat, and the investigation starts from the trace of the failing request.
187
+
188
+ - twice in a row → `critical` (it is down now);
189
+ - 3 of the last 10 runs → `warning` (it is unreliable — a check failing every third time never fails twice in a row);
190
+ - one failure is not an anomaly: the first answer after a restart is often just slow;
191
+ - the same check is reported once in 5 minutes; `OBSERVE_DETECTOR=off` switches this off too.
192
+
145
193
  ## Configuration
146
194
 
147
195
  | Variable | Default | |
148
196
  |---|---|---|
149
197
  | `OBSERVE_AI` | `mock` | `mock` \| `real` |
150
198
  | `OBSERVE_DETECTOR` | on | `off` disables the anomaly detector |
199
+ | `OBSERVE_APP_URL` | `http://localhost:3000` | where the checks from `observe.checks.ts` are sent |
151
200
  | `OBSERVE_MODEL_TIMEOUT_MS` | `90000` | how long one model call may take before it is started again |
152
201
  | `OBSERVE_API_KEY` | — | require `x-api-key` on ingest (`/v1/traces`) |
153
202
  | `OBSERVE_UI_PASSWORD` | — | require a password for the UI, the query API and the chat (browser login, any user name) |
@@ -0,0 +1,91 @@
1
+ import type { Check } from './spec.js';
2
+ export interface CheckResult {
3
+ atMs: number;
4
+ ok: boolean;
5
+ durationMs: number;
6
+ /** Absent when no answer came at all. */
7
+ status?: number;
8
+ /** The trace id sent with the request. */
9
+ traceId: string;
10
+ /** Why it failed, in words; absent when ok. */
11
+ reason?: string;
12
+ }
13
+ export interface CheckStatus {
14
+ name: string;
15
+ method: string;
16
+ /** Where the request really goes. */
17
+ url: string;
18
+ everySeconds: number;
19
+ expect: Check['expect'];
20
+ /** Failed runs in a row up to now; 0 when the last run passed or nothing ran yet. */
21
+ failures: number;
22
+ last?: CheckResult;
23
+ /** Oldest first. */
24
+ history: CheckResult[];
25
+ }
26
+ export interface CheckRunnerOptions {
27
+ checks: Check[];
28
+ /** Where checks with a path go, e.g. http://localhost:3000. */
29
+ baseUrl: string;
30
+ fetch?: typeof fetch;
31
+ /** Wall clock, for `atMs`. */
32
+ now?: () => number;
33
+ /** A clock that never jumps (NTP, sleep), for durations. Default performance.now. */
34
+ elapsed?: () => number;
35
+ /** Results kept per check. Default 50. */
36
+ history?: number;
37
+ /** The first run of every check waits this long (the app may still be starting). Default 3 s. */
38
+ firstDelayMs?: number;
39
+ /**
40
+ * While the app has not answered yet, a refused connection to it within this time of start() is not a result:
41
+ * `next dev` is still starting. After it, "nobody is listening" is exactly what a check is for. Default 60 s.
42
+ */
43
+ startupGraceMs?: number;
44
+ /** A run that met the starting app is tried again this soon, whatever the check's interval. Default 5 s. */
45
+ startupRetryMs?: number;
46
+ onResult?: (check: Check, result: CheckResult, failures: number) => void;
47
+ }
48
+ /** Marks the request as the observer's own (the value is the check's name, URL-encoded: header values are ASCII). */
49
+ export declare const CHECK_HEADER = "x-observe-check";
50
+ /** As much of a body as is read: enough for `bodyIncludes`, and a large download is not buffered every minute. */
51
+ export declare const MAX_BODY_BYTES = 1000000;
52
+ export declare class CheckRunner {
53
+ private readonly options;
54
+ readonly baseUrl: string;
55
+ private readonly checks;
56
+ private readonly fetch;
57
+ private readonly now;
58
+ private readonly elapsed;
59
+ private readonly keep;
60
+ private readonly firstDelayMs;
61
+ private readonly startupGraceMs;
62
+ private readonly startupRetryMs;
63
+ private readonly results;
64
+ /** Failed runs in a row per check — counted, not derived from the kept history, which is cut at `keep`. */
65
+ private readonly streak;
66
+ private readonly traces;
67
+ private running;
68
+ private inFlight;
69
+ private timers;
70
+ /** Changes on every start()/stop(): a run from an earlier one must not report into this one. */
71
+ private generation;
72
+ /** On the `elapsed` clock: a wall clock stepping back would keep the grace time open for ever. */
73
+ private startedAt;
74
+ /** The app itself (a check with a path) has answered since start(). */
75
+ private appAnswered;
76
+ constructor(options: CheckRunnerOptions);
77
+ /** Was this trace started by one of our own requests? Their spans are not real traffic. */
78
+ isCheckTrace(traceId: string): boolean;
79
+ urlOf(check: Check): string;
80
+ /**
81
+ * One request, judged. Never throws: a failed request is a result. Resolves with `undefined` when there is nothing
82
+ * to report — the runner was stopped meanwhile, or the app has not started listening yet (see startupGraceMs).
83
+ */
84
+ run(check: Check): Promise<CheckResult | undefined>;
85
+ private attempt;
86
+ /** Starts the timers. A check whose previous run is still waiting for its answer is skipped, not stacked. */
87
+ start(): void;
88
+ /** Stops the timers and drops the requests on their way: nothing is recorded or reported after this. */
89
+ stop(): void;
90
+ list(): CheckStatus[];
91
+ }
@@ -0,0 +1,239 @@
1
+ // Runs the checks from observe.checks.ts on their timers and keeps the latest results. Each request carries a trace
2
+ // id of its own (traceparent), so a result links to the trace the app recorded for exactly that request.
3
+ import { randomBytes } from 'node:crypto';
4
+ /** Marks the request as the observer's own (the value is the check's name, URL-encoded: header values are ASCII). */
5
+ export const CHECK_HEADER = 'x-observe-check';
6
+ /** As much of a body as is read: enough for `bodyIncludes`, and a large download is not buffered every minute. */
7
+ export const MAX_BODY_BYTES = 1_000_000;
8
+ /** Trace ids of recent check requests that are remembered, to tell their spans from real traffic. */
9
+ const REMEMBERED_TRACES = 5000;
10
+ const STREAM = /text\/event-stream/i;
11
+ /**
12
+ * The body as text, up to MAX_BODY_BYTES (`cut` says whether more was left). Not read at all when nobody asked about
13
+ * it (`wanted` false) or when it is an event stream, which never ends.
14
+ */
15
+ async function readBody(response, wanted) {
16
+ if (!response.body)
17
+ return { text: '', cut: false, read: true };
18
+ if (!wanted || STREAM.test(response.headers.get('content-type') ?? '')) {
19
+ await response.body.cancel().catch(() => { });
20
+ return { text: '', cut: false, read: false };
21
+ }
22
+ const reader = response.body.getReader();
23
+ const chunks = [];
24
+ let size = 0;
25
+ let cut = false;
26
+ for (;;) {
27
+ const { done, value } = await reader.read();
28
+ if (done)
29
+ break;
30
+ chunks.push(value);
31
+ size += value.byteLength;
32
+ if (size >= MAX_BODY_BYTES) {
33
+ cut = true;
34
+ await reader.cancel().catch(() => { });
35
+ break;
36
+ }
37
+ }
38
+ return { text: Buffer.concat(chunks).subarray(0, MAX_BODY_BYTES).toString('utf8'), cut, read: true };
39
+ }
40
+ /** What run() gives the timer when the app is not listening yet: try again soon. */
41
+ const STARTING = Symbol('starting');
42
+ export class CheckRunner {
43
+ options;
44
+ baseUrl;
45
+ checks;
46
+ fetch;
47
+ now;
48
+ elapsed;
49
+ keep;
50
+ firstDelayMs;
51
+ startupGraceMs;
52
+ startupRetryMs;
53
+ results = new Map();
54
+ /** Failed runs in a row per check — counted, not derived from the kept history, which is cut at `keep`. */
55
+ streak = new Map();
56
+ traces = new Set();
57
+ running = new Set();
58
+ inFlight = new Set();
59
+ timers = [];
60
+ /** Changes on every start()/stop(): a run from an earlier one must not report into this one. */
61
+ generation = 0;
62
+ /** On the `elapsed` clock: a wall clock stepping back would keep the grace time open for ever. */
63
+ startedAt = null;
64
+ /** The app itself (a check with a path) has answered since start(). */
65
+ appAnswered = false;
66
+ constructor(options) {
67
+ this.options = options;
68
+ this.checks = options.checks;
69
+ this.baseUrl = options.baseUrl.replace(/\/+$/, '');
70
+ this.fetch = options.fetch ?? globalThis.fetch;
71
+ this.now = options.now ?? Date.now;
72
+ this.elapsed = options.elapsed ?? (() => performance.now());
73
+ this.keep = Math.max(1, options.history ?? 50);
74
+ this.firstDelayMs = options.firstDelayMs ?? 3000;
75
+ this.startupGraceMs = options.startupGraceMs ?? 60_000;
76
+ this.startupRetryMs = options.startupRetryMs ?? 5000;
77
+ }
78
+ /** Was this trace started by one of our own requests? Their spans are not real traffic. */
79
+ isCheckTrace(traceId) {
80
+ return this.traces.has(traceId);
81
+ }
82
+ urlOf(check) {
83
+ return check.url.startsWith('/') ? `${this.baseUrl}${check.url}` : check.url;
84
+ }
85
+ /**
86
+ * One request, judged. Never throws: a failed request is a result. Resolves with `undefined` when there is nothing
87
+ * to report — the runner was stopped meanwhile, or the app has not started listening yet (see startupGraceMs).
88
+ */
89
+ async run(check) {
90
+ const result = await this.attempt(check);
91
+ return result === STARTING ? undefined : result;
92
+ }
93
+ async attempt(check) {
94
+ const own = check.url.startsWith('/');
95
+ const generation = this.generation;
96
+ const traceId = randomBytes(16).toString('hex');
97
+ this.traces.add(traceId);
98
+ if (this.traces.size > REMEMBERED_TRACES)
99
+ this.traces.delete(this.traces.values().next().value);
100
+ const atMs = this.now();
101
+ const started = this.elapsed();
102
+ const controller = new AbortController();
103
+ this.inFlight.add(controller);
104
+ let timedOut = false;
105
+ const timer = setTimeout(() => {
106
+ timedOut = true;
107
+ controller.abort();
108
+ }, check.timeoutMs);
109
+ let result;
110
+ try {
111
+ const response = await this.fetch(this.urlOf(check), {
112
+ method: check.method,
113
+ // Ours last: a check cannot switch off its own trace link or mark.
114
+ headers: { ...check.headers, traceparent: `00-${traceId}-${randomBytes(8).toString('hex')}-01`, [CHECK_HEADER]: encodeURIComponent(check.name) },
115
+ body: check.body,
116
+ // A redirect is an answer ("anonymous visitors are sent to /login"), not something to follow.
117
+ redirect: 'manual',
118
+ signal: controller.signal,
119
+ });
120
+ if (own)
121
+ this.appAnswered = true;
122
+ const e = check.expect;
123
+ // The status is known from here on: a body that never finishes must not turn it into "no answer".
124
+ let body;
125
+ try {
126
+ body = await readBody(response, e.bodyIncludes !== undefined || e.maxMs !== undefined);
127
+ }
128
+ catch (error) {
129
+ if (!timedOut)
130
+ throw error;
131
+ }
132
+ const durationMs = this.elapsed() - started;
133
+ const reasons = [];
134
+ if (e.status ? !e.status.includes(response.status) : response.status < 200 || response.status > 299) {
135
+ reasons.push(`expected status ${e.status ? e.status.join(' or ') : '2xx'}, got ${response.status}`);
136
+ }
137
+ if (e.maxMs !== undefined && durationMs > e.maxMs)
138
+ reasons.push(`took ${Math.round(durationMs)} ms, limit ${e.maxMs} ms`);
139
+ if (!body)
140
+ reasons.push(`the body did not finish within ${check.timeoutMs} ms`);
141
+ else if (e.bodyIncludes !== undefined && !body.text.includes(e.bodyIncludes)) {
142
+ const where = !body.read ? ' (an event stream is not read)' : body.cut ? ' in its first megabyte (the rest is not read)' : '';
143
+ reasons.push(`body does not contain ${JSON.stringify(e.bodyIncludes)}${where}`);
144
+ }
145
+ result = { atMs, ok: reasons.length === 0, durationMs, status: response.status, traceId, ...(reasons.length ? { reason: reasons.join('; ') } : {}) };
146
+ }
147
+ catch (error) {
148
+ const durationMs = this.elapsed() - started;
149
+ const cause = error?.cause;
150
+ // Only the app's own checks, only "nobody is listening": a wrong host name or a certificate error is news at once.
151
+ const starting = own && cause?.code === 'ECONNREFUSED' && !this.appAnswered && this.startedAt !== null && started - this.startedAt < this.startupGraceMs;
152
+ if (starting)
153
+ return generation === this.generation ? STARTING : undefined;
154
+ const reason = timedOut ? `no answer within ${check.timeoutMs} ms` : `request failed: ${cause?.code ?? cause?.message ?? error?.message ?? String(error)}`;
155
+ result = { atMs, ok: false, durationMs, traceId, reason };
156
+ }
157
+ finally {
158
+ clearTimeout(timer);
159
+ this.inFlight.delete(controller);
160
+ }
161
+ if (generation !== this.generation)
162
+ return undefined;
163
+ const list = this.results.get(check.name) ?? [];
164
+ list.push(result);
165
+ if (list.length > this.keep)
166
+ list.splice(0, list.length - this.keep);
167
+ this.results.set(check.name, list);
168
+ const failures = result.ok ? 0 : (this.streak.get(check.name) ?? 0) + 1;
169
+ this.streak.set(check.name, failures);
170
+ try {
171
+ this.options.onResult?.(check, result, failures);
172
+ }
173
+ catch {
174
+ // A listener's problem must not stop the checks.
175
+ }
176
+ return result;
177
+ }
178
+ /** Starts the timers. A check whose previous run is still waiting for its answer is skipped, not stacked. */
179
+ start() {
180
+ this.stop();
181
+ this.startedAt = this.elapsed();
182
+ this.appAnswered = false;
183
+ const generation = this.generation;
184
+ // This start's own set: a run left over from an earlier start must not hold back the first tick of this one.
185
+ const running = this.running;
186
+ for (const check of this.checks) {
187
+ let retryPending = false;
188
+ const tick = () => {
189
+ if (running.has(check.name))
190
+ return;
191
+ running.add(check.name);
192
+ void this.attempt(check)
193
+ .then((result) => {
194
+ // The app is not up yet: do not make a check with a long interval wait a whole interval for its first
195
+ // result. One retry at a time — the interval's own ticks must not each start a chain of them.
196
+ if (result !== STARTING || generation !== this.generation || retryPending)
197
+ return;
198
+ retryPending = true;
199
+ const retry = setTimeout(() => {
200
+ retryPending = false;
201
+ tick();
202
+ }, this.startupRetryMs);
203
+ retry.unref?.();
204
+ this.timers.push(retry);
205
+ })
206
+ .finally(() => running.delete(check.name));
207
+ };
208
+ const first = setTimeout(() => {
209
+ tick();
210
+ const every = setInterval(tick, check.everyMs);
211
+ every.unref?.();
212
+ this.timers.push(every);
213
+ }, Math.min(this.firstDelayMs, check.everyMs));
214
+ first.unref?.();
215
+ this.timers.push(first);
216
+ }
217
+ }
218
+ /** Stops the timers and drops the requests on their way: nothing is recorded or reported after this. */
219
+ stop() {
220
+ this.generation++;
221
+ // clearTimeout also clears an interval.
222
+ for (const timer of this.timers)
223
+ clearTimeout(timer);
224
+ this.timers = [];
225
+ for (const controller of this.inFlight)
226
+ controller.abort();
227
+ this.inFlight = new Set();
228
+ this.running = new Set();
229
+ this.startedAt = null;
230
+ }
231
+ list() {
232
+ return this.checks.map((check) => {
233
+ const history = this.results.get(check.name) ?? [];
234
+ const last = history.at(-1);
235
+ return { name: check.name, method: check.method, url: this.urlOf(check), everySeconds: check.everyMs / 1000, expect: check.expect, failures: this.streak.get(check.name) ?? 0, ...(last ? { last } : {}), history: [...history] };
236
+ });
237
+ }
238
+ }
239
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/checks/runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ export interface CheckExpect {
2
+ /** The status (or any of several) that counts as good. Default: any 2xx. */
3
+ status?: number | number[];
4
+ /** The answer, body included (its first megabyte), must arrive within this many ms. */
5
+ maxMs?: number;
6
+ /** Text the body must contain. */
7
+ bodyIncludes?: string;
8
+ }
9
+ export interface CheckSpec {
10
+ /** Shown in the UI and in anomalies; unique in the file. */
11
+ name: string;
12
+ /** A path ("/api/products") — sent to the app — or a full http(s) URL. */
13
+ url: string;
14
+ /** Default GET. */
15
+ method?: string;
16
+ headers?: Record<string, string>;
17
+ /** A string is sent as is; anything else as JSON. */
18
+ body?: unknown;
19
+ /** Default 60, at least 5. */
20
+ everySeconds?: number;
21
+ /** Default 10 000. */
22
+ timeoutMs?: number;
23
+ expect?: CheckExpect;
24
+ }
25
+ /** A validated check with the defaults filled in. */
26
+ export interface Check {
27
+ name: string;
28
+ url: string;
29
+ method: string;
30
+ headers: Record<string, string>;
31
+ body?: string;
32
+ everyMs: number;
33
+ timeoutMs: number;
34
+ expect: {
35
+ status?: number[];
36
+ maxMs?: number;
37
+ bodyIncludes?: string;
38
+ };
39
+ }
40
+ export declare const CHECK_DEFAULTS: {
41
+ everySeconds: number;
42
+ timeoutMs: number;
43
+ };
44
+ /** Throws with a message that names the check and the field. */
45
+ export declare function validateCheck(raw: unknown): Check;
46
+ /** Validates the whole file's export; names must be unique — results are kept by name. */
47
+ export declare function validateChecks(exported: unknown): Check[];
@@ -0,0 +1,117 @@
1
+ // Scheduled checks: the app's observe.checks.ts lists requests the observer sends on a timer and what a good answer
2
+ // looks like — is the app up, is it fast enough, does it still refuse what it must refuse. Plain data, no functions and
3
+ // no imports: the file is read by the observer, never by the app.
4
+ export const CHECK_DEFAULTS = { everySeconds: 60, timeoutMs: 10_000 };
5
+ const METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
6
+ const KEYS = ['name', 'url', 'method', 'headers', 'body', 'everySeconds', 'timeoutMs', 'expect'];
7
+ const EXPECT_KEYS = ['status', 'maxMs', 'bodyIncludes'];
8
+ /** Statuses whose answer never has a body. */
9
+ const NO_BODY = [204, 205, 304];
10
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
11
+ /** A key nobody reads is a silent typo ("expects", "maxMS") — the check would pass forever. */
12
+ function unknownKey(value, known) {
13
+ return Object.keys(value).find((key) => !known.includes(key));
14
+ }
15
+ function numberIn(value, min, max) {
16
+ return typeof value === 'number' && Number.isFinite(value) && value >= min && value <= max;
17
+ }
18
+ /** Throws with a message that names the check and the field. */
19
+ export function validateCheck(raw) {
20
+ if (!isRecord(raw))
21
+ throw new Error('a check must be an object like { name, url, expect }');
22
+ const name = raw.name;
23
+ if (typeof name !== 'string' || name.trim() === '' || name.length > 80)
24
+ throw new Error('a check needs a `name` (1–80 characters)');
25
+ const fail = (message) => {
26
+ throw new Error(`check "${name}": ${message}`);
27
+ };
28
+ const stray = unknownKey(raw, KEYS);
29
+ if (stray)
30
+ fail(`unknown field \`${stray}\` (known: ${KEYS.join(', ')})`);
31
+ const url = raw.url;
32
+ if (typeof url !== 'string' || !(url.startsWith('/') || /^https?:\/\//i.test(url)))
33
+ fail('`url` must be a path starting with "/" or a full http(s) URL');
34
+ if (typeof url === 'string' && !url.startsWith('/') && !URL.canParse(url))
35
+ fail(`\`url\` is not a valid URL: ${url}`);
36
+ const method = raw.method === undefined ? 'GET' : typeof raw.method === 'string' ? raw.method.toUpperCase() : '';
37
+ if (!METHODS.includes(method))
38
+ fail(`\`method\` must be one of ${METHODS.join(', ')}`);
39
+ const headers = {};
40
+ if (raw.headers !== undefined) {
41
+ if (!isRecord(raw.headers))
42
+ fail('`headers` must be an object of strings');
43
+ for (const [key, value] of Object.entries(raw.headers)) {
44
+ if (typeof value !== 'string')
45
+ fail(`header \`${key}\` must be a string`);
46
+ headers[key.toLowerCase()] = value;
47
+ }
48
+ }
49
+ let body;
50
+ if (raw.body !== undefined) {
51
+ if (method === 'GET' || method === 'HEAD')
52
+ fail(`a ${method} request cannot have a \`body\``);
53
+ if (typeof raw.body === 'string')
54
+ body = raw.body;
55
+ else {
56
+ // A function or a symbol gives undefined, a BigInt or a cycle throws — neither is a body anyone meant to send.
57
+ try {
58
+ body = JSON.stringify(raw.body);
59
+ }
60
+ catch {
61
+ body = undefined;
62
+ }
63
+ if (body === undefined)
64
+ fail('`body` cannot be sent as JSON — use plain data or a string');
65
+ headers['content-type'] ??= 'application/json';
66
+ }
67
+ }
68
+ const everySeconds = raw.everySeconds ?? CHECK_DEFAULTS.everySeconds;
69
+ if (!numberIn(everySeconds, 5, 86_400))
70
+ fail('`everySeconds` must be a number from 5 to 86400');
71
+ const timeoutMs = raw.timeoutMs ?? CHECK_DEFAULTS.timeoutMs;
72
+ if (!numberIn(timeoutMs, 100, 120_000))
73
+ fail('`timeoutMs` must be a number from 100 to 120000');
74
+ const expect = {};
75
+ if (raw.expect !== undefined) {
76
+ const e = raw.expect;
77
+ if (!isRecord(e))
78
+ return fail('`expect` must be an object like { status: 200, maxMs: 500 }');
79
+ const strayExpect = unknownKey(e, EXPECT_KEYS);
80
+ if (strayExpect)
81
+ fail(`unknown field \`expect.${strayExpect}\` (known: ${EXPECT_KEYS.join(', ')})`);
82
+ if (e.status !== undefined) {
83
+ const list = Array.isArray(e.status) ? e.status : [e.status];
84
+ if (list.length === 0 || !list.every((s) => Number.isInteger(s) && numberIn(s, 100, 599)))
85
+ fail('`expect.status` must be a status code (100–599) or a list of them');
86
+ expect.status = list;
87
+ }
88
+ if (e.maxMs !== undefined) {
89
+ if (!numberIn(e.maxMs, 1, 120_000))
90
+ fail('`expect.maxMs` must be a number from 1 to 120000');
91
+ expect.maxMs = e.maxMs;
92
+ }
93
+ if (e.bodyIncludes !== undefined) {
94
+ if (typeof e.bodyIncludes !== 'string' || e.bodyIncludes === '')
95
+ fail('`expect.bodyIncludes` must be a non-empty string');
96
+ // An answer without a body can never contain it: the check would fail forever.
97
+ if (method === 'HEAD')
98
+ fail('`expect.bodyIncludes` cannot be used with HEAD — the answer has no body');
99
+ if (expect.status?.every((s) => NO_BODY.includes(s)))
100
+ fail(`\`expect.bodyIncludes\` cannot be used with status ${expect.status.join(' or ')} — the answer has no body`);
101
+ expect.bodyIncludes = e.bodyIncludes;
102
+ }
103
+ }
104
+ return { name, url: url, method, headers, ...(body === undefined ? {} : { body }), everyMs: everySeconds * 1000, timeoutMs: timeoutMs, expect };
105
+ }
106
+ /** Validates the whole file's export; names must be unique — results are kept by name. */
107
+ export function validateChecks(exported) {
108
+ if (!Array.isArray(exported))
109
+ throw new Error('export default an array of checks: [{ name, url, expect }]');
110
+ const checks = exported.map(validateCheck);
111
+ const names = checks.map((c) => c.name);
112
+ const duplicate = names.find((name, i) => names.indexOf(name) !== i);
113
+ if (duplicate)
114
+ throw new Error(`check "${duplicate}" is defined twice`);
115
+ return checks;
116
+ }
117
+ //# sourceMappingURL=spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec.js","sourceRoot":"","sources":["../../src/checks/spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { Anomaly } from '../debug/detector.js';
2
+ import type { CheckResult } from './runner.js';
3
+ import type { Check } from './spec.js';
4
+ export interface CheckWatchOptions {
5
+ /** Failed runs in a row that make an anomaly. Default 2. */
6
+ failuresInRow?: number;
7
+ /** How many of the latest runs the share rule looks at. Default 10. */
8
+ shareWindow?: number;
9
+ /** Failed runs among them that make an anomaly — a check failing every third time never fails twice in a row. Default 3. */
10
+ shareFailures?: number;
11
+ /** The same check is not reported again within this time. Default 5 min, like the detector. */
12
+ cooldownMs?: number;
13
+ }
14
+ export declare class CheckWatch {
15
+ readonly options: Required<CheckWatchOptions>;
16
+ private readonly recent;
17
+ private readonly lastReportedMs;
18
+ private pending;
19
+ private seq;
20
+ constructor(options?: CheckWatchOptions);
21
+ /** The runner's `onResult`. */
22
+ observe: (check: Check, result: CheckResult, failures: number, url?: string) => void;
23
+ /** New anomalies since the last call. */
24
+ take(): Anomaly[];
25
+ }