feat(ui): server metrics admin dashboard
Adds /admin/server-metrics page mirroring the Database/ClickHouse visibility
rules: sidebar entry gated on capabilities.infrastructureEndpoints, backend
controller now has @ConditionalOnProperty(infrastructureendpoints) and
class-level @PreAuthorize('hasRole(ADMIN)'). Dashboard panels are driven
from docs/server-self-metrics.md via the generic
/api/v1/admin/server-metrics/{catalog,instances,query} API — Server Health,
JVM, HTTP & DB pools, and conditionally Alerting + Deployments when their
metrics appear in the catalog. ThemedChart / Line / Area from the design
system; hooks in ui/src/api/queries/admin/serverMetrics.ts. Not yet
browser-verified against a running dev server — backend IT covers the API
end-to-end (8 tests), UI typecheck + production bundle both clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
113
ui/src/api/queries/admin/serverMetrics.ts
Normal file
113
ui/src/api/queries/admin/serverMetrics.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { adminFetch } from './admin-api';
|
||||
import { useRefreshInterval } from '../use-refresh-interval';
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ServerMetricCatalogEntry {
|
||||
metricName: string;
|
||||
metricType: string;
|
||||
statistics: string[];
|
||||
tagKeys: string[];
|
||||
}
|
||||
|
||||
export interface ServerInstanceInfo {
|
||||
serverInstanceId: string;
|
||||
firstSeen: string;
|
||||
lastSeen: string;
|
||||
}
|
||||
|
||||
export interface ServerMetricPoint {
|
||||
t: string;
|
||||
v: number;
|
||||
}
|
||||
|
||||
export interface ServerMetricSeries {
|
||||
tags: Record<string, string>;
|
||||
points: ServerMetricPoint[];
|
||||
}
|
||||
|
||||
export interface ServerMetricQueryResponse {
|
||||
metric: string;
|
||||
statistic: string;
|
||||
aggregation: string;
|
||||
mode: string;
|
||||
stepSeconds: number;
|
||||
series: ServerMetricSeries[];
|
||||
}
|
||||
|
||||
export interface ServerMetricQueryRequest {
|
||||
metric: string;
|
||||
statistic?: string | null;
|
||||
from: string;
|
||||
to: string;
|
||||
stepSeconds?: number | null;
|
||||
groupByTags?: string[] | null;
|
||||
filterTags?: Record<string, string> | null;
|
||||
aggregation?: string | null;
|
||||
mode?: string | null;
|
||||
serverInstanceIds?: string[] | null;
|
||||
}
|
||||
|
||||
// ── Query Hooks ────────────────────────────────────────────────────────
|
||||
|
||||
export function useServerMetricsCatalog(windowSeconds = 3600) {
|
||||
const refetchInterval = useRefreshInterval(60_000);
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'server-metrics', 'catalog', windowSeconds],
|
||||
queryFn: async () => {
|
||||
const to = new Date();
|
||||
const from = new Date(to.getTime() - windowSeconds * 1000);
|
||||
const params = new URLSearchParams({ from: from.toISOString(), to: to.toISOString() });
|
||||
return adminFetch<ServerMetricCatalogEntry[]>(`/server-metrics/catalog?${params}`);
|
||||
},
|
||||
refetchInterval,
|
||||
});
|
||||
}
|
||||
|
||||
export function useServerMetricsInstances(windowSeconds = 3600) {
|
||||
const refetchInterval = useRefreshInterval(60_000);
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'server-metrics', 'instances', windowSeconds],
|
||||
queryFn: async () => {
|
||||
const to = new Date();
|
||||
const from = new Date(to.getTime() - windowSeconds * 1000);
|
||||
const params = new URLSearchParams({ from: from.toISOString(), to: to.toISOString() });
|
||||
return adminFetch<ServerInstanceInfo[]>(`/server-metrics/instances?${params}`);
|
||||
},
|
||||
refetchInterval,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a time-series query against the server_metrics table.
|
||||
*
|
||||
* The window [from, to) is supplied in seconds of "now minus N" so the panel
|
||||
* refreshes automatically at the polling interval without the caller
|
||||
* recomputing timestamps.
|
||||
*/
|
||||
export function useServerMetricsSeries(
|
||||
request: Omit<ServerMetricQueryRequest, 'from' | 'to'>,
|
||||
windowSeconds: number,
|
||||
opts?: { enabled?: boolean },
|
||||
) {
|
||||
const refetchInterval = useRefreshInterval(30_000);
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'server-metrics', 'query', request, windowSeconds],
|
||||
queryFn: async () => {
|
||||
const to = new Date();
|
||||
const from = new Date(to.getTime() - windowSeconds * 1000);
|
||||
const body: ServerMetricQueryRequest = {
|
||||
...request,
|
||||
from: from.toISOString(),
|
||||
to: to.toISOString(),
|
||||
};
|
||||
return adminFetch<ServerMetricQueryResponse>('/server-metrics/query', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
},
|
||||
refetchInterval,
|
||||
enabled: opts?.enabled ?? true,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user