How-to monitor Hyper-V hosts and virtual machines with OpenTelemetry

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.

Table of Contents

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

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

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.name resource attribute used by Checkmk Dynamic Host Management

  • Sends 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 pause

Each 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

Setting

Value

Connector type

OpenTelemetry data - Metric backend

Resource attribute for hostname lookup

host.name

Service discovery

Enabled

image-20260612-075853.png

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

image-20260612-075928.png

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:

  1. Select Metric.

  2. Choose the desired metric.

  3. Optionally filter by resource attributes.

  4. Save the graph.

Example metric: 
host.disk.reads

 

To limit results to a specific host:
host.name = My-hyperv

 

image-20260612-080024.png

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.

image-20260612-080116.png

The first example uses: host.disk.reads

 

After confirming the configuration works as expected, additional metrics can be added.

Examples:

  • host.disk.reads

  • hyperv.virtual_processor.total_run_time

  • hyperv.dynamic_memory.pressure

image-20260612-080145.png

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.

image-20260612-080216.png

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.


Related articles