From dc8092f46de8c77e5cf77ecdb9ae485b572e138b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 16 Oct 2026 10:33:34 +0200 Subject: [PATCH] hyperv: label replica relationships with the VM ID (#2747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Otto Kröpke --- docs/collector.hyperv.md | 27 ++++++----- .../collector/hyperv/hyperv_replica_vm.go | 29 +++++++---- .../hyperv/hyperv_replica_vm_internal_test.go | 48 +++++++++++++++++++ internal/collector/hyperv/hyperv_test.go | 3 ++ 4 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 internal/collector/hyperv/hyperv_replica_vm_internal_test.go diff --git a/docs/collector.hyperv.md b/docs/collector.hyperv.md index 03805f6d..cef481d0 100644 --- a/docs/collector.hyperv.md +++ b/docs/collector.hyperv.md @@ -206,22 +206,23 @@ Sub-collector `replica_vm`, enabled by default. The performance counter metrics The `health` and `state` metrics come from the `Msvm_ReplicationRelationship` WMI class in the `root/virtualization/v2` namespace and exist for every virtual machine, including those without replication. -| Name | Description | Type | Labels | -|-----------------------------------------------------------------|---------------------------------------------------------------------|---------|----------------------| -| `windows_hyperv_replica_vm_average_replication_latency_seconds` | The average time taken to replicate the virtual machine | gauge | `vm` | -| `windows_hyperv_replica_vm_average_replication_size_bytes` | The average size of the replicated data | gauge | `vm` | -| `windows_hyperv_replica_vm_compression_efficiency` | The compression efficiency of the replicated data | gauge | `vm` | -| `windows_hyperv_replica_vm_last_replication_size_bytes` | The size of the last replicated data | gauge | `vm` | -| `windows_hyperv_replica_vm_network_received_bytes_total` | The total number of bytes received over the network for replication | counter | `vm` | -| `windows_hyperv_replica_vm_network_sent_bytes_total` | The total number of bytes sent over the network for replication | counter | `vm` | -| `windows_hyperv_replica_vm_replications_total` | The total number of replication cycles | counter | `vm` | -| `windows_hyperv_replica_vm_replication_latency_seconds` | The time taken by the last replication of the virtual machine | gauge | `vm` | -| `windows_hyperv_replica_vm_resynchronized_bytes_total` | The total number of bytes sent during resynchronization | counter | `vm` | -| `windows_hyperv_replica_vm_health` | The replication health of the virtual machine, see below | gauge | `vm`, `relationship` | -| `windows_hyperv_replica_vm_state` | The replication state of the virtual machine, see below | gauge | `vm`, `relationship` | +| Name | Description | Type | Labels | +|-----------------------------------------------------------------|---------------------------------------------------------------------|---------|-------------------------------| +| `windows_hyperv_replica_vm_average_replication_latency_seconds` | The average time taken to replicate the virtual machine | gauge | `vm` | +| `windows_hyperv_replica_vm_average_replication_size_bytes` | The average size of the replicated data | gauge | `vm` | +| `windows_hyperv_replica_vm_compression_efficiency` | The compression efficiency of the replicated data | gauge | `vm` | +| `windows_hyperv_replica_vm_last_replication_size_bytes` | The size of the last replicated data | gauge | `vm` | +| `windows_hyperv_replica_vm_network_received_bytes_total` | The total number of bytes received over the network for replication | counter | `vm` | +| `windows_hyperv_replica_vm_network_sent_bytes_total` | The total number of bytes sent over the network for replication | counter | `vm` | +| `windows_hyperv_replica_vm_replications_total` | The total number of replication cycles | counter | `vm` | +| `windows_hyperv_replica_vm_replication_latency_seconds` | The time taken by the last replication of the virtual machine | gauge | `vm` | +| `windows_hyperv_replica_vm_resynchronized_bytes_total` | The total number of bytes sent during resynchronization | counter | `vm` | +| `windows_hyperv_replica_vm_health` | The replication health of the virtual machine, see below | gauge | `vm`, `vm_id`, `relationship` | +| `windows_hyperv_replica_vm_state` | The replication state of the virtual machine, see below | gauge | `vm`, `vm_id`, `relationship` | The `relationship` label is `primary` for the primary replication relationship and `extended` for [extended replication](https://learn.microsoft.com/en-us/windows/win32/hyperv_v2/msvm-replicationrelationship). +The `vm_id` label is the ID of the virtual machine, which keeps virtual machines with the same name apart. Values of `windows_hyperv_replica_vm_health`: diff --git a/internal/collector/hyperv/hyperv_replica_vm.go b/internal/collector/hyperv/hyperv_replica_vm.go index 920d1983..aa6df564 100644 --- a/internal/collector/hyperv/hyperv_replica_vm.go +++ b/internal/collector/hyperv/hyperv_replica_vm.go @@ -161,7 +161,7 @@ func (c *Collector) buildReplicaVM() error { 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"}, + []string{"vm", "vm_id", "relationship"}, nil, ) c.replicaVMState = prometheus.NewDesc( @@ -172,7 +172,7 @@ func (c *Collector) buildReplicaVM() error { "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"}, + []string{"vm", "vm_id", "relationship"}, nil, ) @@ -280,13 +280,14 @@ func (c *Collector) collectReplicaVMRelationships(ch chan<- prometheus.Metric, s } for _, relationship := range dst { - relationshipType := replicationRelationshipType(relationship.InstanceID) + vmID, relationshipType := parseReplicationRelationshipID(relationship.InstanceID) ch <- scrape.MustNewConstMetric( c.replicaVMHealth, prometheus.GaugeValue, float64(relationship.ReplicationHealth), relationship.ElementName, + vmID, relationshipType, ) @@ -295,6 +296,7 @@ func (c *Collector) collectReplicaVMRelationships(ch chan<- prometheus.Metric, s prometheus.GaugeValue, float64(relationship.ReplicationState), relationship.ElementName, + vmID, relationshipType, ) } @@ -302,16 +304,23 @@ func (c *Collector) collectReplicaVMRelationships(ch chan<- prometheus.Metric, s 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 { +// parseReplicationRelationshipID returns the ID of the virtual machine and the +// relationship type encoded in the InstanceID of a replication relationship +// ("Microsoft:\HVR\<0|1>"). The VM ID keeps the series of VMs with the +// same name apart. Relationship 0 is the primary relationship, 1 the extended +// replication. +func parseReplicationRelationshipID(instanceID string) (string, string) { + vmID := strings.TrimPrefix(instanceID, "Microsoft:") + if i := strings.Index(vmID, `\`); i >= 0 { + vmID = vmID[:i] + } + switch instanceID[strings.LastIndex(instanceID, `\`)+1:] { case "0": - return "primary" + return vmID, "primary" case "1": - return "extended" + return vmID, "extended" default: - return "unknown" + return vmID, "unknown" } } diff --git a/internal/collector/hyperv/hyperv_replica_vm_internal_test.go b/internal/collector/hyperv/hyperv_replica_vm_internal_test.go new file mode 100644 index 00000000..fba5f393 --- /dev/null +++ b/internal/collector/hyperv/hyperv_replica_vm_internal_test.go @@ -0,0 +1,48 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseReplicationRelationshipID(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + instanceID string + vmID string + relationshipType string + }{ + {`Microsoft:6B1F2E1A-3C4D-4E5F-8A9B-0C1D2E3F4A5B\HVR\0`, "6B1F2E1A-3C4D-4E5F-8A9B-0C1D2E3F4A5B", "primary"}, + {`Microsoft:6B1F2E1A-3C4D-4E5F-8A9B-0C1D2E3F4A5B\HVR\1`, "6B1F2E1A-3C4D-4E5F-8A9B-0C1D2E3F4A5B", "extended"}, + {`Microsoft:6B1F2E1A-3C4D-4E5F-8A9B-0C1D2E3F4A5B\HVR\2`, "6B1F2E1A-3C4D-4E5F-8A9B-0C1D2E3F4A5B", "unknown"}, + {`unexpected`, "unexpected", "unknown"}, + {``, "", "unknown"}, + } { + t.Run(tc.instanceID, func(t *testing.T) { + t.Parallel() + + vmID, relationshipType := parseReplicationRelationshipID(tc.instanceID) + require.Equal(t, tc.vmID, vmID) + require.Equal(t, tc.relationshipType, relationshipType) + }) + } +} diff --git a/internal/collector/hyperv/hyperv_test.go b/internal/collector/hyperv/hyperv_test.go index b18f4924..5509bdb5 100644 --- a/internal/collector/hyperv/hyperv_test.go +++ b/internal/collector/hyperv/hyperv_test.go @@ -47,6 +47,9 @@ func TestCollector(t *testing.T) { // 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 health query reports failures as 0 instead of an error, so check the value.