use super::{MetricBuilder, MetricExporter};
use crate::device::VgpuHostInfo;
pub struct VgpuMetricExporter<'a> {
pub vgpu_info: &'a [VgpuHostInfo],
}
impl<'a> VgpuMetricExporter<'a> {
pub fn new(vgpu_info: &'a [VgpuHostInfo]) -> Self {
Self { vgpu_info }
}
fn host_mode_code(host_mode: &str) -> u32 {
match host_mode {
"NonSriov" => 0,
"Sriov" => 1,
_ => 2, }
}
fn export_host_info(&self, builder: &mut MetricBuilder) {
builder
.help(
"all_smi_vgpu_host_mode",
"NVIDIA vGPU host mode (0=NonSriov, 1=Sriov, 2=Disabled)",
)
.type_("all_smi_vgpu_host_mode", "gauge");
for host in self.vgpu_info {
let gpu_index_str = host.gpu_index.to_string();
let labels = [
("gpu_index", gpu_index_str.as_str()),
("gpu_uuid", host.gpu_uuid.as_str()),
("gpu", host.gpu_name.as_str()),
("instance", host.instance.as_str()),
("host", host.hostname.as_str()),
("host_mode", host.host_mode.as_str()),
];
builder.metric(
"all_smi_vgpu_host_mode",
&labels,
Self::host_mode_code(&host.host_mode),
);
}
builder
.help(
"all_smi_vgpu_scheduler_state",
"NVIDIA vGPU scheduler ARR mode (0=unsupported, 1=off, 2=adaptive round robin)",
)
.type_("all_smi_vgpu_scheduler_state", "gauge");
for host in self.vgpu_info {
let gpu_index_str = host.gpu_index.to_string();
let arr_supported = if host.is_arr_supported {
"true"
} else {
"false"
};
let labels = [
("gpu_index", gpu_index_str.as_str()),
("gpu_uuid", host.gpu_uuid.as_str()),
("gpu", host.gpu_name.as_str()),
("instance", host.instance.as_str()),
("host", host.hostname.as_str()),
("arr_supported", arr_supported),
];
builder.metric(
"all_smi_vgpu_scheduler_state",
&labels,
host.scheduler_arr_mode,
);
}
builder
.help(
"all_smi_vgpu_scheduler_policy",
"NVIDIA vGPU scheduler policy id",
)
.type_("all_smi_vgpu_scheduler_policy", "gauge");
for host in self.vgpu_info {
let gpu_index_str = host.gpu_index.to_string();
let labels = [
("gpu_index", gpu_index_str.as_str()),
("gpu_uuid", host.gpu_uuid.as_str()),
("gpu", host.gpu_name.as_str()),
("instance", host.instance.as_str()),
("host", host.hostname.as_str()),
];
builder.metric(
"all_smi_vgpu_scheduler_policy",
&labels,
host.scheduler_policy,
);
}
}
fn export_instance_metrics(&self, builder: &mut MetricBuilder) {
let rows = self.collect_rows();
builder
.help(
"all_smi_vgpu_utilization",
"Per-vGPU GPU utilization percentage (0-100) as reported by NVML accounting",
)
.type_("all_smi_vgpu_utilization", "gauge");
for row in &rows {
if let Some(util) = row.vgpu.gpu_utilization {
let labels = Self::instance_labels(row);
builder.metric("all_smi_vgpu_utilization", &labels, util);
}
}
builder
.help(
"all_smi_vgpu_memory_utilization",
"Per-vGPU memory bandwidth utilization percentage (0-100)",
)
.type_("all_smi_vgpu_memory_utilization", "gauge");
for row in &rows {
if let Some(util) = row.vgpu.memory_utilization {
let labels = Self::instance_labels(row);
builder.metric("all_smi_vgpu_memory_utilization", &labels, util);
}
}
builder
.help(
"all_smi_vgpu_memory_used_bytes",
"Per-vGPU framebuffer memory used in bytes",
)
.type_("all_smi_vgpu_memory_used_bytes", "gauge");
for row in &rows {
let labels = Self::instance_labels(row);
builder.metric(
"all_smi_vgpu_memory_used_bytes",
&labels,
row.vgpu.fb_used_bytes,
);
}
builder
.help(
"all_smi_vgpu_memory_total_bytes",
"Per-vGPU framebuffer memory budget in bytes",
)
.type_("all_smi_vgpu_memory_total_bytes", "gauge");
for row in &rows {
let labels = Self::instance_labels(row);
builder.metric(
"all_smi_vgpu_memory_total_bytes",
&labels,
row.vgpu.fb_total_bytes,
);
}
builder
.help(
"all_smi_vgpu_active",
"Per-vGPU liveness (1=accounting PID active, 0=idle)",
)
.type_("all_smi_vgpu_active", "gauge");
for row in &rows {
let labels = Self::instance_labels(row);
builder.metric(
"all_smi_vgpu_active",
&labels,
if row.vgpu.is_active { 1 } else { 0 },
);
}
}
fn collect_rows(&self) -> Vec<Row<'a>> {
let total: usize = self.vgpu_info.iter().map(|h| h.vgpus.len()).sum();
let mut rows = Vec::with_capacity(total);
for host in self.vgpu_info {
let gpu_index_str = host.gpu_index.to_string();
for vgpu in &host.vgpus {
rows.push(Row {
host,
vgpu,
gpu_index_str: gpu_index_str.clone(),
instance_id_str: vgpu.instance_id.to_string(),
});
}
}
rows
}
fn instance_labels<'b>(row: &'b Row<'a>) -> [(&'b str, &'b str); 9] {
[
("gpu_index", row.gpu_index_str.as_str()),
("gpu_uuid", row.host.gpu_uuid.as_str()),
("gpu", row.host.gpu_name.as_str()),
("instance", row.host.instance.as_str()),
("host", row.host.hostname.as_str()),
("vgpu_id", row.instance_id_str.as_str()),
("vgpu_uuid", row.vgpu.uuid.as_str()),
("vgpu_type", row.vgpu.vgpu_type_name.as_str()),
("vgpu_vm_id", row.vgpu.vm_id.as_str()),
]
}
}
struct Row<'a> {
host: &'a VgpuHostInfo,
vgpu: &'a crate::device::VgpuInfo,
gpu_index_str: String,
instance_id_str: String,
}
impl<'a> MetricExporter for VgpuMetricExporter<'a> {
fn export_metrics(&self) -> String {
if self.vgpu_info.is_empty() {
return String::new();
}
let mut builder = MetricBuilder::new();
self.export_host_info(&mut builder);
self.export_instance_metrics(&mut builder);
builder.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device::{VgpuHostInfo, VgpuInfo};
use std::collections::HashMap;
fn sample_host() -> VgpuHostInfo {
VgpuHostInfo {
host_id: "gpu-host".to_string(),
hostname: "gpu-host".to_string(),
instance: "gpu-host".to_string(),
gpu_index: 0,
gpu_uuid: "GPU-abc123".to_string(),
gpu_name: "NVIDIA A100".to_string(),
host_mode: "Sriov".to_string(),
scheduler_policy: 1,
scheduler_arr_mode: 2,
is_arr_supported: true,
vgpus: vec![VgpuInfo {
instance_id: 42,
uuid: "GRID-xxx".to_string(),
vm_id: "vm-1".to_string(),
vgpu_type_name: "GRID A100-8C".to_string(),
fb_used_bytes: 1 << 30,
fb_total_bytes: 8 << 30,
gpu_utilization: Some(75),
memory_utilization: Some(40),
is_active: true,
}],
detail: HashMap::new(),
}
}
#[test]
fn exports_nothing_when_vgpu_info_empty() {
let exporter = VgpuMetricExporter::new(&[]);
assert_eq!(exporter.export_metrics(), "");
}
#[test]
fn exports_expected_metric_families() {
let hosts = vec![sample_host()];
let exporter = VgpuMetricExporter::new(&hosts);
let output = exporter.export_metrics();
assert!(output.contains("all_smi_vgpu_host_mode"));
assert!(output.contains("all_smi_vgpu_scheduler_state"));
assert!(output.contains("all_smi_vgpu_scheduler_policy"));
assert!(output.contains("all_smi_vgpu_utilization"));
assert!(output.contains("all_smi_vgpu_memory_utilization"));
assert!(output.contains("all_smi_vgpu_memory_used_bytes"));
assert!(output.contains("all_smi_vgpu_memory_total_bytes"));
assert!(output.contains("all_smi_vgpu_active"));
}
#[test]
fn exports_required_labels_for_instance_metrics() {
let hosts = vec![sample_host()];
let exporter = VgpuMetricExporter::new(&hosts);
let output = exporter.export_metrics();
assert!(output.contains("gpu_index=\"0\""));
assert!(output.contains("gpu_uuid=\"GPU-abc123\""));
assert!(output.contains("host=\"gpu-host\""));
assert!(output.contains("vgpu_id=\"42\""));
assert!(output.contains("vgpu_uuid=\"GRID-xxx\""));
assert!(output.contains("vgpu_type=\"GRID A100-8C\""));
}
#[test]
fn host_mode_code_is_stable_and_defaults_to_disabled() {
assert_eq!(VgpuMetricExporter::host_mode_code("NonSriov"), 0);
assert_eq!(VgpuMetricExporter::host_mode_code("Sriov"), 1);
assert_eq!(VgpuMetricExporter::host_mode_code("Disabled"), 2);
assert_eq!(VgpuMetricExporter::host_mode_code("garbage"), 2);
}
#[test]
fn util_line_present_only_when_reported() {
let mut host = sample_host();
host.vgpus[0].gpu_utilization = None;
let hosts = vec![host];
let exporter = VgpuMetricExporter::new(&hosts);
let output = exporter.export_metrics();
assert!(output.contains("# HELP all_smi_vgpu_utilization"));
let data_lines = output
.lines()
.filter(|l| l.starts_with("all_smi_vgpu_utilization{"))
.count();
assert_eq!(data_lines, 0);
}
}