68 lines
2.4 KiB
JavaScript
68 lines
2.4 KiB
JavaScript
// Three-valued observability primitives.
|
|||
|
|
//
|
||
|
|
// Every probe answer is one of THREE states, never two:
|
||
|
|
// "ok" — the probe saw the thing and it is present/working
|
||
|
|
// "absent" — the probe SAW the surface and the thing is genuinely missing
|
||
|
|
// "unobservable" — the probe COULD NOT SEE (dependency down, timeout, auth
|
||
|
|
// failure, endpoint missing). Never collapsed into ok/absent.
|
||
|
|
//
|
||
|
|
// Every result carries measurement_surface (exactly what was measured) and
|
||
|
|
// observed_at (ISO timestamp).
|
||
|
|
|
||
|
|
export const OK = "ok";
|
||
|
|
export const ABSENT = "absent";
|
||
|
|
export const UNOBSERVABLE = "unobservable";
|
||
|
|
|
||
|
|
export function probeResult(status, measurementSurface, extra = {}) {
|
||
|
|
return {
|
||
|
|
status,
|
||
|
|
measurement_surface: measurementSurface,
|
||
|
|
observed_at: new Date().toISOString(),
|
||
|
|
...extra,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Perform an HTTP GET probe of `url`. Returns a three-valued probeResult:
|
||
|
|
* - ok when the response status passes `okWhen` (default: 2xx)
|
||
|
|
* - unobservable on network error, timeout, or non-passing HTTP status
|
||
|
|
* (a 401/404/500 from the surface means we could not observe the truth,
|
||
|
|
* not that the dependency is healthy or absent).
|
||
|
|
* `classify(response, bodyText)` may optionally downgrade an HTTP-ok response
|
||
|
|
* to ABSENT (e.g. endpoint reachable but required model not installed).
|
||
|
|
*/
|
||
|
|
export async function httpProbe(url, { fetchImpl = fetch, timeoutMs = 2500, classify } = {}) {
|
||
|
|
const started = Date.now();
|
||
|
|
let res;
|
||
|
|
try {
|
||
|
|
res = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||
|
|
} catch (err) {
|
||
|
|
return probeResult(UNOBSERVABLE, url, {
|
||
|
|
reason: `fetch failed: ${err?.cause?.code || err?.name || "error"}: ${err?.message}`,
|
||
|
|
latency_ms: Date.now() - started,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const latency = Date.now() - started;
|
||
|
|
if (!res.ok) {
|
||
|
|
return probeResult(UNOBSERVABLE, url, {
|
||
|
|
reason: `HTTP ${res.status} from probe surface`,
|
||
|
|
http_status: res.status,
|
||
|
|
latency_ms: latency,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
if (classify) {
|
||
|
|
let bodyText = "";
|
||
|
|
try {
|
||
|
|
bodyText = await res.text();
|
||
|
|
} catch {
|
||
|
|
return probeResult(UNOBSERVABLE, url, {
|
||
|
|
reason: "response body unreadable",
|
||
|
|
latency_ms: latency,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const classified = classify(res, bodyText);
|
||
|
|
if (classified) return probeResult(classified.status, url, { latency_ms: latency, ...classified.extra });
|
||
|
|
}
|
||
|
|
return probeResult(OK, url, { http_status: res.status, latency_ms: latency });
|
||
|
|
}
|