diff --git a/docs/collector.hyperv.md b/docs/collector.hyperv.md index 1761cd54..dba0b049 100644 --- a/docs/collector.hyperv.md +++ b/docs/collector.hyperv.md @@ -12,7 +12,7 @@ The hyperv collector exposes metrics about the Hyper-V hypervisor ### `--collectors.hyperv.enabled` Comma-separated list of collectors to use, for example: -`--collectors.hyperv.enabled=dynamic_memory_balancer,dynamic_memory_vm,hypervisor_logical_processor,hypervisor_root_partition,hypervisor_root_virtual_processor,hypervisor_virtual_processor,legacy_network_adapter,virtual_machine_health_summary,virtual_machine_vid_partition,virtual_network_adapter,virtual_storage_device,virtual_switch`. +`--collectors.hyperv.enabled=dynamic_memory_balancer,dynamic_memory_vm,host,hypervisor_logical_processor,hypervisor_root_partition,hypervisor_root_virtual_processor,hypervisor_virtual_processor,legacy_network_adapter,virtual_machine_health_summary,virtual_machine_vid_partition,virtual_network_adapter,virtual_storage_device,virtual_switch`. Matching is case-sensitive. ## Metrics @@ -98,6 +98,28 @@ Some metrics explained: https://learn.microsoft.com/en-us/archive/blogs/chrisavi | `windows_hyperv_dynamic_memory_vm_physical` | Represents the current amount of memory in the VM. | gauge | `vm` | | `windows_hyperv_dynamic_memory_vm_removed_bytes_total` | Represents the cumulative amount of memory removed from the VM. | counter | `vm` | +### Hyper-V Host + +Metrics about the host's CPU allocation. Processor counts per virtual machine are +read from the Host Compute Service API; the logical processor count comes from the +`Hyper-V Hypervisor Logical Processor` performance counter object. + +| Metric Name | Description | Type | Labels | +|-------------------------------------------------|------------------------------------------------------------------------|-------|--------------------| +| `windows_hyperv_host_logical_processor_count` | Number of logical processors on the host | gauge | None | +| `windows_hyperv_vm_processor_count` | Number of virtual processors assigned to the VM | gauge | `vm_id`, `vm` | +| `windows_hyperv_total_vm_processor_count` | Total number of virtual processors assigned to all VMs | gauge | None | + +The CPU allocation ratio can be computed in PromQL. Virtual cores per **logical** host core: + +``` +windows_hyperv_total_vm_processor_count / windows_hyperv_host_logical_processor_count +``` + +Where hyper-threading (SMT) is enabled, the host reports two logical cores per physical core. +Multiply by the number of threads per core to get the ratio per **physical** core. + + ### Hyper-V Hypervisor Logical Processor | Name | Description | Type | Labels | diff --git a/internal/collector/hyperv/hyperv.go b/internal/collector/hyperv/hyperv.go index b686746c..f9f687a6 100644 --- a/internal/collector/hyperv/hyperv.go +++ b/internal/collector/hyperv/hyperv.go @@ -38,6 +38,7 @@ const ( subCollectorDataStore = "datastore" subCollectorDynamicMemoryBalancer = "dynamic_memory_balancer" subCollectorDynamicMemoryVM = "dynamic_memory_vm" + subCollectorHost = "host" subCollectorHypervisorLogicalProcessor = "hypervisor_logical_processor" subCollectorHypervisorRootPartition = "hypervisor_root_partition" subCollectorHypervisorRootVirtualProcessor = "hypervisor_root_virtual_processor" @@ -62,6 +63,7 @@ var ConfigDefaults = Config{ subCollectorDataStore, subCollectorDynamicMemoryBalancer, subCollectorDynamicMemoryVM, + subCollectorHost, subCollectorHypervisorLogicalProcessor, subCollectorHypervisorRootPartition, subCollectorHypervisorRootVirtualProcessor, @@ -82,6 +84,7 @@ type Collector struct { collectorDataStore collectorDynamicMemoryBalancer collectorDynamicMemoryVM + collectorHost collectorHypervisorLogicalProcessor collectorHypervisorRootPartition collectorHypervisorRootVirtualProcessor @@ -183,6 +186,11 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error { collect: c.collectDynamicMemoryVM, close: c.perfDataCollectorDynamicMemoryVM.Close, }, + subCollectorHost: { + build: c.buildHost, + collect: c.collectHost, + close: c.perfDataCollectorLogicalProcessor.Close, + }, subCollectorHypervisorLogicalProcessor: { build: c.buildHypervisorLogicalProcessor, collect: c.collectHypervisorLogicalProcessor, diff --git a/internal/collector/hyperv/hyperv_host.go b/internal/collector/hyperv/hyperv_host.go new file mode 100644 index 00000000..39efbf2e --- /dev/null +++ b/internal/collector/hyperv/hyperv_host.go @@ -0,0 +1,259 @@ +// 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 + +import ( + "encoding/json" + "errors" + "fmt" + "log/slog" + + "github.com/prometheus-community/windows_exporter/internal/headers/hcs" + "github.com/prometheus-community/windows_exporter/internal/pdh" + "github.com/prometheus-community/windows_exporter/internal/types" + "github.com/prometheus/client_golang/prometheus" + "golang.org/x/sys/windows" +) + +// collectorHost Hyper-V Host metrics +type collectorHost struct { + perfDataCollectorLogicalProcessor *pdh.Collector + perfDataObjectLogicalProcessor []perfDataCounterValuesHost + + vmProcessorCount *prometheus.Desc + hostLogicalProcessorCount *prometheus.Desc + totalVMProcessorCount *prometheus.Desc + memoryPropertyQuery *uint16 +} + +type perfDataCounterValuesHost struct { + Name string + + HypervisorLogicalProcessorTotalRunTimePercent float64 `perfdata:"% Total Run Time"` +} + +type vmTopology struct { + ID string + Name string + State string + ProcessorCount int +} + +// vmMemoryProperties is the "Memory" property of a compute system. It is the only +// HCS property type that exposes a virtual machine's processor allocation: every +// other documented type is rejected for VMMS-owned virtual machines. +type vmMemoryProperties struct { + VirtualNodeCount int `json:"VirtualNodeCount,omitempty"` + VirtualNodes []vmVirtualNode `json:"VirtualNodes,omitempty"` +} + +type vmVirtualNode struct { + VirtualNodeIndex int `json:"VirtualNodeIndex"` + PhysicalNodeNumber int `json:"PhysicalNodeNumber"` + VirtualProcessorCount int `json:"VirtualProcessorCount"` +} + +func (c *Collector) buildHost() error { + var err error + + c.perfDataCollectorLogicalProcessor, err = pdh.NewCollector[perfDataCounterValuesHost](c.logger, pdh.CounterTypeRaw, "Hyper-V Hypervisor Logical Processor", pdh.InstancesAll) + if err != nil { + return fmt.Errorf("failed to create Hyper-V Hypervisor Logical Processor collector: %w", err) + } + + c.memoryPropertyQuery, err = windows.UTF16PtrFromString(`{"PropertyTypes":["Memory"]}`) + if err != nil { + return fmt.Errorf("failed to create memory property query: %w", err) + } + + c.vmProcessorCount = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "vm_processor_count"), + "Number of virtual processors assigned to the VM", + []string{"vm_id", "vm"}, + nil, + ) + + c.hostLogicalProcessorCount = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "host_logical_processor_count"), + "Number of logical processors on the host", + nil, + nil, + ) + + c.totalVMProcessorCount = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "total_vm_processor_count"), + "Total number of virtual processors assigned to all VMs", + nil, + nil, + ) + + return nil +} + +func (c *Collector) collectHost(ch chan<- prometheus.Metric) error { + err := c.perfDataCollectorLogicalProcessor.Collect(&c.perfDataObjectLogicalProcessor) + if err != nil { + return fmt.Errorf("failed to collect Hyper-V Hypervisor Logical Processor metrics: %w", err) + } + + logicalCoreCount := float64(len(c.perfDataObjectLogicalProcessor)) + + vms, err := c.getVMsWithProcessorCounts() + if err != nil { + return fmt.Errorf("failed to get VM processor counts: %w", err) + } + + var totalVirtualCoreCount float64 + + for _, vm := range vms { + if vm.ProcessorCount > 0 { + processorCount := float64(vm.ProcessorCount) + totalVirtualCoreCount += processorCount + + ch <- prometheus.MustNewConstMetric( + c.vmProcessorCount, + prometheus.GaugeValue, + processorCount, + vm.ID, vm.Name, + ) + } + } + + ch <- prometheus.MustNewConstMetric( + c.hostLogicalProcessorCount, + prometheus.GaugeValue, + logicalCoreCount, + ) + + ch <- prometheus.MustNewConstMetric( + c.totalVMProcessorCount, + prometheus.GaugeValue, + totalVirtualCoreCount, + ) + + return nil +} + +func (c *Collector) getVMsWithProcessorCounts() ([]vmTopology, error) { + vmQuery, err := windows.UTF16PtrFromString(`{"Types":["VirtualMachine"]}`) + if err != nil { + return nil, fmt.Errorf("failed to create VM query: %w", err) + } + + operation, err := hcs.CreateOperation() + if err != nil { + return nil, fmt.Errorf("failed to create operation: %w", err) + } + defer hcs.CloseOperation(operation) + + if err := hcs.EnumerateComputeSystems(vmQuery, operation); err != nil { + return nil, fmt.Errorf("failed to enumerate compute systems: %w", err) + } + + resultDocument, err := hcs.WaitForOperationResult(operation, 1000) + if err != nil { + return nil, fmt.Errorf("failed to wait for operation result: %w - %s", err, resultDocument) + } else if resultDocument == "" { + return nil, hcs.ErrEmptyResultDocument + } + + var computeSystems []hcs.Properties + if err := json.Unmarshal([]byte(resultDocument), &computeSystems); err != nil { + return nil, fmt.Errorf("failed to unmarshal compute systems: %w", err) + } + + vms := make([]vmTopology, 0, len(computeSystems)) + errs := make([]error, 0) + + for _, system := range computeSystems { + // HcsEnumerateComputeSystems does not populate State for VMMS-owned virtual + // machines, so only skip a system when it reports a state that is not running. + if system.State != "" && system.State != "Running" { + continue + } + + processorCount, err := c.getVMProcessorCount(system.ID) + if err != nil { + errs = append(errs, fmt.Errorf("failed to get processor count for VM %s: %w", system.ID, err)) + + continue + } + + vms = append(vms, vmTopology{ + ID: system.ID, + Name: system.Name, + State: system.State, + ProcessorCount: processorCount, + }) + } + + if len(errs) > 0 { + c.logger.Debug("some VMs failed processor count retrieval", + slog.Any("err", errors.Join(errs...)), + ) + } + + return vms, nil +} + +func (c *Collector) getVMProcessorCount(vmID string) (int, error) { + computeSystem, err := hcs.OpenComputeSystem(vmID) + if err != nil { + return 0, fmt.Errorf("failed to open compute system: %w", err) + } + + defer hcs.CloseComputeSystem(computeSystem) + + operation, err := hcs.CreateOperation() + if err != nil { + return 0, fmt.Errorf("failed to create operation: %w", err) + } + + defer hcs.CloseOperation(operation) + + if err := hcs.GetComputeSystemProperties(computeSystem, operation, c.memoryPropertyQuery); err != nil { + return 0, fmt.Errorf("failed to get compute system properties: %w", err) + } + + resultDocument, err := hcs.WaitForOperationResult(operation, 1000) + if err != nil { + return 0, fmt.Errorf("failed to wait for operation result: %w", err) + } else if resultDocument == "" { + return 0, hcs.ErrEmptyResultDocument + } + + var properties struct { + Memory *vmMemoryProperties `json:"Memory,omitempty"` + } + + if err := json.Unmarshal([]byte(resultDocument), &properties); err != nil { + return 0, fmt.Errorf("failed to unmarshal properties: %w", err) + } + + if properties.Memory == nil { + return 0, nil + } + + // A virtual machine's processors are reported per virtual NUMA node. + processorCount := 0 + for _, node := range properties.Memory.VirtualNodes { + processorCount += node.VirtualProcessorCount + } + + return processorCount, nil +} diff --git a/internal/headers/hcs/types.go b/internal/headers/hcs/types.go index 85eb588b..31267696 100644 --- a/internal/headers/hcs/types.go +++ b/internal/headers/hcs/types.go @@ -36,6 +36,7 @@ type ( type Properties struct { ID string `json:"Id,omitempty"` + Name string `json:"Name,omitempty"` SystemType string `json:"SystemType,omitempty"` Owner string `json:"Owner,omitempty"` State string `json:"State,omitempty"`