feat: add Prometheus metrics parser for llama-swap GPU series
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
export interface PromSample {
|
||||
metric: string;
|
||||
labels: Record<string, string>;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const LINE_RE = /^([A-Za-z_:][A-Za-z0-9_:]*)(?:\{([^}]*)\})?\s+(-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)$/;
|
||||
|
||||
export function parsePrometheus(text: string): PromSample[] {
|
||||
const samples: PromSample[] = [];
|
||||
for (const rawLine of text.split("\n")) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const m = LINE_RE.exec(line);
|
||||
if (!m) continue;
|
||||
const labels: Record<string, string> = {};
|
||||
if (m[2]) {
|
||||
for (const pair of m[2].split(",")) {
|
||||
const eq = pair.indexOf("=");
|
||||
if (eq === -1) continue;
|
||||
const value = pair.slice(eq + 1).trim();
|
||||
labels[pair.slice(0, eq).trim()] = value.replace(/^"(.*)"$/, "$1");
|
||||
}
|
||||
}
|
||||
samples.push({ metric: m[1], labels, value: parseFloat(m[3]) });
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
export type GpuMetricKind = "util_percent" | "memory_util_percent" | "temperature" | "power" | "fan";
|
||||
|
||||
export const GPU_SERIES: Record<string, GpuMetricKind> = {
|
||||
llamaswap_gpu_util_percent: "util_percent",
|
||||
llamaswap_gpu_memory_util_percent: "memory_util_percent",
|
||||
llamaswap_gpu_temperature_celsius: "temperature",
|
||||
llamaswap_gpu_power_draw_watts: "power",
|
||||
llamaswap_gpu_fan_speed_percent: "fan",
|
||||
};
|
||||
|
||||
export interface GpuSample {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: GpuMetricKind;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export function parseGpuMetrics(text: string): GpuSample[] {
|
||||
const out: GpuSample[] = [];
|
||||
for (const s of parsePrometheus(text)) {
|
||||
const kind = GPU_SERIES[s.metric];
|
||||
if (!kind) continue;
|
||||
const id = s.labels["id"];
|
||||
if (id === undefined) continue;
|
||||
out.push({ id, name: s.labels["name"] ?? "", kind, value: s.value });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function gpuInfos(samples: GpuSample[]): { id: string; name: string }[] {
|
||||
const byId = new Map<string, string>();
|
||||
for (const s of samples) {
|
||||
if (!byId.has(s.id)) byId.set(s.id, s.name);
|
||||
}
|
||||
return [...byId.entries()]
|
||||
.sort((a, b) => parseInt(a[0], 10) - parseInt(b[0], 10))
|
||||
.map(([id, name]) => ({ id, name }));
|
||||
}
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
# HELP llamaswap_gpu_util_percent GPU utilization percent (0-100)
|
||||
# TYPE llamaswap_gpu_util_percent gauge
|
||||
llamaswap_gpu_util_percent{id="0",name="NVIDIA RTX PRO 6000 Blackwell Workstation Edition",uuid="GPU-b77149e6-c22d-599e-9728-c7d4cfee0eb4"} 100
|
||||
llamaswap_gpu_util_percent{id="1",name="NVIDIA RTX PRO 6000 Blackwell Workstation Edition",uuid="GPU-a46fadfa-48d9-c9c4-8fde-9bd641142bb7"} 100
|
||||
llamaswap_gpu_util_percent{id="2",name="NVIDIA GeForce RTX 5090",uuid="GPU-9a59ac37-12fb-5bc4-4767-4f4347898c0a"} 100
|
||||
# HELP llamaswap_gpu_memory_util_percent GPU memory utilization percent (0-100)
|
||||
# TYPE llamaswap_gpu_memory_util_percent gauge
|
||||
llamaswap_gpu_memory_util_percent{id="0",name="NVIDIA RTX PRO 6000 Blackwell Workstation Edition",uuid="GPU-b77149e6-c22d-599e-9728-c7d4cfee0eb4"} 98.41347676402383
|
||||
llamaswap_gpu_memory_util_percent{id="1",name="NVIDIA RTX PRO 6000 Blackwell Workstation Edition",uuid="GPU-a46fadfa-48d9-c9c4-8fde-9bd641142bb7"} 98.41245517790922
|
||||
llamaswap_gpu_memory_util_percent{id="2",name="NVIDIA GeForce RTX 5090",uuid="GPU-9a59ac37-12fb-5bc4-4767-4f4347898c0a"} 97.02211181648113
|
||||
# HELP llamaswap_gpu_temperature_celsius GPU temperature in Celsius
|
||||
# TYPE llamaswap_gpu_temperature_celsius gauge
|
||||
llamaswap_gpu_temperature_celsius{id="0",name="NVIDIA RTX PRO 6000 Blackwell Workstation Edition",uuid="GPU-b77149e6-c22d-599e-9728-c7d4cfee0eb4"} 51
|
||||
llamaswap_gpu_temperature_celsius{id="1",name="NVIDIA RTX PRO 6000 Blackwell Workstation Edition",uuid="GPU-a46fadfa-48d9-c9c4-8fde-9bd641142bb7"} 55
|
||||
llamaswap_gpu_temperature_celsius{id="2",name="NVIDIA GeForce RTX 5090",uuid="GPU-9a59ac37-12fb-5bc4-4767-4f4347898c0a"} 64
|
||||
# HELP llamaswap_gpu_power_draw_watts GPU power draw in watts
|
||||
# TYPE llamaswap_gpu_power_draw_watts gauge
|
||||
llamaswap_gpu_power_draw_watts{id="0",name="NVIDIA RTX PRO 6000 Blackwell Workstation Edition",uuid="GPU-b77149e6-c22d-599e-9728-c7d4cfee0eb4"} 316.56
|
||||
llamaswap_gpu_power_draw_watts{id="1",name="NVIDIA RTX PRO 6000 Blackwell Workstation Edition",uuid="GPU-a46fadfa-48d9-c9c4-8fde-9bd641142bb7"} 336.17
|
||||
llamaswap_gpu_power_draw_watts{id="2",name="NVIDIA GeForce RTX 5090",uuid="GPU-9a59ac37-12fb-5bc4-4767-4f4347898c0a"} 377.28
|
||||
# HELP llamaswap_gpu_fan_speed_percent GPU fan speed percent (0-100)
|
||||
# TYPE llamaswap_gpu_fan_speed_percent gauge
|
||||
llamaswap_gpu_fan_speed_percent{id="0",name="NVIDIA RTX PRO 6000 Blackwell Workstation Edition",uuid="GPU-b77149e6-c22d-599e-9728-c7d4cfee0eb4"} 51
|
||||
llamaswap_gpu_fan_speed_percent{id="1",name="NVIDIA RTX PRO 6000 Blackwell Workstation Edition",uuid="GPU-a46fadfa-48d9-c9c4-8fde-9bd641142bb7"} 62
|
||||
llamaswap_gpu_fan_speed_percent{id="2",name="NVIDIA GeForce RTX 5090",uuid="GPU-9a59ac37-12fb-5bc4-4767-4f4347898c0a"} 89
|
||||
@@ -0,0 +1,35 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { test } from "node:test";
|
||||
import { gpuInfos, parseGpuMetrics, parsePrometheus } from "../src/lib/metrics-parser";
|
||||
|
||||
const FIXTURE = readFileSync(new URL("./fixtures/metrics.txt", import.meta.url), "utf8");
|
||||
|
||||
test("parsePrometheus parses a gauge with labels", () => {
|
||||
const samples = parsePrometheus(`# TYPE llamaswap_gpu_util_percent gauge\nllamaswap_gpu_util_percent{id="0",name="RTX 5090"} 42.5\n`);
|
||||
assert.equal(samples.length, 1);
|
||||
assert.equal(samples[0].metric, "llamaswap_gpu_util_percent");
|
||||
assert.deepEqual(samples[0].labels, { id: "0", name: "RTX 5090" });
|
||||
assert.equal(samples[0].value, 42.5);
|
||||
});
|
||||
|
||||
test("parsePrometheus ignores comments and blank lines", () => {
|
||||
const samples = parsePrometheus("# HELP x y\n# TYPE x gauge\n\n");
|
||||
assert.equal(samples.length, 0);
|
||||
});
|
||||
|
||||
test("parseGpuMetrics extracts all five kinds for three GPUs from the fixture", () => {
|
||||
const samples = parseGpuMetrics(FIXTURE);
|
||||
assert.equal(samples.length, 15);
|
||||
const kinds = new Set(samples.map((s) => s.kind));
|
||||
assert.deepEqual([...kinds].sort(), ["fan", "memory_util_percent", "power", "temperature", "util_percent"]);
|
||||
const power0 = samples.find((s) => s.kind === "power" && s.id === "0");
|
||||
assert.ok(power0);
|
||||
assert.equal(power0.value, 316.56);
|
||||
});
|
||||
|
||||
test("gpuInfos dedupes, preserves names, sorts numerically by id", () => {
|
||||
const infos = gpuInfos(parseGpuMetrics(FIXTURE));
|
||||
assert.deepEqual(infos.map((i) => i.id), ["0", "1", "2"]);
|
||||
assert.equal(infos[2].name, "NVIDIA GeForce RTX 5090");
|
||||
});
|
||||
Reference in New Issue
Block a user