// 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 ( "errors" "fmt" "log/slog" "strings" "time" "github.com/prometheus-community/windows_exporter/internal/labelcache" "github.com/prometheus-community/windows_exporter/internal/mi" "github.com/prometheus-community/windows_exporter/internal/pdh" "github.com/prometheus-community/windows_exporter/internal/types" "github.com/prometheus/client_golang/prometheus" ) // replicaVMQueryTimeout bounds the replication relationship query, so a hung // WMI service does not block the scrape. const replicaVMQueryTimeout = 5 * time.Second // collectorReplicaVM Hyper-V Replica VM metrics type collectorReplicaVM struct { perfDataCollectorReplicaVM *pdh.Collector[perfDataCounterValuesReplicaVM] perfDataObjectReplicaVM []perfDataCounterValuesReplicaVM miQueryReplicaVM mi.Query replicaVMAverageReplicationLatency *prometheus.Desc // \Hyper-V Replica VM(*)\Average Replication Latency replicaVMAverageReplicationSize *prometheus.Desc // \Hyper-V Replica VM(*)\Average Replication Size replicaVMCompressionEfficiency *prometheus.Desc // \Hyper-V Replica VM(*)\Compression Efficiency replicaVMLastReplicationSize *prometheus.Desc // \Hyper-V Replica VM(*)\Last Replication Size replicaVMNetworkReceivedBytes *prometheus.Desc // \Hyper-V Replica VM(*)\Network Bytes Recv replicaVMNetworkSentBytes *prometheus.Desc // \Hyper-V Replica VM(*)\Network Bytes Sent replicaVMReplicationCount *prometheus.Desc // \Hyper-V Replica VM(*)\Replication Count replicaVMReplicationLatency *prometheus.Desc // \Hyper-V Replica VM(*)\Replication Latency replicaVMResynchronizedBytes *prometheus.Desc // \Hyper-V Replica VM(*)\Resynchronized Bytes replicaVMHealth *prometheus.Desc // Msvm_ReplicationRelationship.ReplicationHealth replicaVMState *prometheus.Desc // Msvm_ReplicationRelationship.ReplicationState } type perfDataCounterValuesReplicaVM struct { Name string Occurrence int AverageReplicationLatency float64 `perfdata:"Average Replication Latency"` AverageReplicationSize float64 `perfdata:"Average Replication Size"` CompressionEfficiency float64 `perfdata:"Compression Efficiency"` LastReplicationSize float64 `perfdata:"Last Replication Size"` NetworkBytesRecv float64 `perfdata:"Network Bytes Recv"` NetworkBytesSent float64 `perfdata:"Network Bytes Sent"` ReplicationCount float64 `perfdata:"Replication Count"` ReplicationLatency float64 `perfdata:"Replication Latency"` ResynchronizedBytes float64 `perfdata:"Resynchronized Bytes"` } // msvmReplicationRelationship is the Msvm_ReplicationRelationship WMI class. // https://learn.microsoft.com/en-us/windows/win32/hyperv_v2/msvm-replicationrelationship type msvmReplicationRelationship struct { ElementName string `mi:"ElementName"` InstanceID string `mi:"InstanceID"` ReplicationHealth uint16 `mi:"ReplicationHealth"` ReplicationState uint16 `mi:"ReplicationState"` } func (c *Collector) buildReplicaVM() error { if c.miSession == nil { return mi.ErrNotInitialized } var err error // The Hyper-V Replica VM counter set is missing on hosts without Hyper-V Replica // support, e.g. Windows client editions. Depending on the system, this surfaces // here as a missing object or on collect as no data. The WMI metrics still work there. c.perfDataCollectorReplicaVM, err = pdh.NewCollector[perfDataCounterValuesReplicaVM](c.logger, pdh.CounterTypeRaw, "Hyper-V Replica VM", pdh.InstancesAll) if errors.Is(err, pdh.NewPdhError(pdh.CstatusNoObject)) { c.logger.Debug("Hyper-V Replica VM performance counters are not available", slog.Any("err", err)) c.perfDataCollectorReplicaVM = nil } else if err != nil { return fmt.Errorf("failed to create Hyper-V Replica VM collector: %w", err) } c.miQueryReplicaVM, err = mi.NewQuery("SELECT ElementName, InstanceID, ReplicationHealth, ReplicationState FROM Msvm_ReplicationRelationship") if err != nil { return fmt.Errorf("failed to create WMI query: %w", err) } c.replicaVMAverageReplicationLatency = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "replica_vm_average_replication_latency_seconds"), "The average time taken to replicate the virtual machine", []string{"vm"}, nil, ) c.replicaVMAverageReplicationSize = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "replica_vm_average_replication_size_bytes"), "The average size of the replicated data", []string{"vm"}, nil, ) c.replicaVMCompressionEfficiency = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "replica_vm_compression_efficiency"), "The compression efficiency of the replicated data", []string{"vm"}, nil, ) c.replicaVMLastReplicationSize = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "replica_vm_last_replication_size_bytes"), "The size of the last replicated data", []string{"vm"}, nil, ) c.replicaVMNetworkReceivedBytes = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "replica_vm_network_received_bytes_total"), "The total number of bytes received over the network for replication", []string{"vm"}, nil, ) c.replicaVMNetworkSentBytes = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "replica_vm_network_sent_bytes_total"), "The total number of bytes sent over the network for replication", []string{"vm"}, nil, ) c.replicaVMReplicationCount = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "replica_vm_replications_total"), "The total number of replication cycles", []string{"vm"}, nil, ) c.replicaVMReplicationLatency = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "replica_vm_replication_latency_seconds"), "The time taken by the last replication of the virtual machine", []string{"vm"}, nil, ) c.replicaVMResynchronizedBytes = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "replica_vm_resynchronized_bytes_total"), "The total number of bytes sent during resynchronization", []string{"vm"}, nil, ) c.replicaVMHealth = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "replica_vm_health"), "The replication health of the virtual machine. 0: Not applicable; 1: OK; 2: Warning; 3: Critical", []string{"vm", "relationship"}, nil, ) c.replicaVMState = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "replica_vm_state"), "The replication state of the virtual machine. 0: Disabled; 1: Ready for replication; 2: Waiting to complete initial replication; "+ "3: Replicating; 4: Synced replication complete; 5: Recovered; 6: Committed; 7: Suspended; 8: Critical; "+ "9: Waiting to start resynchronization; 10: Resynchronizing; 11: Resynchronization suspended; 12: Failover in progress; "+ "13: Failback in progress; 14: Failback complete; 15: Disk update in progress; 16: Disk update critical; 17: Unknown; "+ "18: Repurpose replication in progress; 19: Prepared for sync replication; 20: Prepared for group reverse replication; "+ "21: Fire drill in progress", []string{"vm", "relationship"}, nil, ) return nil } func (c *Collector) collectReplicaVM(ch chan<- prometheus.Metric, scrape *labelcache.Scrape) error { return errors.Join( c.collectReplicaVMPerfData(ch, scrape), c.collectReplicaVMRelationships(ch, scrape), ) } func (c *Collector) collectReplicaVMPerfData(ch chan<- prometheus.Metric, scrape *labelcache.Scrape) error { if c.perfDataCollectorReplicaVM == nil { return nil } err := c.perfDataCollectorReplicaVM.Collect(&c.perfDataObjectReplicaVM) if errors.Is(err, pdh.ErrNoData) { // No virtual machine is replicated, or Hyper-V Replica is not supported. return nil } else if err != nil { return fmt.Errorf("failed to collect Hyper-V Replica VM metrics: %w", err) } suffixDuplicateVMs(c.perfDataObjectReplicaVM, func(row *perfDataCounterValuesReplicaVM) *string { return &row.Name }, func(row *perfDataCounterValuesReplicaVM) int { return row.Occurrence }, ) for _, data := range c.perfDataObjectReplicaVM { ch <- scrape.MustNewConstMetric( c.replicaVMAverageReplicationLatency, prometheus.GaugeValue, data.AverageReplicationLatency, data.Name, ) ch <- scrape.MustNewConstMetric( c.replicaVMAverageReplicationSize, prometheus.GaugeValue, data.AverageReplicationSize, data.Name, ) ch <- scrape.MustNewConstMetric( c.replicaVMCompressionEfficiency, prometheus.GaugeValue, data.CompressionEfficiency, data.Name, ) ch <- scrape.MustNewConstMetric( c.replicaVMLastReplicationSize, prometheus.GaugeValue, data.LastReplicationSize, data.Name, ) ch <- scrape.MustNewConstMetric( c.replicaVMNetworkReceivedBytes, prometheus.CounterValue, data.NetworkBytesRecv, data.Name, ) ch <- scrape.MustNewConstMetric( c.replicaVMNetworkSentBytes, prometheus.CounterValue, data.NetworkBytesSent, data.Name, ) ch <- scrape.MustNewConstMetric( c.replicaVMReplicationCount, prometheus.CounterValue, data.ReplicationCount, data.Name, ) ch <- scrape.MustNewConstMetric( c.replicaVMReplicationLatency, prometheus.GaugeValue, data.ReplicationLatency, data.Name, ) ch <- scrape.MustNewConstMetric( c.replicaVMResynchronizedBytes, prometheus.CounterValue, data.ResynchronizedBytes, data.Name, ) } return nil } func (c *Collector) collectReplicaVMRelationships(ch chan<- prometheus.Metric, scrape *labelcache.Scrape) error { var dst []msvmReplicationRelationship if err := c.miSession.Query(&dst, mi.NamespaceRootVirtualizationV2, c.miQueryReplicaVM, replicaVMQueryTimeout); err != nil { return fmt.Errorf("WMI query failed: %w", err) } for _, relationship := range dst { relationshipType := replicationRelationshipType(relationship.InstanceID) ch <- scrape.MustNewConstMetric( c.replicaVMHealth, prometheus.GaugeValue, float64(relationship.ReplicationHealth), relationship.ElementName, relationshipType, ) ch <- scrape.MustNewConstMetric( c.replicaVMState, prometheus.GaugeValue, float64(relationship.ReplicationState), relationship.ElementName, relationshipType, ) } return nil } // replicationRelationshipType returns the relationship type encoded in the // InstanceID ("Microsoft:\HVR\<0|1>"). 0 is the primary relationship, // 1 the extended replication. func replicationRelationshipType(instanceID string) string { switch instanceID[strings.LastIndex(instanceID, `\`)+1:] { case "0": return "primary" case "1": return "extended" default: return "unknown" } }