diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b6baca3..d432ad90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -391,7 +391,7 @@ jobs: WINDOWS_EXPORTER_TEST_RDP_USER: rdp-ci WINDOWS_TEST_OS: ${{ matrix.os }} # These collectors must work on the fixtures below; missing features are failures. - WINDOWS_EXPORTER_TEST_COLLECTORS: "cache,container,cpu,cpu_info,dhcp,diskdrive,dmi,dns,fsrmquota,hyperv,iis,license,logical_disk,memory,mscluster,msmq,mssql,net,netframework,nps,os,pagefile,physical_disk,printer,process,scheduled_task,service,smb,smbclient,system,tcp,terminal_services,time,udp,update${{ matrix.smtp && ',smtp' || '' }}" + WINDOWS_EXPORTER_TEST_COLLECTORS: "cache,container,cpu,cpu_info,dhcp,diskdrive,dmi,dns,fsrmquota,hyperv,iis,license,logical_disk,memory,mscluster,msmq,mssql,net,netframework,nps,os,pagefile,physical_disk,printer,process,scheduled_task,service,smb,smbclient,storage_spaces,system,tcp,terminal_services,time,udp,update${{ matrix.smtp && ',smtp' || '' }}" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/README.md b/README.md index 2dc5e880..650b64d1 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ A Prometheus exporter for Windows machines. | [smb](docs/collector.smb.md) | SMB Server | | | [smbclient](docs/collector.smbclient.md) | SMB Client | | | [smtp](docs/collector.smtp.md) | IIS SMTP Server | | +| [storage_spaces](docs/collector.storage_spaces.md) | Storage Spaces pools and virtual disks | | | [system](docs/collector.system.md) | System calls | ✓ | | [tcp](docs/collector.tcp.md) | TCP connections | | | [terminal_services](docs/collector.terminal_services.md) | Terminal services (RDS) | | diff --git a/docs/README.md b/docs/README.md index eb57e6f0..c797061f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -44,6 +44,7 @@ Actions workflow publishes default-branch changes to GitHub Pages. - [`smb`](collector.smb.md) - [`smbclient`](collector.smbclient.md) - [`smtp`](collector.smtp.md) +- [`storage_spaces`](collector.storage_spaces.md) - [`system`](collector.system.md) - [`tcp`](collector.tcp.md) - [`terminal_services`](collector.terminal_services.md) diff --git a/docs/collector.mscluster.md b/docs/collector.mscluster.md index cfe35622..ad29555a 100644 --- a/docs/collector.mscluster.md +++ b/docs/collector.mscluster.md @@ -5,14 +5,17 @@ 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`,`MSFT_VirtualDisk`,`MSFT_StoragePool` +Classes | `MSCluster_Cluster`,`MSCluster_Network`,`MSCluster_Node`,`MSCluster_Resource`,`MSCluster_ResourceGroup`,`MSCluster_DiskPartition` Enabled by default? | No +Storage pool and virtual disk metrics (for example of Storage Spaces Direct) are exposed by the +[`storage_spaces`](collector.storage_spaces.md) collector. + ## Flags ### `--collector.mscluster.enabled` Comma-separated list of collectors to use, for example: -`--collector.mscluster.enabled=cluster,network,node,resource,resourcegroup,shared_volumes,virtualdisk,storagepool`. +`--collector.mscluster.enabled=cluster,network,node,resource,resourcegroup,shared_volumes`. Matching is case-sensitive. ## Metrics @@ -178,55 +181,18 @@ 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_allocated_size_bytes` | Allocated size of the virtual disk in bytes (capacity actually provisioned, excludes thin-provisioned unused capacity) | 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 (AllocatedSize / FootprintOnPool * 100) | gauge | `name`, `unique_id` | - -### Storage Pool - -| Name | Description | Type | Labels | -|-----------------------------------------------|-------------------------------------------------------------------------------------|-------|---------------------| -| `mscluster_storagepool_info` | Storage pool information (value is always 1) | gauge | `name`, `unique_id` | -| `mscluster_storagepool_health_status` | Health status of the storage pool. 0: Healthy, 1: Warning, 2: Unhealthy, 5: Unknown | gauge | `name`, `unique_id` | -| `mscluster_storagepool_size_bytes` | Total size of the storage pool in bytes | gauge | `name`, `unique_id` | -| `mscluster_storagepool_allocated_size_bytes` | Allocated size of the storage pool in bytes | gauge | `name`, `unique_id` | -| `mscluster_storagepool_operational_status` | Operational status codes reported for the storage pool (one series per status value) | gauge | `name`, `unique_id`, `status` | -| `mscluster_storagepool_thin_provisioning_alert_thresholds` | Thin provisioning alert thresholds configured for the storage pool, in percent (one series per configured threshold) | gauge | `name`, `unique_id`, `threshold` | - ### 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 diff --git a/docs/collector.storage_spaces.md b/docs/collector.storage_spaces.md new file mode 100644 index 00000000..5017f5bb --- /dev/null +++ b/docs/collector.storage_spaces.md @@ -0,0 +1,76 @@ +# storage_spaces collector + +The storage_spaces collector exposes metrics about Storage Spaces storage pools and virtual disks. +It works on standalone hosts as well as on failover clusters using Storage Spaces Direct. + +||| +-|- +Metric name prefix | `storage_spaces` +Classes | [`MSFT_StoragePool`](https://learn.microsoft.com/en-us/windows-hardware/drivers/storage/msft-storagepool), [`MSFT_VirtualDisk`](https://learn.microsoft.com/en-us/windows-hardware/drivers/storage/msft-virtualdisk) +Enabled by default? | No + +## Flags + +### `--collector.storage_spaces.enabled` +Comma-separated list of collectors to use, for example: +`--collector.storage_spaces.enabled=pool,virtual_disk`. +Matching is case-sensitive. + +## Metrics + +### Pool + +| Name | Description | Type | Labels | +|------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|-------|----------------------------------| +| `windows_storage_spaces_pool_info` | Storage pool information (value is always 1) | gauge | `name`, `unique_id` | +| `windows_storage_spaces_pool_health_status` | Health status of the storage pool. 0: Healthy, 1: Warning, 2: Unhealthy, 5: Unknown | gauge | `name`, `unique_id` | +| `windows_storage_spaces_pool_size_bytes` | Total size of the storage pool in bytes | gauge | `name`, `unique_id` | +| `windows_storage_spaces_pool_allocated_size_bytes` | Allocated size of the storage pool in bytes | gauge | `name`, `unique_id` | +| `windows_storage_spaces_pool_operational_status` | Operational status codes reported for the storage pool (one series per status value) | gauge | `name`, `unique_id`, `status` | +| `windows_storage_spaces_pool_thin_provisioning_alert_thresholds` | Thin provisioning alert thresholds configured for the storage pool, in percent (one series per configured threshold) | gauge | `name`, `unique_id`, `threshold` | + +### Virtual Disk + +| Name | Description | Type | Labels | +|------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|-------|---------------------| +| `windows_storage_spaces_virtual_disk_info` | Virtual disk information (value is always 1) | gauge | `name`, `unique_id` | +| `windows_storage_spaces_virtual_disk_health_status` | Health status of the virtual disk. 0: Healthy, 1: Warning, 2: Unhealthy, 5: Unknown | gauge | `name`, `unique_id` | +| `windows_storage_spaces_virtual_disk_size_bytes` | Total size of the virtual disk in bytes | gauge | `name`, `unique_id` | +| `windows_storage_spaces_virtual_disk_allocated_size_bytes` | Allocated size of the virtual disk in bytes (capacity actually provisioned, excludes thin-provisioned unused capacity) | gauge | `name`, `unique_id` | +| `windows_storage_spaces_virtual_disk_footprint_on_pool_bytes` | Physical storage consumed by the virtual disk on the storage pool in bytes | gauge | `name`, `unique_id` | +| `windows_storage_spaces_virtual_disk_storage_efficiency_percent` | Storage efficiency percentage (AllocatedSize / FootprintOnPool * 100), omitted while FootprintOnPool is 0 | gauge | `name`, `unique_id` | + +### Example metric + +`windows_storage_spaces_pool_size_bytes{name="Pool01",unique_id="{8f2c...}"} 1.0995116277760e+12` + +## Useful queries + +Storage pool usage in percent +``` +windows_storage_spaces_pool_allocated_size_bytes / windows_storage_spaces_pool_size_bytes * 100 +``` + +Find virtual disks with low storage efficiency (over-provisioned) +``` +windows_storage_spaces_virtual_disk_storage_efficiency_percent < 50 +``` + +Calculate total virtual disk capacity vs physical usage +``` +sum(windows_storage_spaces_virtual_disk_size_bytes) / sum(windows_storage_spaces_virtual_disk_footprint_on_pool_bytes) * 100 +``` + +## Alerting examples + +#### Unhealthy storage pool +```yaml +- alert: StoragePoolUnhealthy + expr: windows_storage_spaces_pool_health_status != 0 + for: 10m + labels: + severity: warning + annotations: + summary: "Storage pool {{ $labels.name }} is not healthy" + description: "Storage pool {{ $labels.name }} on {{ $labels.instance }} reports health status {{ $value }}." +``` diff --git a/internal/collector/lifecycle_test.go b/internal/collector/lifecycle_test.go index 97c59b9f..7be77fc5 100644 --- a/internal/collector/lifecycle_test.go +++ b/internal/collector/lifecycle_test.go @@ -38,6 +38,7 @@ import ( "github.com/prometheus-community/windows_exporter/internal/collector/mssql" "github.com/prometheus-community/windows_exporter/internal/collector/net" "github.com/prometheus-community/windows_exporter/internal/collector/netframework" + "github.com/prometheus-community/windows_exporter/internal/collector/storage_spaces" "github.com/prometheus-community/windows_exporter/internal/collector/tcp" "github.com/prometheus-community/windows_exporter/internal/collector/update" "github.com/prometheus-community/windows_exporter/internal/mi" @@ -155,6 +156,7 @@ func TestBuildRejectsUnknownSubCollector(t *testing.T) { mssql.New(&mssql.Config{CollectorsEnabled: unknown}), net.New(&net.Config{CollectorsEnabled: unknown}), netframework.New(&netframework.Config{CollectorsEnabled: unknown}), + storage_spaces.New(&storage_spaces.Config{CollectorsEnabled: unknown}), tcp.New(&tcp.Config{CollectorsEnabled: unknown}), } { t.Run(c.GetName(), func(t *testing.T) { diff --git a/internal/collector/mscluster/mscluster.go b/internal/collector/mscluster/mscluster.go index d16e92a0..9e8eebc9 100644 --- a/internal/collector/mscluster/mscluster.go +++ b/internal/collector/mscluster/mscluster.go @@ -40,8 +40,6 @@ const ( subCollectorResource = "resource" subCollectorResourceGroup = "resourcegroup" subCollectorSharedVolumes = "shared_volumes" - subCollectorVirtualDisk = "virtualdisk" - subCollectorStoragePool = "storagepool" ) type Config struct { @@ -57,8 +55,6 @@ var ConfigDefaults = Config{ subCollectorResource, subCollectorResourceGroup, subCollectorSharedVolumes, - subCollectorVirtualDisk, - subCollectorStoragePool, }, } @@ -70,8 +66,6 @@ type Collector struct { collectorResource collectorResourceGroup collectorSharedVolumes - collectorVirtualDisk - collectorStoragePool config Config miSession *mi.Session @@ -135,8 +129,6 @@ func (c *Collector) Build(_ *slog.Logger, miSession *mi.Session) error { subCollectorResource, subCollectorResourceGroup, subCollectorSharedVolumes, - subCollectorVirtualDisk, - subCollectorStoragePool, } for _, name := range c.config.CollectorsEnabled { @@ -191,18 +183,6 @@ 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)) - } - } - - if slices.Contains(c.config.CollectorsEnabled, subCollectorStoragePool) { - if err := c.buildStoragePool(); err != nil { - errs = append(errs, fmt.Errorf("failed to build storagepool collector: %w", err)) - } - } - return errors.Join(errs...) } @@ -282,25 +262,5 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time. }) } - if slices.Contains(c.config.CollectorsEnabled, subCollectorVirtualDisk) { - g.Go(func() error { - if err := c.collectVirtualDisk(ch, maxScrapeDuration); err != nil { - return fmt.Errorf("failed to collect virtualdisk metrics: %w", err) - } - - return nil - }) - } - - if slices.Contains(c.config.CollectorsEnabled, subCollectorStoragePool) { - g.Go(func() error { - if err := c.collectStoragePool(ch, maxScrapeDuration); err != nil { - return fmt.Errorf("failed to collect storagepool metrics: %w", err) - } - - return nil - }) - } - return g.Wait() } diff --git a/internal/collector/mscluster/mscluster_test.go b/internal/collector/mscluster/mscluster_test.go index 93d9e966..64b41d7d 100644 --- a/internal/collector/mscluster/mscluster_test.go +++ b/internal/collector/mscluster/mscluster_test.go @@ -22,7 +22,6 @@ import ( "github.com/prometheus-community/windows_exporter/internal/collector/mscluster" "github.com/prometheus-community/windows_exporter/internal/utils/testutils" - "github.com/prometheus/client_golang/prometheus" ) func BenchmarkCollector(b *testing.B) { @@ -30,7 +29,5 @@ func BenchmarkCollector(b *testing.B) { } func TestCollector(t *testing.T) { - metrics := testutils.TestCollector(t, mscluster.New, nil) - testutils.RequireFixtureMetric(t, metrics, mscluster.Name, "windows_mscluster_virtualdisk_size_bytes", prometheus.Labels{"name": "CIVirtualDisk"}) - testutils.RequireFixtureMetric(t, metrics, mscluster.Name, "windows_mscluster_storagepool_size_bytes", prometheus.Labels{"name": "GitHubActions"}) + testutils.TestCollector(t, mscluster.New, nil) } diff --git a/internal/collector/storage_spaces/storage_spaces.go b/internal/collector/storage_spaces/storage_spaces.go new file mode 100644 index 00000000..89dc2118 --- /dev/null +++ b/internal/collector/storage_spaces/storage_spaces.go @@ -0,0 +1,180 @@ +// 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 storage_spaces + +import ( + "errors" + "fmt" + "log/slog" + "slices" + "strings" + "time" + + "github.com/alecthomas/kingpin/v2" + "github.com/prometheus-community/windows_exporter/internal/mi" + "github.com/prometheus-community/windows_exporter/internal/utils/recovery" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + Name = "storage_spaces" + + subCollectorPool = "pool" + subCollectorVirtualDisk = "virtual_disk" +) + +type Config struct { + CollectorsEnabled []string `yaml:"enabled"` +} + +//nolint:gochecknoglobals +var ConfigDefaults = Config{ + CollectorsEnabled: []string{ + subCollectorPool, + subCollectorVirtualDisk, + }, +} + +// A Collector is a Prometheus Collector for the Storage Spaces WMI classes +// MSFT_StoragePool and MSFT_VirtualDisk. +type Collector struct { + collectorStoragePool + collectorVirtualDisk + + config Config + miSession *mi.Session +} + +func New(config *Config) *Collector { + if config == nil { + config = &ConfigDefaults + } + + if config.CollectorsEnabled == nil { + config.CollectorsEnabled = ConfigDefaults.CollectorsEnabled + } + + c := &Collector{ + config: *config, + } + + return c +} + +func NewWithFlags(app *kingpin.Application) *Collector { + c := &Collector{ + config: ConfigDefaults, + } + c.config.CollectorsEnabled = make([]string, 0) + + var collectorsEnabled string + + app.Flag( + "collector.storage_spaces.enabled", + "Comma-separated list of collectors to use.", + ).Default(strings.Join(ConfigDefaults.CollectorsEnabled, ",")).StringVar(&collectorsEnabled) + + app.Action(func(*kingpin.ParseContext) error { + c.config.CollectorsEnabled = strings.Split(collectorsEnabled, ",") + + return nil + }) + + return c +} + +func (c *Collector) GetName() string { + return Name +} + +func (c *Collector) Close() error { + return nil +} + +func (c *Collector) Build(_ *slog.Logger, miSession *mi.Session) error { + if len(c.config.CollectorsEnabled) == 0 { + return nil + } + + subCollectors := []string{ + subCollectorPool, + subCollectorVirtualDisk, + } + + for _, name := range c.config.CollectorsEnabled { + if !slices.Contains(subCollectors, name) { + return fmt.Errorf("unknown sub collector: %s. Possible values: %s", name, + strings.Join(subCollectors, ", "), + ) + } + } + + if miSession == nil { + return errors.New("miSession is nil") + } + + c.miSession = miSession + + errs := make([]error, 0) + + if slices.Contains(c.config.CollectorsEnabled, subCollectorPool) { + if err := c.buildStoragePool(); err != nil { + errs = append(errs, fmt.Errorf("failed to build pool collector: %w", err)) + } + } + + if slices.Contains(c.config.CollectorsEnabled, subCollectorVirtualDisk) { + if err := c.buildVirtualDisk(); err != nil { + errs = append(errs, fmt.Errorf("failed to build virtual_disk collector: %w", err)) + } + } + + return errors.Join(errs...) +} + +// Collect sends the metric values for each metric +// to the provided prometheus Metric channel. +func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error { + if len(c.config.CollectorsEnabled) == 0 { + return nil + } + + var g recovery.Group + + if slices.Contains(c.config.CollectorsEnabled, subCollectorPool) { + g.Go(func() error { + if err := c.collectStoragePool(ch, maxScrapeDuration); err != nil { + return fmt.Errorf("failed to collect pool metrics: %w", err) + } + + return nil + }) + } + + if slices.Contains(c.config.CollectorsEnabled, subCollectorVirtualDisk) { + g.Go(func() error { + if err := c.collectVirtualDisk(ch, maxScrapeDuration); err != nil { + return fmt.Errorf("failed to collect virtual_disk metrics: %w", err) + } + + return nil + }) + } + + return g.Wait() +} diff --git a/internal/collector/mscluster/mscluster_storagepool.go b/internal/collector/storage_spaces/storage_spaces_pool.go similarity index 97% rename from internal/collector/mscluster/mscluster_storagepool.go rename to internal/collector/storage_spaces/storage_spaces_pool.go index c3a124d8..9e56fe5a 100644 --- a/internal/collector/mscluster/mscluster_storagepool.go +++ b/internal/collector/storage_spaces/storage_spaces_pool.go @@ -15,7 +15,7 @@ //go:build windows -package mscluster +package storage_spaces import ( "fmt" @@ -27,7 +27,7 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -const nameStoragePool = Name + "_storagepool" +const nameStoragePool = Name + "_pool" type collectorStoragePool struct { storagePoolMIQuery mi.Query @@ -92,14 +92,14 @@ func (c *Collector) buildStoragePool() error { c.storagePoolOperationalStatus = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, nameStoragePool, "operational_status"), - "Operational status codes reported for the storage pool (one series per status value).", + "Operational status codes reported for the storage pool (one series per status value)", []string{"name", "unique_id", "status"}, nil, ) c.storagePoolThinProvisioningAlertThresholds = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, nameStoragePool, "thin_provisioning_alert_thresholds"), - "Thin provisioning alert thresholds configured for the storage pool, in percent (one series per configured threshold).", + "Thin provisioning alert thresholds configured for the storage pool, in percent (one series per configured threshold)", []string{"name", "unique_id", "threshold"}, nil, ) diff --git a/internal/collector/storage_spaces/storage_spaces_test.go b/internal/collector/storage_spaces/storage_spaces_test.go new file mode 100644 index 00000000..f669067e --- /dev/null +++ b/internal/collector/storage_spaces/storage_spaces_test.go @@ -0,0 +1,69 @@ +// 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 storage_spaces_test + +import ( + "testing" + + "github.com/prometheus-community/windows_exporter/internal/collector/storage_spaces" + "github.com/prometheus-community/windows_exporter/internal/utils/testutils" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" +) + +func BenchmarkCollector(b *testing.B) { + testutils.FuncBenchmarkCollector(b, storage_spaces.Name, storage_spaces.NewWithFlags) +} + +func TestCollector(t *testing.T) { + metrics := testutils.TestCollector(t, storage_spaces.New, nil) + + pool := prometheus.Labels{"name": "GitHubActions"} + virtualDisk := prometheus.Labels{"name": "CIVirtualDisk"} + + testutils.RequireFixtureMetric(t, metrics, storage_spaces.Name, "windows_storage_spaces_pool_info", pool) + testutils.RequireFixtureMetric(t, metrics, storage_spaces.Name, "windows_storage_spaces_virtual_disk_info", virtualDisk) + + for _, tc := range []struct { + metric string + labels prometheus.Labels + }{ + {"windows_storage_spaces_pool_health_status", pool}, + {"windows_storage_spaces_virtual_disk_health_status", virtualDisk}, + } { + if metric := testutils.RequireFixtureMetric(t, metrics, storage_spaces.Name, tc.metric, tc.labels); metric != nil { + require.InDelta(t, 0, metric.GetGauge().GetValue(), 0, "%s: expected healthy", tc.metric) + } + } + + // The fixture pool consists of two 10 GiB disks. + if metric := testutils.RequireFixtureMetric(t, metrics, storage_spaces.Name, "windows_storage_spaces_pool_size_bytes", pool); metric != nil { + require.Greater(t, metric.GetGauge().GetValue(), float64(10<<30)) + } + + // Storage Spaces rounds the requested 1 GiB up to whole slabs per column. + if metric := testutils.RequireFixtureMetric(t, metrics, storage_spaces.Name, "windows_storage_spaces_virtual_disk_size_bytes", virtualDisk); metric != nil { + require.GreaterOrEqual(t, metric.GetGauge().GetValue(), float64(1<<30)) + } + + if metric := testutils.RequireFixtureMetric(t, metrics, storage_spaces.Name, "windows_storage_spaces_virtual_disk_storage_efficiency_percent", virtualDisk); metric != nil { + value := metric.GetGauge().GetValue() + require.Greater(t, value, 0.0) + require.LessOrEqual(t, value, 100.0) + } +} diff --git a/internal/collector/mscluster/mscluster_virtualdisk.go b/internal/collector/storage_spaces/storage_spaces_virtual_disk.go similarity index 88% rename from internal/collector/mscluster/mscluster_virtualdisk.go rename to internal/collector/storage_spaces/storage_spaces_virtual_disk.go index 5788e209..930ce88e 100644 --- a/internal/collector/mscluster/mscluster_virtualdisk.go +++ b/internal/collector/storage_spaces/storage_spaces_virtual_disk.go @@ -15,7 +15,7 @@ //go:build windows -package mscluster +package storage_spaces import ( "fmt" @@ -26,7 +26,7 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -const nameVirtualDisk = Name + "_virtualdisk" +const nameVirtualDisk = Name + "_virtual_disk" type collectorVirtualDisk struct { virtualDiskMIQuery mi.Query @@ -62,7 +62,7 @@ func (c *Collector) buildVirtualDisk() error { c.virtualDiskInfo = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, nameVirtualDisk, "info"), - "Virtual Disk information (value is always 1)", + "Virtual disk information (value is always 1)", []string{"name", "unique_id"}, nil, ) @@ -83,7 +83,7 @@ func (c *Collector) buildVirtualDisk() error { c.virtualDiskAllocatedSize = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, nameVirtualDisk, "allocated_size_bytes"), - "Allocated size of the virtual disk in bytes (data actually provisioned, excludes thin-provisioned unused capacity)", + "Allocated size of the virtual disk in bytes (capacity actually provisioned, excludes thin-provisioned unused capacity)", []string{"name", "unique_id"}, nil, ) @@ -97,7 +97,7 @@ func (c *Collector) buildVirtualDisk() error { c.virtualDiskStorageEfficiency = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, nameVirtualDisk, "storage_efficiency_percent"), - "Storage efficiency percentage (AllocatedSize / FootprintOnPool * 100)", + "Storage efficiency percentage (AllocatedSize / FootprintOnPool * 100), omitted while FootprintOnPool is 0", []string{"name", "unique_id"}, nil, ) @@ -159,25 +159,21 @@ func (c *Collector) collectVirtualDisk(ch chan<- prometheus.Metric, maxScrapeDur vdisk.UniqueId, ) - // Calculate storage efficiency (avoid division by zero). // AllocatedSize (not Size) is the correct numerator: Size is the // thin-provisioned ceiling, while AllocatedSize reflects the capacity // actually provisioned. Using Size inflates the ratio on thin-provisioned // disks (e.g. S2D FTT2 CSVs). - var storageEfficiency float64 + // Without a footprint the ratio is undefined; skip it instead of + // publishing 0. if vdisk.FootprintOnPool > 0 { - storageEfficiency = float64(vdisk.AllocatedSize) / float64(vdisk.FootprintOnPool) * 100 - } else { - storageEfficiency = 0 + ch <- prometheus.MustNewConstMetric( + c.virtualDiskStorageEfficiency, + prometheus.GaugeValue, + float64(vdisk.AllocatedSize)/float64(vdisk.FootprintOnPool)*100, + vdisk.FriendlyName, + vdisk.UniqueId, + ) } - - ch <- prometheus.MustNewConstMetric( - c.virtualDiskStorageEfficiency, - prometheus.GaugeValue, - storageEfficiency, - vdisk.FriendlyName, - vdisk.UniqueId, - ) } return nil diff --git a/pkg/collector/collection.go b/pkg/collector/collection.go index 7ffa4793..a1ce09df 100644 --- a/pkg/collector/collection.go +++ b/pkg/collector/collection.go @@ -68,6 +68,7 @@ import ( "github.com/prometheus-community/windows_exporter/internal/collector/smb" "github.com/prometheus-community/windows_exporter/internal/collector/smbclient" "github.com/prometheus-community/windows_exporter/internal/collector/smtp" + "github.com/prometheus-community/windows_exporter/internal/collector/storage_spaces" "github.com/prometheus-community/windows_exporter/internal/collector/system" "github.com/prometheus-community/windows_exporter/internal/collector/tcp" "github.com/prometheus-community/windows_exporter/internal/collector/terminal_services" @@ -141,6 +142,7 @@ func NewWithConfig(config Config) *Collection { collectors[smb.Name] = smb.New(&config.SMB) collectors[smbclient.Name] = smbclient.New(&config.SMBClient) collectors[smtp.Name] = smtp.New(&config.SMTP) + collectors[storage_spaces.Name] = storage_spaces.New(&config.StorageSpaces) collectors[system.Name] = system.New(&config.System) collectors[tcp.Name] = tcp.New(&config.TCP) collectors[terminal_services.Name] = terminal_services.New(&config.TerminalServices) diff --git a/pkg/collector/config.go b/pkg/collector/config.go index 1aa60300..be1f790d 100644 --- a/pkg/collector/config.go +++ b/pkg/collector/config.go @@ -58,6 +58,7 @@ import ( "github.com/prometheus-community/windows_exporter/internal/collector/smb" "github.com/prometheus-community/windows_exporter/internal/collector/smbclient" "github.com/prometheus-community/windows_exporter/internal/collector/smtp" + "github.com/prometheus-community/windows_exporter/internal/collector/storage_spaces" "github.com/prometheus-community/windows_exporter/internal/collector/system" "github.com/prometheus-community/windows_exporter/internal/collector/tcp" "github.com/prometheus-community/windows_exporter/internal/collector/terminal_services" @@ -111,6 +112,7 @@ type Config struct { SMB smb.Config `yaml:"smb"` SMBClient smbclient.Config `yaml:"smb_client"` SMTP smtp.Config `yaml:"smtp"` + StorageSpaces storage_spaces.Config `yaml:"storage_spaces"` System system.Config `yaml:"system"` TCP tcp.Config `yaml:"tcp"` TerminalServices terminal_services.Config `yaml:"terminal_services"` @@ -168,6 +170,7 @@ var ConfigDefaults = Config{ SMB: smb.ConfigDefaults, SMBClient: smbclient.ConfigDefaults, SMTP: smtp.ConfigDefaults, + StorageSpaces: storage_spaces.ConfigDefaults, System: system.ConfigDefaults, TCP: tcp.ConfigDefaults, TerminalServices: terminal_services.ConfigDefaults, diff --git a/pkg/collector/map.go b/pkg/collector/map.go index 5f4e90aa..c79965fc 100644 --- a/pkg/collector/map.go +++ b/pkg/collector/map.go @@ -62,6 +62,7 @@ import ( "github.com/prometheus-community/windows_exporter/internal/collector/smb" "github.com/prometheus-community/windows_exporter/internal/collector/smbclient" "github.com/prometheus-community/windows_exporter/internal/collector/smtp" + "github.com/prometheus-community/windows_exporter/internal/collector/storage_spaces" "github.com/prometheus-community/windows_exporter/internal/collector/system" "github.com/prometheus-community/windows_exporter/internal/collector/tcp" "github.com/prometheus-community/windows_exporter/internal/collector/terminal_services" @@ -122,6 +123,7 @@ var BuildersWithFlags = map[string]BuilderWithFlags[Collector]{ smb.Name: NewBuilderWithFlags(smb.NewWithFlags), smbclient.Name: NewBuilderWithFlags(smbclient.NewWithFlags), smtp.Name: NewBuilderWithFlags(smtp.NewWithFlags), + storage_spaces.Name: NewBuilderWithFlags(storage_spaces.NewWithFlags), system.Name: NewBuilderWithFlags(system.NewWithFlags), tcp.Name: NewBuilderWithFlags(tcp.NewWithFlags), terminal_services.Name: NewBuilderWithFlags(terminal_services.NewWithFlags),