// 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 hyperv_test import ( "log/slog" "strings" "testing" "time" "github.com/prometheus-community/windows_exporter/internal/collector/hyperv" "github.com/prometheus-community/windows_exporter/internal/pdh" "github.com/prometheus-community/windows_exporter/internal/utils/testutils" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/require" ) func BenchmarkCollector(b *testing.B) { testutils.FuncBenchmarkCollector(b, hyperv.Name, hyperv.NewWithFlags) } func TestCollector(t *testing.T) { metrics := testutils.TestCollector(t, hyperv.New, nil) testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_dynamic_memory_vm_physical_bytes", prometheus.Labels{"vm": "GitHubActions"}) testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_vm_processors", prometheus.Labels{"vm": "GitHubActions"}) // An empty "Hyper-V DataStore" counter set is tolerated as an optional // instance group, so require a series explicitly (#2328). testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_datastore_fragmentation_ratio", nil) // The fixture VM is not replicated, but every VM has a primary replication relationship. if metric := testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_replica_vm_state", prometheus.Labels{"vm": "GitHubActions", "relationship": "primary"}); metric != nil { require.InDelta(t, 0, metric.GetGauge().GetValue(), 0, "expected replication to be disabled") // vm_id keeps VMs with the same name apart; it is the VM's GUID. require.Regexp(t, `^[0-9A-Fa-f-]{36}$`, labelValue(metric, "vm_id")) } // The PDH objects only list running VMs; vm_state also lists the CIVirtualHardDisk VM, which is never started. if metric := testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_vm_state", prometheus.Labels{"vm": "GitHubActions", "state": "running"}); metric != nil { require.InDelta(t, 1, metric.GetGauge().GetValue(), 0, "expected the GitHubActions VM to be running") require.Regexp(t, `^[0-9A-Fa-f-]{36}$`, labelValue(metric, "vm_id")) } if metric := testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_vm_state", prometheus.Labels{"vm": "CIVirtualHardDisk", "state": "off"}); metric != nil { require.InDelta(t, 1, metric.GetGauge().GetValue(), 0, "expected the CIVirtualHardDisk VM to be off") } // The health query reports failures as 0 instead of an error, so check the value. if metric := testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_wmi_health", nil); metric != nil { require.InDelta(t, 1, metric.GetGauge().GetValue(), 0, "Hyper-V WMI health query failed") } } // TestCollectorVirtualStorageDeviceLatency checks that the latencies are published // as counters in seconds for every device, and that the removed gauges, which // exposed the same raw value in 100ns ticks, are gone. func TestCollectorVirtualStorageDeviceLatency(t *testing.T) { metrics := testutils.TestCollector(t, hyperv.New, &hyperv.Config{CollectorsEnabled: []string{"virtual_storage_device"}}) devices := testutils.MetricValuesByLabel(metrics, "windows_hyperv_virtual_storage_device_throughput_total", "device") for _, name := range []string{ "windows_hyperv_virtual_storage_device_io_latency_seconds_total", "windows_hyperv_virtual_storage_device_lower_io_latency_seconds_total", } { got := testutils.MetricValuesByLabel(metrics, name, "device") require.Len(t, got, len(devices), name) if len(got) > 0 { require.Equal(t, dto.MetricType_COUNTER, metrics[name].GetType()) } } for _, removed := range []string{ "windows_hyperv_virtual_storage_device_latency_seconds", "windows_hyperv_virtual_storage_device_lower_latency_seconds", "windows_hyperv_virtual_storage_device_queue_length", "windows_hyperv_virtual_storage_device_lower_queue_length", } { require.NotContains(t, metrics, removed) } } // TestCollectorLegacyNetworkAdapter checks that the legacy network adapter // metrics are published with the counter type documented in // docs/collector.hyperv.md; bytes_dropped_total used to be published as a // gauge. The CI fixture VM carries a legacy network adapter. func TestCollectorLegacyNetworkAdapter(t *testing.T) { metrics := testutils.TestCollector(t, hyperv.New, &hyperv.Config{CollectorsEnabled: []string{"legacy_network_adapter"}}) for _, name := range []string{ "windows_hyperv_legacy_network_adapter_bytes_dropped_total", "windows_hyperv_legacy_network_adapter_bytes_received_total", "windows_hyperv_legacy_network_adapter_bytes_sent_total", "windows_hyperv_legacy_network_adapter_frames_dropped_total", "windows_hyperv_legacy_network_adapter_frames_received_total", "windows_hyperv_legacy_network_adapter_frames_sent_total", } { testutils.RequireFixtureMetric(t, metrics, hyperv.Name, name, nil) if family, ok := metrics[name]; ok { require.Equal(t, dto.MetricType_COUNTER, family.GetType(), name) } } } // TestCollectorVirtualHardDisk checks the disk of the CI fixture VM, which is // created with New-VM -NewVHDSizeBytes 1GB (a dynamic VHDX). func TestCollectorVirtualHardDisk(t *testing.T) { metrics := testutils.TestCollector(t, hyperv.New, &hyperv.Config{CollectorsEnabled: []string{"virtual_hard_disk", "virtual_storage_device"}}) labels := prometheus.Labels{"vm": "GitHubActions", "type": "dynamic"} // The device label joins the disk with its I/O metrics: the running VM's // disk has exactly one "Hyper-V Virtual Storage Device" instance. if metric := testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_virtual_hard_disk_size_bytes", labels); metric != nil { device := labelValue(metric, "device") require.NotEmpty(t, device, "the running VM's disk has no device") require.Contains(t, testutils.MetricValuesByLabel(metrics, "windows_hyperv_virtual_storage_device_throughput_total", "device"), device, "device %q is not a virtual_storage_device instance", device) } if metric := testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_virtual_hard_disk_size_bytes", labels); metric != nil { require.InDelta(t, 1<<30, metric.GetGauge().GetValue(), 0) } if metric := testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_virtual_hard_disk_file_size_bytes", labels); metric != nil { require.Positive(t, metric.GetGauge().GetValue()) require.Less(t, metric.GetGauge().GetValue(), float64(1<<30)) } // The first refresh runs during Build, so the first scrape has data. if metric := testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_virtual_hard_disk_last_refresh_timestamp_seconds", nil); metric != nil { require.InDelta(t, float64(time.Now().Unix()), metric.GetGauge().GetValue(), 300) } if metric := testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_virtual_hard_disk_read_errors", nil); metric != nil { require.InDelta(t, 0, metric.GetGauge().GetValue(), 0) } } // TestVirtualHardDiskRefreshIntervalMustBePositive checks that an invalid // refresh interval stops the build instead of starting a busy loop. func TestVirtualHardDiskRefreshIntervalMustBePositive(t *testing.T) { t.Parallel() c := hyperv.New(&hyperv.Config{ CollectorsEnabled: []string{"virtual_hard_disk"}, VirtualHardDisk: hyperv.VirtualHardDiskConfig{RefreshInterval: -time.Second}, }) require.ErrorContains(t, c.Build(slog.New(slog.DiscardHandler), nil), "refresh-interval must be positive") require.NoError(t, c.Close()) } // TestCollectorVirtualHardDiskCheckpoint checks the CI fixture VM // CIVirtualHardDisk: a 64 MiB dynamic disk with a checkpoint (so the VM runs // on a differencing .avhdx) and an 8 MiB fixed disk attached after the // checkpoint. Each attached disk must be reported once, the checkpoint's own // settings and the parent disk must not be reported. func TestCollectorVirtualHardDiskCheckpoint(t *testing.T) { metrics := testutils.TestCollector(t, hyperv.New, &hyperv.Config{CollectorsEnabled: []string{"virtual_hard_disk"}}) differencing := testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_virtual_hard_disk_size_bytes", prometheus.Labels{"vm": "CIVirtualHardDisk", "type": "differencing"}) fixed := testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_virtual_hard_disk_size_bytes", prometheus.Labels{"vm": "CIVirtualHardDisk", "type": "fixed"}) if differencing == nil || fixed == nil { return } require.InDelta(t, 64<<20, differencing.GetGauge().GetValue(), 0) require.InDelta(t, 8<<20, fixed.GetGauge().GetValue(), 0) // The VM never runs, so its disks have no I/O counter instance. require.Empty(t, labelValue(differencing, "device")) require.Empty(t, labelValue(fixed, "device")) for _, name := range []string{"windows_hyperv_virtual_hard_disk_size_bytes", "windows_hyperv_virtual_hard_disk_file_size_bytes"} { var paths []string for _, metric := range metrics[name].GetMetric() { labels := make(map[string]string) for _, label := range metric.GetLabel() { labels[label.GetName()] = label.GetValue() } if labels["vm"] == "CIVirtualHardDisk" { paths = append(paths, labels["path"]) } } require.Len(t, paths, 2, "%s: expected the differencing and the fixed disk, got %v", name, paths) for _, path := range paths { require.True(t, strings.HasSuffix(strings.ToLower(path), ".avhdx") || strings.HasSuffix(strings.ToLower(path), "fixed.vhdx"), "%s: unexpected disk %s", name, path) } } } func labelValue(metric *dto.Metric, name string) string { for _, label := range metric.GetLabel() { if label.GetName() == name { return label.GetValue() } } return "" } // TestCloseReleasesQuery ensures Close releases the PDH queries of the sub-collectors. func TestCloseReleasesQuery(t *testing.T) { t.Parallel() c := hyperv.New(&hyperv.Config{CollectorsEnabled: []string{"dynamic_memory_balancer"}}) require.NoError(t, c.Build(slog.New(slog.DiscardHandler), nil)) require.NoError(t, c.Close()) ch := make(chan prometheus.Metric, 100) require.ErrorIs(t, c.Collect(ch, time.Second), pdh.ErrPerformanceCounterNotInitialized) }