181 lines
4.5 KiB
TypeScript
181 lines
4.5 KiB
TypeScript
/**
|
|
* Grafana Dashboard API Client
|
|
*
|
|
* Fetches dashboard data and panel snapshots from Grafana.
|
|
* Requires service account token with Viewer role.
|
|
*
|
|
* Setup:
|
|
* 1. Grafana UI > Administration > Service Accounts > Add
|
|
* 2. Create token, set GRAFANA_API_TOKEN env var
|
|
* 3. Or use Grafana API directly: POST /api/serviceaccounts
|
|
*/
|
|
|
|
const GRAFANA_URL = process.env.GRAFANA_URL || 'https://grafana.riotpiao.com';
|
|
const GRAFANA_API_TOKEN = process.env.GRAFANA_API_TOKEN || '';
|
|
|
|
interface GrafanaHeaders {
|
|
'Content-Type': string;
|
|
Authorization?: string;
|
|
}
|
|
|
|
function headers(): GrafanaHeaders {
|
|
const h: GrafanaHeaders = { 'Content-Type': 'application/json' };
|
|
if (GRAFANA_API_TOKEN) {
|
|
h.Authorization = `Bearer ${GRAFANA_API_TOKEN}`;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
export interface Dashboard {
|
|
uid: string;
|
|
title: string;
|
|
panels: Panel[];
|
|
}
|
|
|
|
export interface Panel {
|
|
id: number;
|
|
title: string;
|
|
type: string;
|
|
gridPos: { h: number; w: number; x: number; y: number };
|
|
}
|
|
|
|
export interface DashboardMeta {
|
|
uid: string;
|
|
title: string;
|
|
url: string;
|
|
slug: string;
|
|
}
|
|
|
|
/**
|
|
* Get dashboard by UID
|
|
*/
|
|
export async function getDashboard(uid: string): Promise<Dashboard> {
|
|
const response = await fetch(`${GRAFANA_URL}/api/dashboards/uid/${uid}`, {
|
|
headers: headers(),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Grafana API error: ${response.status} ${await response.text()}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.dashboard;
|
|
}
|
|
|
|
/**
|
|
* List all dashboards
|
|
*/
|
|
export async function listDashboards(): Promise<DashboardMeta[]> {
|
|
const response = await fetch(`${GRAFANA_URL}/api/search?type=dash-db`, {
|
|
headers: headers(),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Grafana API error: ${response.status}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Query Prometheus datasource directly via Grafana proxy
|
|
* Bypasses need for direct Prometheus access
|
|
*/
|
|
export async function queryPrometheus(
|
|
query: string,
|
|
start?: number,
|
|
end?: number,
|
|
step?: number
|
|
): Promise<PrometheusResult> {
|
|
const params = new URLSearchParams({
|
|
query,
|
|
start: String(start || Math.floor(Date.now() / 1000) - 3600),
|
|
end: String(end || Math.floor(Date.now() / 1000)),
|
|
step: String(step || 60),
|
|
});
|
|
|
|
const response = await fetch(
|
|
`${GRAFANA_URL}/api/datasources/proxy/uid/prometheus/api/v1/query_range?${params}`,
|
|
{ headers: headers() }
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Prometheus query error: ${response.status}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
export interface PrometheusResult {
|
|
status: string;
|
|
data: {
|
|
resultType: string;
|
|
result: {
|
|
metric: Record<string, string>;
|
|
values: [number, string][];
|
|
}[];
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get LLM-specific metrics
|
|
*/
|
|
export async function getLLMMetrics(): Promise<LLMMetrics> {
|
|
const [podsReady, cpuUsage, memoryUsage] = await Promise.all([
|
|
queryPrometheus('sum(kube_pod_status_ready{namespace="llm-serving",condition="true"})'),
|
|
queryPrometheus('sum(rate(container_cpu_usage_seconds_total{namespace="llm-serving"}[5m])) by (pod)'),
|
|
queryPrometheus('sum(container_memory_working_set_bytes{namespace="llm-serving"}) by (pod)'),
|
|
]);
|
|
|
|
return {
|
|
podsReady: extractLatestValue(podsReady),
|
|
cpuByPod: extractSeriesLatest(cpuUsage),
|
|
memoryByPod: extractSeriesLatest(memoryUsage),
|
|
};
|
|
}
|
|
|
|
export interface LLMMetrics {
|
|
podsReady: number;
|
|
cpuByPod: Record<string, number>;
|
|
memoryByPod: Record<string, number>;
|
|
}
|
|
|
|
function extractLatestValue(result: PrometheusResult): number {
|
|
const values = result.data?.result?.[0]?.values;
|
|
if (!values || values.length === 0) return 0;
|
|
return parseFloat(values[values.length - 1][1]);
|
|
}
|
|
|
|
function extractSeriesLatest(result: PrometheusResult): Record<string, number> {
|
|
const out: Record<string, number> = {};
|
|
for (const series of result.data?.result || []) {
|
|
const pod = series.metric.pod || 'unknown';
|
|
const values = series.values;
|
|
if (values && values.length > 0) {
|
|
out[pod] = parseFloat(values[values.length - 1][1]);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Generate iframe embed URL for a panel
|
|
* Requires Grafana security.allow_embedding=true
|
|
*/
|
|
export function getPanelEmbedUrl(
|
|
dashboardUid: string,
|
|
panelId: number,
|
|
from = 'now-6h',
|
|
to = 'now',
|
|
refresh = '30s'
|
|
): string {
|
|
return `${GRAFANA_URL}/d-solo/${dashboardUid}?panelId=${panelId}&from=${from}&to=${to}&refresh=${refresh}`;
|
|
}
|
|
|
|
/**
|
|
* Generate full dashboard URL
|
|
*/
|
|
export function getDashboardUrl(dashboardUid: string): string {
|
|
return `${GRAFANA_URL}/d/${dashboardUid}`;
|
|
}
|