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 } var zfsStateValues = map[string]float64{ "ONLINE": 1, "DEGRADED": 2, "FAULTED": 3, "OFFLINE": 4, "REMOVED": 5, "UNAVAIL": 6, } // NewPveNodeZfsCollector creates a ZFS metrics collector. func NewPveNodeZfsCollector(apiClient *proxmox.PveApiClient, registry *TTLRegistry) *PveNodeZfsCollector { c := PveNodeZfsCollector{apiClient: apiClient, registry: registry} componentLabelNames := []string{"cluster", "node", "pool", "component", "path", "leaf"} c.state = NewTTLGaugeVec( prometheus.GaugeOpts{ Name: "pve_node_zfs_state", Help: "ZFS pool component state (0 = UNKNOWN, 1 = ONLINE, 2 = DEGRADED, 3 = FAULTED, 4 = OFFLINE, 5 = REMOVED, 6 = UNAVAIL).", }, componentLabelNames, 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.", }, componentLabelNames, 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.", }, componentLabelNames, 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.", }, componentLabelNames, 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 value, ok := zfsStateValues[strings.ToUpper(state)]; ok { return value } return 0 }