From 56c29a6280523cdf8431c9633c3829779dc1fa30 Mon Sep 17 00:00:00 2001 From: Dominik Eisenberg <64131471+Dominik-esb@users.noreply.github.com> Date: Sun, 8 Feb 2026 21:01:53 +0100 Subject: [PATCH] mscluster: Add virtual disk metrics sub-collector (#2296) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Dominik Eisenberg Signed-off-by: Jan-Otto Kröpke Signed-off-by: EisenbergD Co-authored-by: Jan-Otto Kröpke Co-authored-by: EisenbergD --- docs/collector.mscluster.md | 32 +++- internal/collector/mscluster/mscluster.go | 15 ++ .../mscluster/mscluster_virtualdisk.go | 156 ++++++++++++++++++ internal/mi/types.go | 1 + 4 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 internal/collector/mscluster/mscluster_virtualdisk.go diff --git a/docs/collector.mscluster.md b/docs/collector.mscluster.md index 3847a0a9..15dcd46f 100644 --- a/docs/collector.mscluster.md +++ b/docs/collector.mscluster.md @@ -5,14 +5,14 @@ The MSCluster_Cluster class is a dynamic WMI class that represents a cluster. ||| -|- Metric name prefix | `mscluster` -Classes | `MSCluster_Cluster`,`MSCluster_Network`,`MSCluster_Node`,`MSCluster_Resource`,`MSCluster_ResourceGroup`,`MSCluster_DiskPartition` +Classes | `MSCluster_Cluster`,`MSCluster_Network`,`MSCluster_Node`,`MSCluster_Resource`,`MSCluster_ResourceGroup`,`MSCluster_DiskPartition`,`MSFT_VirtualDisk` Enabled by default? | No ## Flags ### `--collectors.mscluster.enabled` Comma-separated list of collectors to use, for example: -`--collectors.mscluster.enabled=cluster,network,node,resource,resourcegroup,shared_volumes`. +`--collectors.mscluster.enabled=cluster,network,node,resource,resouregroup,shared_volumes,virtualdisk`. Matching is case-sensitive. ## Metrics @@ -178,19 +178,45 @@ Matching is case-sensitive. | `mscluster_shared_volumes_total_bytes` | Total size of the Cluster Shared Volume in bytes | gauge | `name`,`volume_guid` | | `mscluster_shared_volumes_free_bytes` | Free space on the Cluster Shared Volume in bytes | gauge | `name`,`volume_guid` | +### Virtual Disk + +| Name | Description | Type | Labels | +|-----------------------------------------------------------|------------------------------------------------------------------------------------------------|-------|--------| +| `mscluster_virtualdisk_info` | Virtual disk information (value is always 1) | gauge | `name`, `unique_id` | +| `mscluster_virtualdisk_health_status` | Health status of the virtual disk. 0: Healthy, 1: Warning, 2: Unhealthy, 5: Unknown | gauge | `name`, `unique_id` | +| `mscluster_virtualdisk_size_bytes` | Total size of the virtual disk in bytes | gauge | `name`, `unique_id` | +| `mscluster_virtualdisk_footprint_on_pool_bytes` | Physical storage consumed by the virtual disk on the storage pool in bytes | gauge | `name`, `unique_id` | +| `mscluster_virtualdisk_storage_efficiency_percent` | Storage efficiency percentage (Size / FootprintOnPool * 100) | gauge | `name`, `unique_id` | + ### Example metric Query the state of all cluster resource owned by node1 ``` windows_mscluster_resource_owner_node{node_name="node1"} ``` +Query virtual disk storage efficiency for thin provisioned disks +``` +windows_mscluster_virtualdisk_storage_efficiency_percent +``` + ## Useful queries Counts the number of Network Name cluster resource ``` count(windows_mscluster_resource_state{type="Network Name"}) ``` +Find virtual disks with low storage efficiency (over-provisioned) +``` +windows_mscluster_virtualdisk_storage_efficiency_percent < 50 +``` + +Calculate total virtual disk capacity vs physical usage +``` +sum(windows_mscluster_virtualdisk_size_bytes) / sum(windows_mscluster_virtualdisk_footprint_on_pool_bytes) * 100 +``` + ## Alerting examples + #### Low free space on cluster shared volume ```yaml # Alerts if volume has less then 20% free space @@ -208,4 +234,4 @@ count(windows_mscluster_resource_state{type="Network Name"}) summary: "Low CSV free space on {{ $labels.name }}" description: | Cluster Shared Volume {{ $labels.name }} on cluster {{ $labels.cluster }} has less than 20% free space (current: {{ printf "%.2f" $value }}%) -``` +``` \ No newline at end of file diff --git a/internal/collector/mscluster/mscluster.go b/internal/collector/mscluster/mscluster.go index 1ef39619..87125173 100644 --- a/internal/collector/mscluster/mscluster.go +++ b/internal/collector/mscluster/mscluster.go @@ -39,6 +39,7 @@ const ( subCollectorResource = "resource" subCollectorResourceGroup = "resourcegroup" subCollectorSharedVolumes = "shared_volumes" + subCollectorVirtualDisk = "virtualdisk" ) type Config struct { @@ -54,6 +55,7 @@ var ConfigDefaults = Config{ subCollectorResource, subCollectorResourceGroup, subCollectorSharedVolumes, + subCollectorVirtualDisk, }, } @@ -65,6 +67,7 @@ type Collector struct { collectorResource collectorResourceGroup collectorSharedVolumes + collectorVirtualDisk config Config miSession *mi.Session @@ -165,6 +168,12 @@ func (c *Collector) Build(_ *slog.Logger, miSession *mi.Session) error { } } + if slices.Contains(c.config.CollectorsEnabled, subCollectorVirtualDisk) { + if err := c.buildVirtualDisk(); err != nil { + errs = append(errs, fmt.Errorf("failed to build virtualdisk collector: %w", err)) + } + } + return errors.Join(errs...) } @@ -243,6 +252,12 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric) error { errCh <- fmt.Errorf("failed to collect shared_volumes metrics: %w", err) } } + + if slices.Contains(c.config.CollectorsEnabled, subCollectorVirtualDisk) { + if err := c.collectVirtualDisk(ch); err != nil { + errCh <- fmt.Errorf("failed to collect virtualdisk metrics: %w", err) + } + } }() wg.Wait() diff --git a/internal/collector/mscluster/mscluster_virtualdisk.go b/internal/collector/mscluster/mscluster_virtualdisk.go new file mode 100644 index 00000000..a00d0af4 --- /dev/null +++ b/internal/collector/mscluster/mscluster_virtualdisk.go @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows + +package mscluster + +import ( + "fmt" + + "github.com/prometheus-community/windows_exporter/internal/mi" + "github.com/prometheus-community/windows_exporter/internal/types" + "github.com/prometheus/client_golang/prometheus" +) + +const nameVirtualDisk = Name + "_virtualdisk" + +type collectorVirtualDisk struct { + virtualDiskMIQuery mi.Query + + virtualDiskInfo *prometheus.Desc + virtualDiskHealthStatus *prometheus.Desc + virtualDiskSize *prometheus.Desc + virtualDiskFootprintOnPool *prometheus.Desc + virtualDiskStorageEfficiency *prometheus.Desc +} + +// msftVirtualDisk represents the MSFT_VirtualDisk WMI class +type msftVirtualDisk struct { + FriendlyName string `mi:"FriendlyName"` + UniqueId string `mi:"UniqueId"` + HealthStatus uint16 `mi:"HealthStatus"` + Size uint64 `mi:"Size"` + FootprintOnPool uint64 `mi:"FootprintOnPool"` + // OperationalStatus []uint16 `mi:"OperationalStatus"` Not supported my mi query: https://github.com/prometheus-community/windows_exporter/pull/2296#issuecomment-3736584632 +} + +func (c *Collector) buildVirtualDisk() error { + wmiSelect := "FriendlyName,UniqueId,HealthStatus,Size,FootprintOnPool" + + virtualDiskMIQuery, err := mi.NewQuery(fmt.Sprintf("SELECT %s FROM MSFT_VirtualDisk", wmiSelect)) + if err != nil { + return fmt.Errorf("failed to create WMI query: %w", err) + } + + c.virtualDiskMIQuery = virtualDiskMIQuery + + c.virtualDiskInfo = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, nameVirtualDisk, "info"), + "Virtual Disk information (value is always 1)", + []string{"name", "unique_id"}, + nil, + ) + + c.virtualDiskHealthStatus = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, nameVirtualDisk, "health_status"), + "Health status of the virtual disk. 0: Healthy, 1: Warning, 2: Unhealthy, 5: Unknown", + []string{"name", "unique_id"}, + nil, + ) + + c.virtualDiskSize = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, nameVirtualDisk, "size_bytes"), + "Total size of the virtual disk in bytes", + []string{"name", "unique_id"}, + nil, + ) + + c.virtualDiskFootprintOnPool = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, nameVirtualDisk, "footprint_on_pool_bytes"), + "Physical storage consumed by the virtual disk on the storage pool in bytes", + []string{"name", "unique_id"}, + nil, + ) + + c.virtualDiskStorageEfficiency = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, nameVirtualDisk, "storage_efficiency_percent"), + "Storage efficiency percentage (Size / FootprintOnPool * 100)", + []string{"name", "unique_id"}, + nil, + ) + + return nil +} + +func (c *Collector) collectVirtualDisk(ch chan<- prometheus.Metric) error { + var dst []msftVirtualDisk + + if err := c.miSession.Query(&dst, mi.NamespaceRootStorage, c.virtualDiskMIQuery); err != nil { + return fmt.Errorf("WMI query failed: %w", err) + } + + for _, vdisk := range dst { + ch <- prometheus.MustNewConstMetric( + c.virtualDiskInfo, + prometheus.GaugeValue, + 1.0, + vdisk.FriendlyName, + vdisk.UniqueId, + ) + + ch <- prometheus.MustNewConstMetric( + c.virtualDiskHealthStatus, + prometheus.GaugeValue, + float64(vdisk.HealthStatus), + vdisk.FriendlyName, + vdisk.UniqueId, + ) + + ch <- prometheus.MustNewConstMetric( + c.virtualDiskSize, + prometheus.GaugeValue, + float64(vdisk.Size), + vdisk.FriendlyName, + vdisk.UniqueId, + ) + + ch <- prometheus.MustNewConstMetric( + c.virtualDiskFootprintOnPool, + prometheus.GaugeValue, + float64(vdisk.FootprintOnPool), + vdisk.FriendlyName, + vdisk.UniqueId, + ) + + // Calculate storage efficiency (avoid division by zero) + var storageEfficiency float64 + if vdisk.FootprintOnPool > 0 { + storageEfficiency = float64(vdisk.Size) / float64(vdisk.FootprintOnPool) * 100 + } else { + storageEfficiency = 0 + } + + ch <- prometheus.MustNewConstMetric( + c.virtualDiskStorageEfficiency, + prometheus.GaugeValue, + storageEfficiency, + vdisk.FriendlyName, + vdisk.UniqueId, + ) + } + + return nil +} diff --git a/internal/mi/types.go b/internal/mi/types.go index 4ef5a8c0..77578af6 100644 --- a/internal/mi/types.go +++ b/internal/mi/types.go @@ -54,6 +54,7 @@ var ( NamespaceRootWebAdministration = utils.Must(NewNamespace("root/WebAdministration")) NamespaceRootMSCluster = utils.Must(NewNamespace("root/MSCluster")) NamespaceRootMicrosoftDNS = utils.Must(NewNamespace("root/MicrosoftDNS")) + NamespaceRootStorage = utils.Must(NewNamespace("root/Microsoft/Windows/Storage")) ) type Query *uint16