Files
homelab/integrations/nextjs/grafana-client.ts
T

181 lines
4.5 KiB
TypeScript
Raw Normal View History

/**
* 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}`;
}