How-to monitor Hyper-V hosts and virtual machines with OpenTelemetry
This article demonstrates one possible implementation for collecting Hyper-V metrics with OpenTelemetry.
The setup of OpenTelemetry receivers, processors, exporters, and metric transformations is considered a custom configuration and is outside the scope of standard Checkmk support.
Overview
The default Hyper-V monitoring available through the Checkmk agent provides basic visibility into the Hyper-V host and virtual machine environment. In some deployments, additional metrics may be required to provide deeper insight into Hyper-V performance, resource consumption, and virtual machine behavior.
By using OpenTelemetry support introduced in Checkmk 2.5, it is possible to collect and visualize a much larger set of Hyper-V metrics. These metrics can be displayed as custom graphs, converted into services, and automatically associated with dynamically created hosts.
This article demonstrates one approach for collecting Hyper-V host and virtual machine metrics using OpenTelemetry Collector Contrib and forwarding those metrics to Checkmk.
Scope
This procedure was validated using the following software versions:
Component | Version |
|---|---|
Checkmk Edition | Cloud, Ultimate, Ultimate Managed Services |
Checkmk Version | 2.5.0 |
OpenTelemetry Collector Contrib | 0.147 |
Hyper-V | 10.0.26100.7019 |
Windows | Windows 11 Enterprise 25H2 |
Windows Server | Untested |
Challenge
The standard Hyper-V agent plugin provides a useful baseline of monitoring data.
However, administrators may require additional information about:
Hyper-V host CPU utilization
Hyper-V memory balancing
Virtual processor performance
Dynamic memory pressure
Virtual switch activity
Virtual disk activity
Individual virtual machine resource consumption
In these situations, OpenTelemetry can be used as an additional data source to collect and forward metrics that are not available through the standard agent checks.
Example environment:
System | Hostname |
|---|---|
Hyper-V Host | My-hyperv |
Virtual Machine | My-VM01 |
Virtual Machine | My-VM02 |
The goals of this configuration are:
Collect detailed Hyper-V host and VM metrics using OpenTelemetry.
Automatically create VM hosts using Dynamic Host Management.
Display VM-related metrics directly from the Hyper-V host.
Convert selected metrics into Checkmk services for long-term visibility and alerting.
Solution Overview
OpenTelemetry Collector Contrib runs directly on the Hyper-V host.
The collector gathers performance counter data from Windows and Hyper-V and forwards the resulting metrics to Checkmk through the OTLP interface.
The supplied configuration contains two primary metric pipelines:
Host Metrics Pipeline
The host pipeline collects information about the Hyper-V host itself, including:
CPU utilization
Memory utilization
Disk performance
Network activity
System uptime
Hyper-V host health
Hyper-V virtual switch metrics
Hyper-V memory balancer statistics
These metrics are associated with the Hyper-V host.
Virtual Machine Metrics Pipeline
The VM pipeline collects metrics for all Hyper-V virtual machines running on the host, including:
Virtual processor utilization
Guest memory usage
Memory pressure
Virtual network throughput
Virtual storage performance
Hypervisor partition statistics
Configuration
1. Install OpenTelemetry Collector Contrib
Install OpenTelemetry Collector Contrib on the Hyper-V host.
The collector is responsible for gathering Windows Performance Counter data and forwarding it to Checkmk.
Place the provided config.yaml file in the OpenTelemetry installation directory.
Example location: C:\Program Files\OpenTelemetryContrib\
The supplied configuration performs several important tasks:
Collects host-level metrics
Collects VM-level metrics
Filters unnecessary performance counter instances
Extracts VM names automatically
Maps VM names to the
host.nameresource attribute used by Checkmk Dynamic Host ManagementSends metrics to the Checkmk OTLP endpoint
# =============================================================================
# OpenTelemetry Collector Contrib - config.yaml
# otelcol-contrib v0.150.1 on Windows 11 (Hyper-V host)
# Place in: C:\Program Files\OpenTelemetryContrib\config.yaml
#
# Architecture:
# metrics/host → host telemetry, auto-detected hostname
# metrics/vm → ALL per-VM metrics (cpu, memory, partition, network, storage)
# VM name extracted automatically from instance attribute
#
# In Checkmk Dynamic Host Management:
# Resource attribute for hostname lookup → host.name
# =============================================================================
receivers:
# ===========================================================================
# RECEIVER 1: Windows 11 Host Telemetry
# ===========================================================================
windowsperfcounters/host:
collection_interval: 30s
metrics:
host.cpu.processor_time:
description: "Processor time executing non-idle threads"
unit: "%"
gauge:
host.cpu.privileged_time:
description: "Non-idle time in privileged (kernel) mode"
unit: "%"
gauge:
host.cpu.user_time:
description: "Non-idle time in user mode"
unit: "%"
gauge:
host.cpu.idle_time:
description: "Processor idle time"
unit: "%"
gauge:
host.cpu.interrupt_time:
description: "Time servicing hardware interrupts"
unit: "%"
gauge:
host.cpu.dpc_time:
description: "Time servicing deferred procedure calls"
unit: "%"
gauge:
host.cpu.interrupts:
description: "Hardware interrupts per second"
unit: "{interrupts}/s"
gauge:
host.memory.available:
description: "Physical memory available for allocation"
unit: "MBy"
gauge:
host.memory.committed:
description: "Committed virtual memory"
unit: "By"
gauge:
host.memory.commit_limit:
description: "Max memory committable without extending paging"
unit: "By"
gauge:
host.memory.committed_pct:
description: "Ratio of committed bytes to commit limit"
unit: "%"
gauge:
host.memory.page_faults:
description: "Page faults per second"
unit: "{faults}/s"
gauge:
host.memory.pages_per_sec:
description: "Pages read/written for hard page faults"
unit: "{pages}/s"
gauge:
host.memory.pages_input:
description: "Pages read from disk for hard page faults"
unit: "{pages}/s"
gauge:
host.memory.pages_output:
description: "Pages written to disk to free memory"
unit: "{pages}/s"
gauge:
host.memory.pool_nonpaged:
description: "Nonpaged pool size"
unit: "By"
gauge:
host.memory.pool_paged:
description: "Paged pool size"
unit: "By"
gauge:
host.memory.cache:
description: "File system cache size"
unit: "By"
gauge:
host.paging.usage:
description: "Current paging file usage"
unit: "%"
gauge:
host.paging.usage_peak:
description: "Peak paging file usage"
unit: "%"
gauge:
host.disk.free_space:
description: "Free space on logical disk"
unit: "%"
gauge:
host.disk.free_mb:
description: "Free megabytes on logical disk"
unit: "MBy"
gauge:
host.disk.time:
description: "Disk busy time"
unit: "%"
gauge:
host.disk.idle_time:
description: "Disk idle time"
unit: "%"
gauge:
host.disk.read_bytes:
description: "Disk read bytes per second"
unit: "By/s"
gauge:
host.disk.write_bytes:
description: "Disk write bytes per second"
unit: "By/s"
gauge:
host.disk.reads:
description: "Disk read operations per second"
unit: "{operations}/s"
gauge:
host.disk.writes:
description: "Disk write operations per second"
unit: "{operations}/s"
gauge:
host.disk.read_latency:
description: "Average time per read"
unit: "s"
gauge:
host.disk.write_latency:
description: "Average time per write"
unit: "s"
gauge:
host.disk.queue_length:
description: "Average disk queue length"
unit: "{requests}"
gauge:
host.disk.current_queue:
description: "Current disk queue length"
unit: "{requests}"
gauge:
host.pdisk.time:
description: "Physical disk busy time"
unit: "%"
gauge:
host.pdisk.idle_time:
description: "Physical disk idle time"
unit: "%"
gauge:
host.pdisk.read_bytes:
description: "Physical disk read bytes per second"
unit: "By/s"
gauge:
host.pdisk.write_bytes:
description: "Physical disk write bytes per second"
unit: "By/s"
gauge:
host.pdisk.read_latency:
description: "Average physical disk read time"
unit: "s"
gauge:
host.pdisk.write_latency:
description: "Average physical disk write time"
unit: "s"
gauge:
host.pdisk.queue_length:
description: "Average physical disk queue length"
unit: "{requests}"
gauge:
host.pdisk.current_queue:
description: "Current physical disk queue length"
unit: "{requests}"
gauge:
host.net.bytes_recv:
description: "Bytes received per second"
unit: "By/s"
gauge:
host.net.bytes_sent:
description: "Bytes sent per second"
unit: "By/s"
gauge:
host.net.bytes_total:
description: "Total bytes per second"
unit: "By/s"
gauge:
host.net.bandwidth:
description: "Network interface bandwidth"
unit: "By/s"
gauge:
host.net.packets_recv:
description: "Packets received per second"
unit: "{packets}/s"
gauge:
host.net.packets_sent:
description: "Packets sent per second"
unit: "{packets}/s"
gauge:
host.net.recv_errors:
description: "Inbound packet errors"
unit: "{errors}"
gauge:
host.net.send_errors:
description: "Outbound packet errors"
unit: "{errors}"
gauge:
host.net.recv_discarded:
description: "Inbound packets discarded"
unit: "{packets}"
gauge:
host.net.send_discarded:
description: "Outbound packets discarded"
unit: "{packets}"
gauge:
host.net.output_queue:
description: "Output packet queue length"
unit: "{packets}"
gauge:
host.system.uptime:
description: "System uptime"
unit: "s"
gauge:
host.system.processes:
description: "Running processes"
unit: "{processes}"
gauge:
host.system.threads:
description: "Running threads"
unit: "{threads}"
gauge:
host.system.processor_queue:
description: "Threads waiting for processor"
unit: "{threads}"
gauge:
host.system.context_switches:
description: "Context switches per second"
unit: "{switches}/s"
gauge:
hyperv.health.ok:
description: "VMs in healthy state"
unit: "{vms}"
gauge:
hyperv.health.critical:
description: "VMs in critical state"
unit: "{vms}"
gauge:
hyperv.logical_processors:
description: "Logical processors in hypervisor"
unit: "{cpus}"
gauge:
hyperv.virtual_processors:
description: "Virtual processors across all partitions"
unit: "{cpus}"
gauge:
hyperv.partitions:
description: "Hypervisor partitions"
unit: "{partitions}"
gauge:
hyperv.total_pages:
description: "Hypervisor memory pages"
unit: "{pages}"
gauge:
hyperv.host_cpu.total_run_time:
description: "Host logical processor total run time"
unit: "%"
gauge:
hyperv.host_cpu.guest_run_time:
description: "Host logical processor guest run time"
unit: "%"
gauge:
hyperv.host_cpu.hypervisor_run_time:
description: "Hypervisor code run time"
unit: "%"
gauge:
hyperv.host_cpu.idle_time:
description: "Host logical processor idle time"
unit: "%"
gauge:
hyperv.host_cpu.context_switches:
description: "vCPU context switches per second"
unit: "{switches}/s"
gauge:
hyperv.host_cpu.hw_interrupts:
description: "Hardware interrupts per second"
unit: "{interrupts}/s"
gauge:
hyperv.memory.available:
description: "Memory available for VM balancing"
unit: "MBy"
gauge:
hyperv.memory.avg_pressure:
description: "Average VM memory pressure"
unit: "%"
gauge:
hyperv.memory.current_pressure:
description: "Current system memory pressure"
unit: "%"
gauge:
hyperv.vswitch.bytes_recv:
description: "Virtual switch bytes received per second"
unit: "By/s"
gauge:
hyperv.vswitch.bytes_sent:
description: "Virtual switch bytes sent per second"
unit: "By/s"
gauge:
hyperv.vswitch.packets_recv:
description: "Virtual switch packets received per second"
unit: "{packets}/s"
gauge:
hyperv.vswitch.packets_sent:
description: "Virtual switch packets sent per second"
unit: "{packets}/s"
gauge:
hyperv.vswitch.dropped_incoming:
description: "Virtual switch incoming drops per second"
unit: "{packets}/s"
gauge:
hyperv.vswitch.dropped_outgoing:
description: "Virtual switch outgoing drops per second"
unit: "{packets}/s"
gauge:
hyperv.virtual_processor.total_run_time:
description: "vCPU total run time"
unit: "%"
gauge:
hyperv.virtual_processor.guest_run_time:
description: "vCPU guest run time"
unit: "%"
gauge:
hyperv.virtual_processor.hypervisor_run_time:
description: "vCPU hypervisor run time"
unit: "%"
gauge:
hyperv.dynamic_memory.current:
description: "Current memory assigned to VM"
unit: "MB"
gauge:
hyperv.dynamic_memory.guest_visible:
description: "Memory visible to the guest OS"
unit: "MB"
gauge:
hyperv.dynamic_memory.pressure:
description: "Current memory pressure"
unit: "%"
gauge:
perfcounters:
- object: "Processor Information"
instances: ["_Total"]
counters:
- { name: "% Processor Time", metric: host.cpu.processor_time }
- { name: "% Privileged Time", metric: host.cpu.privileged_time }
- { name: "% User Time", metric: host.cpu.user_time }
- { name: "% Idle Time", metric: host.cpu.idle_time }
- { name: "% Interrupt Time", metric: host.cpu.interrupt_time }
- { name: "% DPC Time", metric: host.cpu.dpc_time }
- { name: "Interrupts/sec", metric: host.cpu.interrupts }
- object: "Memory"
counters:
- { name: "Available MBytes", metric: host.memory.available }
- { name: "Committed Bytes", metric: host.memory.committed }
- { name: "Commit Limit", metric: host.memory.commit_limit }
- { name: "% Committed Bytes In Use", metric: host.memory.committed_pct }
- { name: "Page Faults/sec", metric: host.memory.page_faults }
- { name: "Pages/sec", metric: host.memory.pages_per_sec }
- { name: "Pages Input/sec", metric: host.memory.pages_input }
- { name: "Pages Output/sec", metric: host.memory.pages_output }
- { name: "Pool Nonpaged Bytes", metric: host.memory.pool_nonpaged }
- { name: "Pool Paged Bytes", metric: host.memory.pool_paged }
- { name: "Cache Bytes", metric: host.memory.cache }
- object: "Paging File"
instances: ["_Total"]
counters:
- { name: "% Usage", metric: host.paging.usage }
- { name: "% Usage Peak", metric: host.paging.usage_peak }
- object: "LogicalDisk"
instances: ["*"]
counters:
- { name: "% Free Space", metric: host.disk.free_space }
- { name: "Free Megabytes", metric: host.disk.free_mb }
- { name: "% Disk Time", metric: host.disk.time }
- { name: "% Idle Time", metric: host.disk.idle_time }
- { name: "Disk Read Bytes/sec", metric: host.disk.read_bytes }
- { name: "Disk Write Bytes/sec", metric: host.disk.write_bytes }
- { name: "Disk Reads/sec", metric: host.disk.reads }
- { name: "Disk Writes/sec", metric: host.disk.writes }
- { name: "Avg. Disk sec/Read", metric: host.disk.read_latency }
- { name: "Avg. Disk sec/Write", metric: host.disk.write_latency }
- { name: "Avg. Disk Queue Length", metric: host.disk.queue_length }
- { name: "Current Disk Queue Length", metric: host.disk.current_queue }
- object: "PhysicalDisk"
instances: ["*"]
counters:
- { name: "% Disk Time", metric: host.pdisk.time }
- { name: "% Idle Time", metric: host.pdisk.idle_time }
- { name: "Disk Read Bytes/sec", metric: host.pdisk.read_bytes }
- { name: "Disk Write Bytes/sec", metric: host.pdisk.write_bytes }
- { name: "Avg. Disk sec/Read", metric: host.pdisk.read_latency }
- { name: "Avg. Disk sec/Write", metric: host.pdisk.write_latency }
- { name: "Avg. Disk Queue Length", metric: host.pdisk.queue_length }
- { name: "Current Disk Queue Length", metric: host.pdisk.current_queue }
- object: "Network Interface"
instances: ["*"]
counters:
- { name: "Bytes Received/sec", metric: host.net.bytes_recv }
- { name: "Bytes Sent/sec", metric: host.net.bytes_sent }
- { name: "Bytes Total/sec", metric: host.net.bytes_total }
- { name: "Current Bandwidth", metric: host.net.bandwidth }
- { name: "Packets Received/sec", metric: host.net.packets_recv }
- { name: "Packets Sent/sec", metric: host.net.packets_sent }
- { name: "Packets Received Errors", metric: host.net.recv_errors }
- { name: "Packets Outbound Errors", metric: host.net.send_errors }
- { name: "Packets Received Discarded", metric: host.net.recv_discarded }
- { name: "Packets Outbound Discarded", metric: host.net.send_discarded }
- { name: "Output Queue Length", metric: host.net.output_queue }
- object: "System"
counters:
- { name: "System Up Time", metric: host.system.uptime }
- { name: "Processes", metric: host.system.processes }
- { name: "Threads", metric: host.system.threads }
- { name: "Processor Queue Length", metric: host.system.processor_queue }
- { name: "Context Switches/sec", metric: host.system.context_switches }
- object: "Hyper-V Virtual Machine Health Summary"
counters:
- { name: "Health Ok", metric: hyperv.health.ok }
- { name: "Health Critical", metric: hyperv.health.critical }
- object: "Hyper-V Hypervisor"
counters:
- { name: "Logical Processors", metric: hyperv.logical_processors }
- { name: "Virtual Processors", metric: hyperv.virtual_processors }
- { name: "Partitions", metric: hyperv.partitions }
- { name: "Total Pages", metric: hyperv.total_pages }
- object: "Hyper-V Hypervisor Logical Processor"
instances: ["_Total"]
counters:
- { name: "% Total Run Time", metric: hyperv.host_cpu.total_run_time }
- { name: "% Guest Run Time", metric: hyperv.host_cpu.guest_run_time }
- { name: "% Hypervisor Run Time", metric: hyperv.host_cpu.hypervisor_run_time }
- { name: "% Idle Time", metric: hyperv.host_cpu.idle_time }
- { name: "Context Switches/sec", metric: hyperv.host_cpu.context_switches }
- { name: "Hardware Interrupts/sec", metric: hyperv.host_cpu.hw_interrupts }
- object: "Hyper-V Dynamic Memory Balancer"
instances: ["*"]
counters:
- { name: "Available Memory", metric: hyperv.memory.available }
- { name: "Average Pressure", metric: hyperv.memory.avg_pressure }
- { name: "System Current Pressure", metric: hyperv.memory.current_pressure }
- object: "Hyper-V Virtual Switch"
instances: ["*"]
counters:
- { name: "Bytes Received/sec", metric: hyperv.vswitch.bytes_recv }
- { name: "Bytes Sent/sec", metric: hyperv.vswitch.bytes_sent }
- { name: "Packets Received/sec", metric: hyperv.vswitch.packets_recv }
- { name: "Packets Sent/sec", metric: hyperv.vswitch.packets_sent }
- { name: "Dropped Packets Incoming/sec", metric: hyperv.vswitch.dropped_incoming }
- { name: "Dropped Packets Outgoing/sec", metric: hyperv.vswitch.dropped_outgoing }
- object: "Hyper-V Hypervisor Virtual Processor"
instances: ["*"]
counters:
- name: "% Total Run Time"
metric: hyperv.virtual_processor.total_run_time
- name: "% Guest Run Time"
metric: hyperv.virtual_processor.guest_run_time
- name: "% Hypervisor Run Time"
metric: hyperv.virtual_processor.hypervisor_run_time
- object: "Hyper-V Dynamic Memory VM"
instances: ["*"]
counters:
- name: "Physical Memory (MB)"
metric: hyperv.dynamic_memory.current
- name: "Guest Visible Physical Memory (MB)"
metric: hyperv.dynamic_memory.guest_visible
- name: "Current Pressure"
metric: hyperv.dynamic_memory.pressure
# ===========================================================================
# RECEIVER 2: ALL Hyper-V VM Telemetry (single receiver)
# ===========================================================================
windowsperfcounters/vm:
collection_interval: 30s
metrics:
# ── CPU ──
vm.cpu.total_run_time:
description: "vCPU total run time"
unit: "%"
gauge:
vm.cpu.guest_run_time:
description: "vCPU guest code run time"
unit: "%"
gauge:
vm.cpu.hypervisor_run_time:
description: "vCPU hypervisor code run time"
unit: "%"
gauge:
vm.cpu.remote_run_time:
description: "vCPU remote physical processor run time"
unit: "%"
gauge:
vm.cpu.wait_time:
description: "vCPU dispatch wait time"
unit: "ns"
gauge:
# ── Memory ──
vm.memory.physical:
description: "Physical memory assigned to VM"
unit: "MBy"
gauge:
vm.memory.guest_visible:
description: "Memory visible to guest OS"
unit: "MBy"
gauge:
vm.memory.guest_available:
description: "Memory available inside guest OS"
unit: "MBy"
gauge:
vm.memory.current_pressure:
description: "Current memory pressure"
unit: "%"
gauge:
vm.memory.average_pressure:
description: "Average memory pressure"
unit: "%"
gauge:
vm.memory.maximum_pressure:
description: "Maximum memory pressure"
unit: "%"
gauge:
vm.memory.minimum_pressure:
description: "Minimum memory pressure"
unit: "%"
gauge:
vm.memory.added:
description: "Memory added by dynamic memory"
unit: "MBy"
gauge:
vm.memory.removed:
description: "Memory removed by dynamic memory"
unit: "MBy"
gauge:
# ── Partition ──
vm.partition.virtual_processors:
description: "Virtual processors assigned to VM"
unit: "{cpus}"
gauge:
vm.partition.tlb_pages:
description: "Virtual TLB pages"
unit: "{pages}"
gauge:
vm.partition.address_spaces:
description: "Address spaces in VM partition"
unit: "{spaces}"
gauge:
# ── Network ──
vm.net.bytes_recv:
description: "VM vNIC bytes received per second"
unit: "By/s"
gauge:
vm.net.bytes_sent:
description: "VM vNIC bytes sent per second"
unit: "By/s"
gauge:
vm.net.packets_recv:
description: "VM vNIC packets received per second"
unit: "{packets}/s"
gauge:
vm.net.packets_sent:
description: "VM vNIC packets sent per second"
unit: "{packets}/s"
gauge:
vm.net.dropped_incoming:
description: "VM vNIC incoming drops per second"
unit: "{packets}/s"
gauge:
vm.net.dropped_outgoing:
description: "VM vNIC outgoing drops per second"
unit: "{packets}/s"
gauge:
# ── Storage ──
vm.storage.read_bytes:
description: "VM virtual disk read bytes per second"
unit: "By/s"
gauge:
vm.storage.write_bytes:
description: "VM virtual disk write bytes per second"
unit: "By/s"
gauge:
vm.storage.read_ops:
description: "VM virtual disk read ops per second"
unit: "{operations}/s"
gauge:
vm.storage.write_ops:
description: "VM virtual disk write ops per second"
unit: "{operations}/s"
gauge:
vm.storage.latency:
description: "VM virtual disk IO latency"
unit: "s"
gauge:
vm.storage.queue_length:
description: "VM virtual disk queue length"
unit: "{requests}"
gauge:
vm.storage.errors:
description: "VM virtual disk errors"
unit: "{errors}"
gauge:
vm.storage.throughput:
description: "VM virtual disk throughput"
unit: "By/s"
gauge:
perfcounters:
# Instance: "VMName:Hv VP N"
- object: "Hyper-V Hypervisor Virtual Processor"
instances: ["*"]
counters:
- { name: "% Total Run Time", metric: vm.cpu.total_run_time }
- { name: "% Guest Run Time", metric: vm.cpu.guest_run_time }
- { name: "% Hypervisor Run Time", metric: vm.cpu.hypervisor_run_time }
- { name: "% Remote Run Time", metric: vm.cpu.remote_run_time }
- { name: "CPU Wait Time Per Dispatch", metric: vm.cpu.wait_time }
# Instance: "VMName"
- object: "Hyper-V Dynamic Memory VM"
instances: ["*"]
counters:
- { name: "Physical Memory", metric: vm.memory.physical }
- { name: "Guest Visible Physical Memory", metric: vm.memory.guest_visible }
- { name: "Guest Available Memory", metric: vm.memory.guest_available }
- { name: "Current Pressure", metric: vm.memory.current_pressure }
- { name: "Average Pressure", metric: vm.memory.average_pressure }
- { name: "Maximum Pressure", metric: vm.memory.maximum_pressure }
- { name: "Minimum Pressure", metric: vm.memory.minimum_pressure }
- { name: "Added Memory", metric: vm.memory.added }
- { name: "Removed Memory", metric: vm.memory.removed }
# Instance: "VMName:HvPt"
- object: "Hyper-V Hypervisor Partition"
instances: ["*"]
counters:
- { name: "Virtual Processors", metric: vm.partition.virtual_processors }
- { name: "Virtual TLB Pages", metric: vm.partition.tlb_pages }
- { name: "Address Spaces", metric: vm.partition.address_spaces }
# Instance: "VMName_Network Adapter_GUID" or "Default Switch_GUID"
- object: "Hyper-V Virtual Network Adapter"
instances: ["*"]
counters:
- { name: "Bytes Received/sec", metric: vm.net.bytes_recv }
- { name: "Bytes Sent/sec", metric: vm.net.bytes_sent }
- { name: "Packets Received/sec", metric: vm.net.packets_recv }
- { name: "Packets Sent/sec", metric: vm.net.packets_sent }
- { name: "Dropped Packets Incoming/sec", metric: vm.net.dropped_incoming }
- { name: "Dropped Packets Outgoing/sec", metric: vm.net.dropped_outgoing }
# Instance: "C:-...-VMName.vhdx" or "...-GUID.vmgs"
- object: "Hyper-V Virtual Storage Device"
instances: ["*"]
counters:
- { name: "Read Bytes/sec", metric: vm.storage.read_bytes }
- { name: "Write Bytes/sec", metric: vm.storage.write_bytes }
- { name: "Read Operations/Sec", metric: vm.storage.read_ops }
- { name: "Write Operations/Sec", metric: vm.storage.write_ops }
- { name: "Latency", metric: vm.storage.latency }
- { name: "Queue Length", metric: vm.storage.queue_length }
- { name: "Error Count", metric: vm.storage.errors }
- { name: "Throughput", metric: vm.storage.throughput }
# =============================================================================
# PROCESSORS
# =============================================================================
processors:
# ── Host: auto-detect hostname from OS ──
resourcedetection/host:
detectors: [env, system]
system:
hostname_sources: ["os"]
override: false
# ── VM: drop non-VM data points ──
# Drops: "Default Switch_..." (host-level virtual switch adapters)
# "*.vmgs" (VM config files, GUID-based, no VM name)
filter/vm:
error_mode: ignore
metrics:
datapoint:
- 'IsMatch(attributes["instance"], "^Default Switch.*")'
- 'IsMatch(attributes["instance"], ".*\\.vmgs$")'
- 'IsMatch(attributes["instance"], ".*\\.iso.*")'
# ── VM: extract VM name from instance attribute ──
#
# Instance formats and extraction logic:
# "My-VM01" → as-is (memory)
# "My-VM01:Hv VP 0" → Split on ":" → "My-VM01"
# "My-VM01:HvPt" → Split on ":" → "My-VM01"
# "My-VM01_Network Adapter_GUID" → Split on "_Network Adapter" → "My-VM01"
# "C:-...-Virtual Hard Disks-My-VM01.vhdx"
# → Split on "Virtual Hard Disks-" → "My-VM01.vhdx"
# → Split on "." → "My-VM01"
# "C:-...-Virtual Hard Disks-My-VM01 [1].vhdx"
# → Split on "Virtual Hard Disks-" → "My-VM01 [1].vhdx"
# → Split on " [" → "My-VM01"
# (if no " [", split on "." instead)
#
transform/vm:
error_mode: ignore
metric_statements:
- context: datapoint
statements:
# Step 1: Default — copy full instance as vm.name
- set(attributes["vm.name"], attributes["instance"])
where attributes["instance"] != nil
# Step 2: "VMName:suffix" (CPU, Partition) → take before ":"
- set(attributes["vm.name"], Split(attributes["instance"], ":")[0])
where attributes["instance"] != nil and IsMatch(attributes["instance"], ".*:.*")
# Step 3: "VMName_Network Adapter_GUID" → take before "_Network Adapter"
- set(attributes["vm.name"], Split(attributes["instance"], "_Network Adapter")[0])
where attributes["instance"] != nil and IsMatch(attributes["instance"], ".*_Network Adapter.*")
# Step 4: Storage paths containing "Virtual Hard Disks-"
# Extract filename after "Virtual Hard Disks-"
- set(attributes["vm.name"], Split(attributes["instance"], "Virtual Hard Disks-")[1])
where attributes["instance"] != nil and IsMatch(attributes["instance"], ".*Virtual Hard Disks-.*")
# Step 4b: Clean up " [N].vhdx" suffix → take before " ["
- set(attributes["vm.name"], Split(attributes["vm.name"], " [")[0])
where attributes["vm.name"] != nil and IsMatch(attributes["vm.name"], ".* \\[.*")
# Step 4c: Clean up ".vhdx" suffix (for disks without " [N]")
- set(attributes["vm.name"], Split(attributes["vm.name"], ".vhdx")[0])
where attributes["vm.name"] != nil and IsMatch(attributes["vm.name"], ".*\\.vhdx$")
# ── Promote vm.name to resource level → one Checkmk host per VM ──
groupbyattrs/vm:
keys:
- vm.name
# ── Copy vm.name → host.name for Checkmk hostname lookup ──
transform/vm_hostname:
error_mode: ignore
metric_statements:
- context: resource
statements:
- set(attributes["host.name"], attributes["vm.name"])
where attributes["vm.name"] != nil
# =============================================================================
# EXPORTERS
# =============================================================================
exporters:
otlp_grpc:
endpoint: "192.168.3.199:4317"
tls:
insecure: true
debug:
verbosity: basic # basic | normal | detailed
sampling_initial: 2
sampling_thereafter: 1
# =============================================================================
# SERVICE
# =============================================================================
service:
telemetry:
metrics:
level: none
pipelines:
metrics/host:
receivers: [windowsperfcounters/host]
processors: [resourcedetection/host]
exporters: [otlp_grpc, debug]
metrics/vm:
receivers: [windowsperfcounters/vm]
processors: [filter/vm, transform/vm, groupbyattrs/vm, transform/vm_hostname]
exporters: [otlp_grpc, debug]
Optional Logging Script
When testing or troubleshooting the collector, it can be helpful to capture output to a log file.
Place this script in the same directory with otelcol-contrib.exe
@echo off
cd /d "C:\Program Files\OpenTelemetryContrib"
if not exist logs mkdir logs
for /f %%I in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMdd_HHmmss"') do set dt=%%I
set LOGFILE=logs\collector_%dt%.log
otelcol-contrib.exe --config config.yaml > %LOGFILE% 2>&1
pauseEach execution creates a timestamped log file that can be used to verify successful metric collection and export.
2. Configure OpenTelemetry in Checkmk
After the collector has been configured, create the OpenTelemetry connection in Checkmk.
Follow the standard OpenTelemetry collector setup procedure documented in the Checkmk OpenTelemetry documentation.
Once the collector is successfully sending metrics, configure Dynamic Host Management.
The Dynamic Host Management connection is responsible for automatically creating hosts based on incoming OpenTelemetry data.
Important settings:
Setting | Value |
|---|---|
Connector type | OpenTelemetry data - Metric backend |
Resource attribute for hostname lookup |
|
Service discovery | Enabled |
The host.name attribute is particularly important because it determines how Checkmk identifies incoming metrics and associates them with hosts.
Without this mapping, automatic host creation will not function as expected.
3. Verify Host Creation
Allow Dynamic Host Management to perform a synchronization cycle or trigger synchronization manually.
If the OpenTelemetry collector is functioning correctly and metrics are being received, Checkmk should automatically create hosts for:
My-VM01
My-VM02
My-hyperv
At this point, the OpenTelemetry collector is already transmitting data to Checkmk.
Even before converting metrics into services, metrics can be queried directly through the metric backend.
If desired, the Hyper-V host can also be created manually and assigned a static IP address.
4. Create Custom Graphs
One advantage of OpenTelemetry integration is the ability to work directly with raw metrics.
Navigate to: Customize → Custom graphs
Create a new graph and select Graph line (OpenTelemetry).
Within the Query section:
Select Metric.
Choose the desired metric.
Optionally filter by resource attributes.
Save the graph.
Example metric: host.disk.reads
To limit results to a specific host: host.name = My-hyperv
This allows administrators to visualize metrics without requiring a dedicated Checkmk check plugin.
At this stage, Hyper-V host metrics are being successfully collected and visualized directly from OpenTelemetry data. This confirms that the OpenTelemetry pipeline is working and that metrics from the Hyper-V host are available in Checkmk.
Metric Retention
Metrics stored only in the metric backend are retained for approximately 14 days in Checkmk 2.5.
At the time of writing, this retention period is not configurable.
For longer-term visibility and threshold monitoring, metrics should be converted into services.
5. Convert Metrics into Services
To expose OpenTelemetry metrics as normal Checkmk services, create a Metric Backend rule.
Navigate to: Setup → Agents → Other integrations → Metric backend (custom query)
Create a new rule and assign it to the Hyper-V host.
The first example uses: host.disk.reads
After confirming the configuration works as expected, additional metrics can be added.
Examples:
host.disk.readshyperv.virtual_processor.total_run_timehyperv.dynamic_memory.pressure
These metrics are useful because they expose activity occurring within the Hyper-V virtual machines while presenting the information directly on the Hyper-V host.
This provides a consolidated view of the virtualization environment without needing to navigate between multiple hosts.
6. Discover Services
After saving the Metric Backend rule, perform service discovery on the Hyper-V host.
Navigate to: Setup → Hosts → hyperv → My-hyperv
Run service discovery.
Checkmk should discover services based on the configured OpenTelemetry metrics.
Notice that services from both the My-hyperv host and the virtual machines are visible.
Examples include:
Host disk metrics
Host CPU metrics
Virtual processor utilization
Dynamic memory pressure
VM-specific performance metrics
This configuration also exposes virtual machine metrics through the Hyper-V host, allowing VM performance data to be monitored without deploying OpenTelemetry collectors inside each virtual machine. The VM metrics are collected through the windowsperfcounters/vm receiver and presented through the Hyper-V host in Checkmk.
Additional Information
Combining the Checkmk Agent and OpenTelemetry
OpenTelemetry is not intended to replace the Checkmk agent.
In many environments, the best results are achieved by using both data sources together.
This approach provides:
Traditional Checkmk agent services
OpenTelemetry metric collection
Additional Hyper-V visibility
Greater flexibility when building dashboards and services
When both sources are used, host properties should be reviewed to ensure data is assigned correctly.
Time Synchronization
Time synchronization is critical when working with OpenTelemetry metrics.
All participating systems should use the same time zone, ideally UTC:
Checkmk server
My-hyperv host
Hyper-V virtual machines
A mismatch between host and guest time zones can result in unexpected metric behavior.
Particular attention should be paid after changing the Hyper-V host time zone, as guest operating systems may not automatically update their configured time zone.