From e5a5e6416b5789936b99eaf9002495ae2c96b61a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Lo=C5=A1=C5=A5=C3=A1k?= Date: Mon, 17 Aug 2026 21:29:09 +0200 Subject: [PATCH] Added cluster mode metric --- README.md | 5 ++++- metrics/pve_cluster_state_collector.go | 19 +++++++++++++--- proxmox/model.go | 9 ++++++++ proxmox/model_test.go | 30 ++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 proxmox/model_test.go diff --git a/README.md b/README.md index 6309133..30d1a47 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,9 @@ metrics: sdn: true # Enable collection of software-defined network (SDN) metrics. ``` +The cluster state collector exposes `pve_cluster_mode`, where `1` means that +Proxmox VE is configured as a cluster and `0` means that it is a standalone node. + ## Build To build the Docker image for PVE Exporter, use the following command: @@ -101,4 +104,4 @@ Contributions are welcome! Please open an issue or submit a pull request if you ## Support -If you encounter any issues or have questions, please open an issue on the project's GitHub repository. \ No newline at end of file +If you encounter any issues or have questions, please open an issue on the project's GitHub repository. diff --git a/metrics/pve_cluster_state_collector.go b/metrics/pve_cluster_state_collector.go index ee0fb92..757da4c 100644 --- a/metrics/pve_cluster_state_collector.go +++ b/metrics/pve_cluster_state_collector.go @@ -12,8 +12,9 @@ type PveClusterStateCollector struct { apiClient *proxmox.PveApiClient // PVE API client instance. registry *TTLRegistry // TTL metrics registry. - nodes *TTLGaugeVec // Count of nodes prometheus gauge. - quorate *TTLGaugeVec // Cluster quorum state prometheus gauge. + nodes *TTLGaugeVec // Count of nodes prometheus gauge. + quorate *TTLGaugeVec // Cluster quorum state prometheus gauge. + clusterMode *TTLGaugeVec // PVE cluster mode prometheus gauge. } // Create new instance of PVE cluster state collector. @@ -42,6 +43,17 @@ func NewPveClusterStateCollector(apiClient *proxmox.PveApiClient, registry *TTLR 5*time.Minute, ) c.registry.Register(c.quorate) + + // PVE cluster mode gauge. + c.clusterMode = NewTTLGaugeVec( + prometheus.GaugeOpts{ + Name: "pve_cluster_mode", + Help: "PVE cluster mode (1 = cluster, 0 = standalone node).", + }, + []string{"cluster"}, + 5*time.Minute, + ) + c.registry.Register(c.clusterMode) c.registry.StartCleanupLoop(5 * time.Second) return &c @@ -54,9 +66,10 @@ func (c *PveClusterStateCollector) CollectMetrics() error { return err } - l := prometheus.Labels{"cluster": cluster.Name} + l := prometheus.Labels{"cluster": cluster.GetClusterName()} c.nodes.With(l).Set(float64(cluster.Nodes)) c.quorate.With(l).Set(float64(cluster.Quorate)) + c.clusterMode.With(l).Set(cluster.GetClusterModeNumeric()) return nil } diff --git a/proxmox/model.go b/proxmox/model.go index 1494df5..d0a3ac5 100644 --- a/proxmox/model.go +++ b/proxmox/model.go @@ -486,6 +486,15 @@ func (r *PveQemuStatus) IsRunning() bool { return false } +// GetClusterModeNumeric returns whether the PVE installation is configured as a cluster. +// The cluster status API includes an object with type "cluster" only in cluster mode. +func (r *PveClusterStatus) GetClusterModeNumeric() float64 { + if r.Type == "cluster" { + return 1 + } + return 0 +} + // GetClusterName returns name of cluster for labeling purposes. // If there is no cluster then return standalone node with node name. Otherwise, it returns cluster name. func (r *PveClusterStatus) GetClusterName() string { diff --git a/proxmox/model_test.go b/proxmox/model_test.go new file mode 100644 index 0000000..d268df0 --- /dev/null +++ b/proxmox/model_test.go @@ -0,0 +1,30 @@ +package proxmox + +import "testing" + +func TestPveClusterStatusGetClusterModeNumeric(t *testing.T) { + tests := []struct { + name string + status PveClusterStatus + want float64 + }{ + { + name: "cluster mode", + status: PveClusterStatus{Type: "cluster"}, + want: 1, + }, + { + name: "standalone node", + status: PveClusterStatus{}, + want: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.status.GetClusterModeNumeric(); got != tt.want { + t.Fatalf("GetClusterModeNumeric() = %v, want %v", got, tt.want) + } + }) + } +}