Added ZFS state metrics
Build Docker image on push / docker (push) Successful in 23s
Build and push Docker image on tag / docker (push) Successful in 23s

This commit is contained in:
2026-08-17 22:08:57 +02:00
parent f8f9e0c852
commit ba88e5997a
9 changed files with 363 additions and 3 deletions
+5
View File
@@ -55,6 +55,11 @@ func NewPveMetricsManager(apiClient *proxmox.PveApiClient, conf *configuration.P
c.RegisterCollector(NewPveNodeDiskCollector(apiClient, c.registry))
}
// Node ZFS pool collector.
if metricsCf.ZFS {
c.RegisterCollector(NewPveNodeZfsCollector(apiClient, c.registry))
}
// Node SDN collector.
if metricsCf.SDN {
c.RegisterCollector(NewPveSdnCollector(apiClient, c.registry))
+153
View File
@@ -0,0 +1,153 @@
package metrics
import (
"strconv"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"lostak.dev/pve-exporter/proxmox"
)
// PveNodeZfsCollector collects ZFS pool and component health metrics.
type PveNodeZfsCollector struct {
apiClient *proxmox.PveApiClient
registry *TTLRegistry
state *TTLGaugeVec
readErrors *TTLGaugeVec
writeErrors *TTLGaugeVec
checksumErrors *TTLGaugeVec
}
// zfsMetricComponent is a flattened entry from the recursive ZFS topology.
type zfsMetricComponent struct {
proxmox.PveZfsComponent
Path string
}
// NewPveNodeZfsCollector creates a ZFS metrics collector.
func NewPveNodeZfsCollector(apiClient *proxmox.PveApiClient, registry *TTLRegistry) *PveNodeZfsCollector {
c := PveNodeZfsCollector{apiClient: apiClient, registry: registry}
labelNames := []string{"cluster", "node", "pool", "component", "path", "leaf"}
c.state = NewTTLGaugeVec(
prometheus.GaugeOpts{
Name: "pve_node_zfs_state",
Help: "ZFS pool component state (1 = ONLINE, 0 = any other state).",
},
labelNames,
5*time.Minute,
)
c.registry.Register(c.state)
c.readErrors = NewTTLGaugeVec(
prometheus.GaugeOpts{
Name: "pve_node_zfs_read_errors",
Help: "ZFS pool component read error count.",
},
labelNames,
5*time.Minute,
)
c.registry.Register(c.readErrors)
c.writeErrors = NewTTLGaugeVec(
prometheus.GaugeOpts{
Name: "pve_node_zfs_write_errors",
Help: "ZFS pool component write error count.",
},
labelNames,
5*time.Minute,
)
c.registry.Register(c.writeErrors)
c.checksumErrors = NewTTLGaugeVec(
prometheus.GaugeOpts{
Name: "pve_node_zfs_checksum_errors",
Help: "ZFS pool component checksum error count.",
},
labelNames,
5*time.Minute,
)
c.registry.Register(c.checksumErrors)
return &c
}
// CollectMetrics implements PveMetricsCollector.
func (c *PveNodeZfsCollector) CollectMetrics() error {
cluster, err := c.apiClient.GetClusterStatus()
if err != nil {
return err
}
for _, node := range cluster.NodeStatuses {
pools, err := c.apiClient.GetNodeZfsPools(node.Name)
if err != nil {
return err
}
for _, pool := range *pools {
status, err := c.apiClient.GetNodeZfsPoolStatus(node.Name, pool.Name)
if err != nil {
return err
}
for _, component := range flattenZfsComponents(status) {
labels := prometheus.Labels{
"cluster": cluster.GetClusterName(),
"node": node.Name,
"pool": pool.Name,
"component": component.Name,
"path": component.Path,
"leaf": strconv.FormatBool(component.Leaf == 1),
}
if component.State != "" {
c.state.With(labels).Set(zfsStateNumeric(component.State))
}
if component.Read != nil {
c.readErrors.With(labels).Set(float64(*component.Read))
}
if component.Write != nil {
c.writeErrors.With(labels).Set(float64(*component.Write))
}
if component.Checksum != nil {
c.checksumErrors.With(labels).Set(float64(*component.Checksum))
}
}
}
}
return nil
}
// GetName implements PveMetricsCollector.
func (c *PveNodeZfsCollector) GetName() string {
return "Node ZFS"
}
func flattenZfsComponents(status *proxmox.PveZfsPoolStatus) []zfsMetricComponent {
components := make([]zfsMetricComponent, 0)
appendZfsComponent(&components, status.PveZfsComponent, nil)
return components
}
func appendZfsComponent(components *[]zfsMetricComponent, component proxmox.PveZfsComponent, parents []string) {
pathParts := append(append([]string{}, parents...), component.Name)
*components = append(*components, zfsMetricComponent{
PveZfsComponent: component,
Path: strings.Join(pathParts, " > "),
})
for _, child := range component.Children {
appendZfsComponent(components, child, pathParts)
}
}
func zfsStateNumeric(state string) float64 {
if strings.EqualFold(state, "ONLINE") {
return 1
}
return 0
}
+64
View File
@@ -0,0 +1,64 @@
package metrics
import (
"testing"
"lostak.dev/pve-exporter/proxmox"
)
func TestFlattenZfsComponents(t *testing.T) {
zero := uint64(0)
status := &proxmox.PveZfsPoolStatus{
PveZfsComponent: proxmox.PveZfsComponent{
Name: "hdd",
State: "ONLINE",
Children: []proxmox.PveZfsComponent{
{
Name: "hdd",
Children: []proxmox.PveZfsComponent{
{
Name: "raidz1-0",
State: "ONLINE",
Read: &zero,
Write: &zero,
Checksum: &zero,
Children: []proxmox.PveZfsComponent{
{
Name: "/dev/disk/by-id/disk-1-part1",
State: "ONLINE",
Leaf: 1,
Read: &zero,
Write: &zero,
Checksum: &zero,
},
},
},
},
},
},
},
}
components := flattenZfsComponents(status)
if len(components) != 4 {
t.Fatalf("flattenZfsComponents() returned %d components, want 4", len(components))
}
leaf := components[3]
wantPath := "hdd > hdd > raidz1-0 > /dev/disk/by-id/disk-1-part1"
if leaf.Path != wantPath {
t.Fatalf("leaf path = %q, want %q", leaf.Path, wantPath)
}
if leaf.Leaf != 1 || leaf.Read == nil || leaf.Write == nil || leaf.Checksum == nil {
t.Fatal("leaf component lost its type or error counters")
}
}
func TestZfsStateNumeric(t *testing.T) {
if got := zfsStateNumeric("ONLINE"); got != 1 {
t.Fatalf("zfsStateNumeric(ONLINE) = %v, want 1", got)
}
if got := zfsStateNumeric("DEGRADED"); got != 0 {
t.Fatalf("zfsStateNumeric(DEGRADED) = %v, want 0", got)
}
}