Compare commits
2 commits
9aa30b07db
...
243d71686d
| Author | SHA1 | Date | |
|---|---|---|---|
| 243d71686d | |||
|
|
5e66367687 |
2 changed files with 180 additions and 10 deletions
|
|
@ -3,7 +3,7 @@ import {
|
||||||
DASHBOARD_SCHEMA_VERSION,
|
DASHBOARD_SCHEMA_VERSION,
|
||||||
type DashboardDocument,
|
type DashboardDocument,
|
||||||
} from "@dimensionlab/dashboard-model";
|
} from "@dimensionlab/dashboard-model";
|
||||||
import { resolveDashboardDatasources } from ".";
|
import { resolveDashboardDatasources, resolveDashboardTile } from ".";
|
||||||
|
|
||||||
describe("dashboard datasource resolution", () => {
|
describe("dashboard datasource resolution", () => {
|
||||||
test("hydrates telemetry, service health, weather, and summary data from live adapters", async () => {
|
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(resolved).not.toBe(testDocument);
|
||||||
expect(testDocument.telemetry[0].value.value).toBe(1);
|
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();
|
const testDocument = testDashboard();
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ export type DashboardTileResolution =
|
||||||
|
|
||||||
export interface DatasourceResolutionOptions {
|
export interface DatasourceResolutionOptions {
|
||||||
fetch?: DatasourceFetch;
|
fetch?: DatasourceFetch;
|
||||||
|
now?: () => number;
|
||||||
prometheusBaseUrl?: string;
|
prometheusBaseUrl?: string;
|
||||||
prometheusRangeSeconds?: number;
|
prometheusRangeSeconds?: number;
|
||||||
prometheusStepSeconds?: number;
|
prometheusStepSeconds?: number;
|
||||||
|
|
@ -113,9 +114,7 @@ export async function resolveDashboardTile(
|
||||||
const item = module.id === "runtime-health-summary"
|
const item = module.id === "runtime-health-summary"
|
||||||
? runtimeHealthSummary(
|
? runtimeHealthSummary(
|
||||||
module,
|
module,
|
||||||
await Promise.all(
|
await serviceGroupsSnapshot(document, context),
|
||||||
document.serviceGroups.map((group) => resolveServiceGroup(group, context)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: await resolveModule(module, context);
|
: await resolveModule(module, context);
|
||||||
|
|
||||||
|
|
@ -132,7 +131,7 @@ export async function resolveDashboardTile(
|
||||||
item: await resolveStatusTile(
|
item: await resolveStatusTile(
|
||||||
statusItem,
|
statusItem,
|
||||||
document.metadata.refreshIntervalSeconds,
|
document.metadata.refreshIntervalSeconds,
|
||||||
document.serviceGroups,
|
document,
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -140,14 +139,27 @@ export async function resolveDashboardTile(
|
||||||
|
|
||||||
interface DatasourceContext {
|
interface DatasourceContext {
|
||||||
fetch: DatasourceFetch;
|
fetch: DatasourceFetch;
|
||||||
|
fetchIdentity: number;
|
||||||
|
now: () => number;
|
||||||
prometheusBaseUrl: string;
|
prometheusBaseUrl: string;
|
||||||
prometheusRangeSeconds: number;
|
prometheusRangeSeconds: number;
|
||||||
prometheusStepSeconds: number;
|
prometheusStepSeconds: number;
|
||||||
requestTimeoutMs: number;
|
requestTimeoutMs: number;
|
||||||
|
serviceGroupsSnapshot?: Promise<ServiceGroup[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type DatasourceFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
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 {
|
interface PrometheusVectorResult {
|
||||||
metric?: Record<string, string>;
|
metric?: Record<string, string>;
|
||||||
value?: [number, string];
|
value?: [number, string];
|
||||||
|
|
@ -159,8 +171,12 @@ interface PrometheusMatrixResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
function datasourceContext(options: DatasourceResolutionOptions): DatasourceContext {
|
function datasourceContext(options: DatasourceResolutionOptions): DatasourceContext {
|
||||||
|
const fetch = options.fetch || globalThis.fetch;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fetch: options.fetch || globalThis.fetch,
|
fetch,
|
||||||
|
fetchIdentity: datasourceFetchIdentity(fetch),
|
||||||
|
now: options.now || Date.now,
|
||||||
prometheusBaseUrl:
|
prometheusBaseUrl:
|
||||||
options.prometheusBaseUrl ||
|
options.prometheusBaseUrl ||
|
||||||
process.env.PROMETHEUS_BASE_URL ||
|
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(
|
async function resolveTelemetryCard(
|
||||||
card: TelemetryCard,
|
card: TelemetryCard,
|
||||||
context: DatasourceContext,
|
context: DatasourceContext,
|
||||||
|
|
@ -485,13 +563,11 @@ function resolveStatusItem(
|
||||||
async function resolveStatusTile(
|
async function resolveStatusTile(
|
||||||
item: StatusItem,
|
item: StatusItem,
|
||||||
refreshIntervalSeconds: number | undefined,
|
refreshIntervalSeconds: number | undefined,
|
||||||
serviceGroups: ServiceGroup[],
|
document: DashboardDocument,
|
||||||
context: DatasourceContext,
|
context: DatasourceContext,
|
||||||
): Promise<StatusItem> {
|
): Promise<StatusItem> {
|
||||||
if (item.id === "system-status") {
|
if (item.id === "system-status") {
|
||||||
const resolvedGroups = await Promise.all(
|
const resolvedGroups = await serviceGroupsSnapshot(document, context);
|
||||||
serviceGroups.map((group) => resolveServiceGroup(group, context)),
|
|
||||||
);
|
|
||||||
const health = serviceHealthSummary(resolvedGroups);
|
const health = serviceHealthSummary(resolvedGroups);
|
||||||
return {
|
return {
|
||||||
...structuredClone(item),
|
...structuredClone(item),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue