perf(web): share service health snapshots
This commit is contained in:
parent
9aa30b07db
commit
5e66367687
2 changed files with 180 additions and 10 deletions
|
|
@ -3,7 +3,7 @@ import {
|
|||
DASHBOARD_SCHEMA_VERSION,
|
||||
type DashboardDocument,
|
||||
} from "@dimensionlab/dashboard-model";
|
||||
import { resolveDashboardDatasources } from ".";
|
||||
import { resolveDashboardDatasources, resolveDashboardTile } from ".";
|
||||
|
||||
describe("dashboard datasource resolution", () => {
|
||||
test("hydrates telemetry, service health, weather, and summary data from live adapters", async () => {
|
||||
|
|
@ -124,6 +124,100 @@ describe("dashboard datasource resolution", () => {
|
|||
expect(resolved).not.toBe(testDocument);
|
||||
expect(testDocument.telemetry[0].value.value).toBe(1);
|
||||
});
|
||||
|
||||
test("shares service health snapshots across aggregate tile hydration", async () => {
|
||||
let resolveFetch: ((response: Response) => void) | undefined;
|
||||
const fetch = vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url !== "https://service.example/health") {
|
||||
throw new Error(`Unhandled test request: ${url}`);
|
||||
}
|
||||
|
||||
return new Promise<Response>((resolve) => {
|
||||
resolveFetch = resolve;
|
||||
});
|
||||
});
|
||||
const document = testDashboard();
|
||||
|
||||
const moduleTile = resolveDashboardTile(
|
||||
document,
|
||||
{ kind: "module", id: "runtime-health-summary" },
|
||||
{ fetch },
|
||||
);
|
||||
const statusTile = resolveDashboardTile(
|
||||
document,
|
||||
{ kind: "status", stripId: "footer", id: "system-status" },
|
||||
{ fetch },
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolveFetch?.(jsonResponse({ status: "UP", ping: 42 }));
|
||||
expect(await moduleTile).toMatchObject({
|
||||
state: "ready",
|
||||
item: {
|
||||
id: "runtime-health-summary",
|
||||
severity: "ok",
|
||||
value: "all systems operational",
|
||||
},
|
||||
});
|
||||
expect(await statusTile).toMatchObject({
|
||||
state: "ready",
|
||||
item: {
|
||||
id: "system-status",
|
||||
severity: "ok",
|
||||
value: "All systems operational",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("isolates service health snapshots by datasource fetch context", async () => {
|
||||
const firstFetch = vi.fn(async () =>
|
||||
jsonResponse({
|
||||
status: "UP",
|
||||
ping: 42,
|
||||
})
|
||||
);
|
||||
const secondFetch = vi.fn(async () =>
|
||||
jsonResponse({
|
||||
status: "DOWN",
|
||||
ping: 0,
|
||||
})
|
||||
);
|
||||
const document = testDashboard();
|
||||
document.serviceGroups[0].services[0] = {
|
||||
...document.serviceGroups[0].services[0],
|
||||
id: "api-isolated",
|
||||
datasource: {
|
||||
type: "external",
|
||||
adapter: "http-status",
|
||||
reference: "GET https://service.example/isolated",
|
||||
},
|
||||
};
|
||||
|
||||
await resolveDashboardTile(
|
||||
document,
|
||||
{ kind: "status", stripId: "footer", id: "system-status" },
|
||||
{ fetch: firstFetch },
|
||||
);
|
||||
const second = await resolveDashboardTile(
|
||||
document,
|
||||
{ kind: "status", stripId: "footer", id: "system-status" },
|
||||
{ fetch: secondFetch },
|
||||
);
|
||||
|
||||
expect(firstFetch).toHaveBeenCalledTimes(1);
|
||||
expect(secondFetch).toHaveBeenCalledTimes(1);
|
||||
expect(second).toMatchObject({
|
||||
state: "ready",
|
||||
item: {
|
||||
id: "system-status",
|
||||
severity: "danger",
|
||||
value: "1 service down",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const testDocument = testDashboard();
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export type DashboardTileResolution =
|
|||
|
||||
export interface DatasourceResolutionOptions {
|
||||
fetch?: DatasourceFetch;
|
||||
now?: () => number;
|
||||
prometheusBaseUrl?: string;
|
||||
prometheusRangeSeconds?: number;
|
||||
prometheusStepSeconds?: number;
|
||||
|
|
@ -113,9 +114,7 @@ export async function resolveDashboardTile(
|
|||
const item = module.id === "runtime-health-summary"
|
||||
? runtimeHealthSummary(
|
||||
module,
|
||||
await Promise.all(
|
||||
document.serviceGroups.map((group) => resolveServiceGroup(group, context)),
|
||||
),
|
||||
await serviceGroupsSnapshot(document, context),
|
||||
)
|
||||
: await resolveModule(module, context);
|
||||
|
||||
|
|
@ -132,7 +131,7 @@ export async function resolveDashboardTile(
|
|||
item: await resolveStatusTile(
|
||||
statusItem,
|
||||
document.metadata.refreshIntervalSeconds,
|
||||
document.serviceGroups,
|
||||
document,
|
||||
context,
|
||||
),
|
||||
};
|
||||
|
|
@ -140,14 +139,27 @@ export async function resolveDashboardTile(
|
|||
|
||||
interface DatasourceContext {
|
||||
fetch: DatasourceFetch;
|
||||
fetchIdentity: number;
|
||||
now: () => number;
|
||||
prometheusBaseUrl: string;
|
||||
prometheusRangeSeconds: number;
|
||||
prometheusStepSeconds: number;
|
||||
requestTimeoutMs: number;
|
||||
serviceGroupsSnapshot?: Promise<ServiceGroup[]>;
|
||||
}
|
||||
|
||||
type DatasourceFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
interface ServiceGroupsSnapshotEntry {
|
||||
expiresAt: number;
|
||||
snapshot: Promise<ServiceGroup[]>;
|
||||
}
|
||||
|
||||
const serviceGroupsSnapshotTtlMs = 30_000;
|
||||
const serviceGroupsSnapshotCache = new Map<string, ServiceGroupsSnapshotEntry>();
|
||||
const datasourceFetchIdentities = new WeakMap<DatasourceFetch, number>();
|
||||
let nextDatasourceFetchIdentity = 1;
|
||||
|
||||
interface PrometheusVectorResult {
|
||||
metric?: Record<string, string>;
|
||||
value?: [number, string];
|
||||
|
|
@ -159,8 +171,12 @@ interface PrometheusMatrixResult {
|
|||
}
|
||||
|
||||
function datasourceContext(options: DatasourceResolutionOptions): DatasourceContext {
|
||||
const fetch = options.fetch || globalThis.fetch;
|
||||
|
||||
return {
|
||||
fetch: options.fetch || globalThis.fetch,
|
||||
fetch,
|
||||
fetchIdentity: datasourceFetchIdentity(fetch),
|
||||
now: options.now || Date.now,
|
||||
prometheusBaseUrl:
|
||||
options.prometheusBaseUrl ||
|
||||
process.env.PROMETHEUS_BASE_URL ||
|
||||
|
|
@ -171,6 +187,68 @@ function datasourceContext(options: DatasourceResolutionOptions): DatasourceCont
|
|||
};
|
||||
}
|
||||
|
||||
function datasourceFetchIdentity(fetch: DatasourceFetch): number {
|
||||
const existing = datasourceFetchIdentities.get(fetch);
|
||||
if (existing) return existing;
|
||||
|
||||
const next = nextDatasourceFetchIdentity;
|
||||
nextDatasourceFetchIdentity += 1;
|
||||
datasourceFetchIdentities.set(fetch, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
function serviceGroupsSnapshot(
|
||||
document: DashboardDocument,
|
||||
context: DatasourceContext,
|
||||
): Promise<ServiceGroup[]> {
|
||||
if (context.serviceGroupsSnapshot) return context.serviceGroupsSnapshot;
|
||||
|
||||
const key = serviceGroupsSnapshotKey(document, context);
|
||||
const now = context.now();
|
||||
const cached = serviceGroupsSnapshotCache.get(key);
|
||||
if (cached && cached.expiresAt > now) {
|
||||
context.serviceGroupsSnapshot = cached.snapshot;
|
||||
return cached.snapshot;
|
||||
}
|
||||
|
||||
const snapshot = Promise.all(
|
||||
document.serviceGroups.map((group) => resolveServiceGroup(group, context)),
|
||||
);
|
||||
context.serviceGroupsSnapshot = snapshot;
|
||||
serviceGroupsSnapshotCache.set(key, {
|
||||
expiresAt: now + serviceGroupsSnapshotTtlMs,
|
||||
snapshot,
|
||||
});
|
||||
snapshot.catch(() => {
|
||||
if (serviceGroupsSnapshotCache.get(key)?.snapshot === snapshot) {
|
||||
serviceGroupsSnapshotCache.delete(key);
|
||||
}
|
||||
});
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function serviceGroupsSnapshotKey(
|
||||
document: DashboardDocument,
|
||||
context: DatasourceContext,
|
||||
): string {
|
||||
return JSON.stringify(
|
||||
{
|
||||
fetchIdentity: context.fetchIdentity,
|
||||
prometheusBaseUrl: context.prometheusBaseUrl,
|
||||
prometheusRangeSeconds: context.prometheusRangeSeconds,
|
||||
prometheusStepSeconds: context.prometheusStepSeconds,
|
||||
requestTimeoutMs: context.requestTimeoutMs,
|
||||
serviceGroups: document.serviceGroups.map((group) => ({
|
||||
id: group.id,
|
||||
services: group.services.map((service) => ({
|
||||
datasource: service.datasource,
|
||||
id: service.id,
|
||||
})),
|
||||
})),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveTelemetryCard(
|
||||
card: TelemetryCard,
|
||||
context: DatasourceContext,
|
||||
|
|
@ -485,13 +563,11 @@ function resolveStatusItem(
|
|||
async function resolveStatusTile(
|
||||
item: StatusItem,
|
||||
refreshIntervalSeconds: number | undefined,
|
||||
serviceGroups: ServiceGroup[],
|
||||
document: DashboardDocument,
|
||||
context: DatasourceContext,
|
||||
): Promise<StatusItem> {
|
||||
if (item.id === "system-status") {
|
||||
const resolvedGroups = await Promise.all(
|
||||
serviceGroups.map((group) => resolveServiceGroup(group, context)),
|
||||
);
|
||||
const resolvedGroups = await serviceGroupsSnapshot(document, context);
|
||||
const health = serviceHealthSummary(resolvedGroups);
|
||||
return {
|
||||
...structuredClone(item),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue