# VMware vSphere Monitoring with OpenTelemetry Collector on Windows

> A complete guide to monitoring VMware vSphere with the OpenTelemetry Collector Contrib distribution on Windows: vCenter setup, the full collector config, running it as a service, ESXi syslog, the metrics reference, validation, and the stream-name gotcha that leaves the Metrics explorer empty.

Source: https://openobserve.ai/blog/vmware-vsphere-monitoring-opentelemetry/
Published: 2026-09-10
Authors: Chaitanya Sistla
Category: How To
Tags: OpenTelemetry, Monitoring, Integrations, Metrics, DevOps

---

VMware vSphere monitoring with OpenTelemetry means running the OpenTelemetry Collector Contrib distribution with the `vcenter` receiver, which polls the vSphere SDK for datacenter, cluster, host, VM, datastore, and vSAN metrics and exports them over OTLP. No vendor agent goes on your ESXi hosts, and no software gets installed on the vCenter appliance.

This guide walks the whole path on a Windows host: preparing vCenter, installing the collector, writing the config, running it as a service, adding ESXi syslog, and validating that data actually landed in [OpenObserve](https://openobserve.ai/). It also covers the one configuration mistake that catches almost everyone the first time, where the inventory shows up perfectly and the Metrics explorer stays stubbornly empty.

**Key takeaways**

- The `vcenter` receiver ships only in the Contrib collector build, and it polls vCenter over HTTPS 443 with a read-only account.
- Missing disk latency or throughput series is nearly always a vCenter statistics level problem, not a collector problem.
- Use two separate `otlphttp` exporters: metrics with no `stream-name` header, logs with one. Sharing a single exporter is what collapses every metric into one stream.
- The collector's own telemetry on port 8888 tells you which half of the pipeline to blame before you go hunting in the UI.

## How does the vcenter receiver collect VMware metrics?

![The OpenTelemetry Collector on a Windows host, pulling vCenter metrics over HTTPS and exporting them to OpenObserve over OTLP HTTP](/assets/vmware_vsphere_otel_collector_architecture.png)

A few things worth knowing before you touch any config:

- The `vcenter` receiver is a poll-based scraper. It authenticates to the vSphere SDK endpoint at `https://<vcenter>/sdk` and pulls metrics for datacenters, clusters, hosts, resource pools, VMs, datastores, and vSAN on an interval you choose.
- The collector runs anywhere with network reach to vCenter over 443. It does not need to run on the vCenter appliance. A Windows VM or jump host is fine.
- Metrics leave the collector through the `otlphttp` exporter pointed at the OpenObserve OTLP HTTP ingestion endpoint, with Basic auth.
- Metrics and logs use two separate exporter definitions pointed at the same endpoint. The only difference between them is whether the `stream-name` header is present. That detail matters more than it looks, and it gets its own section below.
- ESXi syslog is optional and completely independent of the metrics path.

If you have not worked with the collector before, the [OpenTelemetry Collector glossary entry](/glossary/what-is-opentelemetry-collector/) covers the receiver, processor, exporter model this config is built on.

## What you need before you start

| Requirement | Detail |
| --- | --- |
| Windows host | Windows Server 2016 or later, or Windows 10/11, x64. 2 vCPU and 4 GB RAM covers most environments |
| Network | Outbound 443 to vCenter. Outbound 5080 (self-hosted) or 443 (cloud) to OpenObserve. Inbound UDP 514 only if you collect ESXi syslog |
| vSphere versions | The receiver is built and tested against vCenter and ESXi 7.0 and 8 |
| vCenter account | A dedicated read-only user with permissions propagated to every monitored object |
| OpenObserve | An organization, and ingestion credentials (user email plus an ingestion token). Streams get created on first ingest |
| Admin rights | Local Administrator on the Windows host, to install the service |

## Preparing vCenter

### Create a read-only monitoring user

1. In the vSphere Client, go to **Administration > Single Sign On > Users and Groups**.
2. Create a user, for example `otel-monitor@vsphere.local`, with a strong password.
3. Go to **Administration > Access Control > Global Permissions**, or the root vCenter object then **Permissions**.
4. Add the user with the built-in **Read-only** role and check **Propagate to children**.

The receiver only reads inventory and performance data. It never needs write, console, or datastore browse permissions, so there is no reason to grant them.

### Check your performance counter statistics levels

Several metrics depend on vCenter statistics levels, found under **Administration > vCenter Server Settings > Statistics**, or **Configure > General > Statistics** on the vCenter object:

| Statistics level | Metrics that need it |
| --- | --- |
| Level 1 (default) | Most capacity and usage metrics: CPU, memory, counts, datastore |
| Level 2 | `vcenter.host.disk.latency.avg`, `vcenter.vm.disk.latency.avg`, `vcenter.vm.disk.throughput` |
| Level 3 | `vcenter.host.disk.latency.max` |
| Level 4 | `vcenter.host.disk.throughput` |

If disk latency or throughput series are missing in OpenObserve, this is almost always why. Raise the level on the shortest interval (5 minutes) only. Raising every interval bloats the vCenter statistics database and buys you nothing.

Two related gotchas worth checking at the same time:

- Powered-off VMs emit no performance counters. Inventory metrics such as `vcenter.datacenter.vm.count` still include them, but CPU, memory, disk, and network series will not exist for them.
- Hosts in maintenance mode or in a disconnected state drop out of performance collection while staying in the inventory. If a whole cluster looks like names without numbers, check host connection state first.

### TLS

By default the receiver rejects self-signed vCenter certificates. Your options, best first:

1. Use a vCenter with a CA-signed certificate, in which case there is nothing to configure.
2. Export the vCenter CA certificate and reference it with `tls.ca_file`.
3. Set `tls.insecure_skip_verify: true`. Acceptable for a proof of concept, avoid it in production.

## Installing the OpenTelemetry Collector Contrib on Windows

The `vcenter` receiver ships only in the [Contrib distribution](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/vcenterreceiver). The core `otelcol` build does not include it.

### Download

Grab the latest Windows amd64 release from the [collector releases page](https://github.com/open-telemetry/opentelemetry-collector-releases/releases). In PowerShell, running as Administrator:

```powershell
# Pin the version you want, and check the releases page for the current one
$version = "0.135.0"

New-Item -ItemType Directory -Force -Path "C:\otelcol" | Out-Null

Invoke-WebRequest `
  -Uri "https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v$version/otelcol-contrib_${version}_windows_amd64.tar.gz" `
  -OutFile "C:\otelcol\otelcol-contrib.tar.gz"

tar -xzf C:\otelcol\otelcol-contrib.tar.gz -C C:\otelcol
```

You should now have `C:\otelcol\otelcol-contrib.exe`.

Releases also publish an MSI (`otelcol-contrib_<version>_windows_x64.msi`) that installs the binary and registers a Windows service named `otelcol-contrib` in one step:

```powershell
msiexec /i "otelcol-contrib_0.135.0_windows_x64.msi" /qn COLLECTOR_SVC_ARGS='--config "C:\otelcol\config.yaml"'
```

### Verify you got the right build

```powershell
C:\otelcol\otelcol-contrib.exe components | Select-String "vcenter"
```

You should see `vcenter` listed under receivers. If nothing comes back, you downloaded the core distribution by mistake.

### Directory layout used here

```text
C:\otelcol\
  otelcol-contrib.exe
  config.yaml
  logs\            (optional, the collector's own file logs)
```

## Finding your OpenObserve endpoint and credentials

In OpenObserve, go to **Data Sources (Ingestion) > Custom > Metrics > OTLP HTTP**, or the Traces and Logs tabs for their endpoints. The UI shows a ready-made endpoint plus an `Authorization: Basic <base64>` header value for your organization. Copy both. They are exactly what the collector needs.

### Endpoint patterns

| Deployment | OTLP HTTP base endpoint |
| --- | --- |
| Self-hosted, single node | `http://<host>:5080/api/<org_name>` |
| Self-hosted behind TLS or a load balancer | `https://o2.yourcompany.com/api/<org_name>` |
| OpenObserve Cloud | `https://api.openobserve.ai/api/<org_id>` |

The `otlphttp` exporter appends `/v1/metrics`, `/v1/logs`, and `/v1/traces` to the base endpoint on its own, so configure only the base path shown above. Appending the signal path yourself is a common source of 404s.

One warning about localhost. Use `http://localhost:5080/api/default` only when OpenObserve and the collector run on the same machine. If you browse the OpenObserve UI at `localhost:5080` from your desktop but the collector runs on a different Windows host, the collector needs the real hostname or IP of the OpenObserve machine. The default organization on a fresh self-hosted install is `default`.

### Building the Basic auth token by hand

The token is `base64(email:ingestion_password)`:

```powershell
$pair = "you@company.com:YOUR_INGESTION_TOKEN"
[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pair))
```

Use the resulting string as `Basic <value>` in the exporter headers.

## The full collector config

Save this as `C:\otelcol\config.yaml`. It is a complete, production-oriented config: the vcenter receiver, a health check, memory protection, resource tagging, batching, retry and queueing on the exporter, and the collector's own telemetry.

Read the exporter section carefully. `otlphttp/openobserve_metrics` deliberately carries no `stream-name` header, which is what lets OpenObserve create one stream per metric name and populate the Metrics explorer normally. `otlphttp/openobserve_logs` carries the header and is used only by the logs pipeline further down. Do not add `stream-name` to the metrics exporter.

```yaml
receivers:
  vcenter:
    endpoint: https://vcenter.yourcompany.local
    username: otel-monitor@vsphere.local
    password: ${env:VCENTER_PASSWORD}
    collection_interval: 2m
    initial_delay: 1s
    timeout: 90s
    tls:
      # Preferred: point at your vCenter CA bundle
      # ca_file: C:\otelcol\vcenter-ca.pem
      # Proof of concept only:
      insecure_skip_verify: true
    metrics:
      # Defaults are all on. Enable the optional extras below if you want them.
      vcenter.host.memory.capacity:
        enabled: true
      vcenter.vm.memory.granted:
        enabled: true
      vcenter.vm.cpu.time:
        enabled: true
      vcenter.vm.network.broadcast.packet.rate:
        enabled: true
      vcenter.vm.network.multicast.packet.rate:
        enabled: true

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 1024
    spike_limit_mib: 256

  resource:
    attributes:
      - key: deployment.environment
        value: production
        action: upsert
      - key: service.name
        value: vmware-vcenter
        action: upsert
      - key: vcenter.instance
        value: vcenter.yourcompany.local
        action: upsert

  batch:
    send_batch_size: 8192
    send_batch_max_size: 16384
    timeout: 10s

exporters:
  # METRICS. No stream-name header. OpenObserve creates one stream per metric name.
  otlphttp/openobserve_metrics:
    endpoint: https://api.openobserve.ai/api/your_org_id
    # Self-hosted example:
    # endpoint: http://openobserve.internal:5080/api/default
    headers:
      Authorization: "Basic ${env:O2_AUTH_TOKEN}"
    compression: gzip
    timeout: 30s
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 60s
      max_elapsed_time: 10m
    sending_queue:
      enabled: true
      num_consumers: 4
      queue_size: 5000

  # LOGS ONLY. Used by the syslog pipeline below. Safe to delete if you are not
  # collecting ESXi logs.
  otlphttp/openobserve_logs:
    endpoint: https://api.openobserve.ai/api/your_org_id
    headers:
      Authorization: "Basic ${env:O2_AUTH_TOKEN}"
      stream-name: vmware
    compression: gzip
    timeout: 30s
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 60s
      max_elapsed_time: 10m
    sending_queue:
      enabled: true
      num_consumers: 4
      queue_size: 5000

  # Handy while validating, remove in production
  debug:
    verbosity: basic
    sampling_initial: 2
    sampling_thereafter: 100

extensions:
  health_check:
    endpoint: 127.0.0.1:13133

service:
  extensions: [health_check]
  pipelines:
    metrics/vmware:
      receivers: [vcenter]
      processors: [memory_limiter, resource, batch]
      exporters: [otlphttp/openobserve_metrics]
  telemetry:
    logs:
      level: info
    metrics:
      level: basic
      readers:
        - pull:
            exporter:
              prometheus:
                host: 127.0.0.1
                port: 8888
```

A few notes on the choices above:

- `collection_interval: 2m` is a sane default. For very large vCenters with thousands of VMs, start at `5m` and confirm scrapes finish inside the interval.
- `${env:...}` substitution keeps secrets out of the file. Set them as machine-level environment variables so the service account can read them.
- The `stream-name` header names the target logs stream. Leaving it off the metrics exporter is intentional and required.
- Do not leave both `debug` and a production exporter on the same pipeline long term. The debug exporter is chatty.

## Running the collector as a Windows service

### Set secrets as machine environment variables

Run this in an elevated PowerShell so the service account can see them:

```powershell
[System.Environment]::SetEnvironmentVariable("VCENTER_PASSWORD", "YourVCenterPassword", "Machine")
[System.Environment]::SetEnvironmentVariable("O2_AUTH_TOKEN", "PASTE_YOUR_BASE64_TOKEN_HERE", "Machine")
```

`O2_AUTH_TOKEN` holds only the base64 part. The config prefixes it with `Basic `.

### Test run in the foreground first

Before creating the service, always dry-run:

```powershell
$env:VCENTER_PASSWORD = "YourVCenterPassword"
$env:O2_AUTH_TOKEN = "PASTE_YOUR_BASE64_TOKEN_HERE"
C:\otelcol\otelcol-contrib.exe --config C:\otelcol\config.yaml
```

A healthy startup ends with `Everything is ready. Begin running and processing data.` The first vcenter scrape fires after `initial_delay` plus connection setup, so expect the first metric batch within one collection interval.

### Create the service

```powershell
New-Service -Name "otelcol-contrib" `
  -DisplayName "OpenTelemetry Collector Contrib" `
  -Description "Collects VMware vCenter metrics and ships to OpenObserve" `
  -BinaryPathName '"C:\otelcol\otelcol-contrib.exe" --config "C:\otelcol\config.yaml"' `
  -StartupType Automatic

Start-Service otelcol-contrib
Get-Service otelcol-contrib
```

If you installed via the MSI the service already exists, so just edit `config.yaml` and restart:

```powershell
Restart-Service otelcol-contrib
```

### Auto-restart on crash

```powershell
sc.exe failure otelcol-contrib reset= 86400 actions= restart/5000/restart/10000/restart/30000
```

### Where the collector's own logs go

Running as a service, the collector writes to the Windows Event Log, in the Application log under source `otelcol-contrib`:

```powershell
Get-WinEvent -ProviderName "otelcol-contrib" -MaxEvents 50 | Format-List TimeCreated, Message
```

### Firewall

Only needed if you enable the syslog receiver:

```powershell
New-NetFirewallRule -DisplayName "OTel Syslog UDP 514" -Direction Inbound -Protocol UDP -LocalPort 514 -Action Allow
```

This is the same service pattern used for [Windows host and event log monitoring](/blog/windows-monitoring-openobserve/), so if you already run a collector for Windows telemetry you can fold the vcenter receiver into that config instead of standing up a second one.

## Optional: sending ESXi syslog to OpenObserve

The vcenter receiver is metrics only. For ESXi host logs, point each host's remote syslog at the same collector and add a logs pipeline.

### Configure the ESXi hosts

On each host, under **Host > Configure > System > Advanced System Settings**:

- `Syslog.global.logHost` = `udp://<windows-collector-ip>:514`

Then open the syslog firewall rule on the host under **Configure > Firewall > Edit**, or via esxcli:

```bash
esxcli system syslog config set --loghost='udp://<collector-ip>:514'
esxcli network firewall ruleset set --ruleset-id=syslog --enabled=true
esxcli system syslog reload
```

### Add the pipeline to the collector

Note that this pipeline uses `otlphttp/openobserve_logs`, the exporter that carries the `stream-name` header. The metrics pipeline is unchanged and keeps using `otlphttp/openobserve_metrics`.

```yaml
receivers:
  syslog:
    udp:
      listen_address: "0.0.0.0:514"
    protocol: rfc3164
    location: UTC
    operators:
      - type: add
        field: attributes.source
        value: esxi-syslog

service:
  pipelines:
    metrics/vmware:
      receivers: [vcenter]
      processors: [memory_limiter, resource, batch]
      exporters: [otlphttp/openobserve_metrics]

    logs/esxi:
      receivers: [syslog]
      processors: [memory_limiter, resource, batch]
      exporters: [otlphttp/openobserve_logs]
```

Logs land in OpenObserve under the stream named by the `stream-name` header, in this case `vmware`. To split logs into multiple named streams, define additional `otlphttp` exporters with different `stream-name` values and assign one per pipeline.

## Which VMware metrics does the vcenter receiver emit?

Units follow OpenTelemetry conventions: `By` bytes, `MiBy` mebibytes, `MHz` megahertz, `%` percent, `ms` milliseconds, `us` microseconds, `{KiBy/s}` kibibytes per second.

### Datacenter metrics (on by default)

| Metric | Unit | Type | Description | Attributes |
| --- | --- | --- | --- | --- |
| `vcenter.datacenter.cluster.count` | `{clusters}` | Sum | Number of clusters in the datacenter | `status` (red/yellow/green/gray) |
| `vcenter.datacenter.cpu.limit` | `MHz` | Sum | Total CPU available to the datacenter | |
| `vcenter.datacenter.datastore.count` | `{datastores}` | Sum | Number of datastores in the datacenter | |
| `vcenter.datacenter.disk.space` | `By` | Sum | Available and used disk space | `disk_state` (available/used) |
| `vcenter.datacenter.host.count` | `{hosts}` | Sum | Number of hosts in the datacenter | `status`, `power_state` |
| `vcenter.datacenter.memory.limit` | `By` | Sum | Total memory available to the datacenter | |
| `vcenter.datacenter.vm.count` | `{virtual_machines}` | Sum | Number of VMs in the datacenter | `status`, `power_state` |

### Cluster metrics (on by default)

| Metric | Unit | Type | Description | Attributes |
| --- | --- | --- | --- | --- |
| `vcenter.cluster.cpu.effective` | `MHz` | Sum | Effective CPU available, excluding hosts in maintenance or unresponsive | |
| `vcenter.cluster.cpu.limit` | `MHz` | Sum | Total CPU available to the cluster | |
| `vcenter.cluster.host.count` | `{hosts}` | Sum | Number of hosts in the cluster | `effective` (bool) |
| `vcenter.cluster.memory.effective` | `By` | Sum | Effective available memory | |
| `vcenter.cluster.memory.limit` | `By` | Sum | Total available memory of the cluster | |
| `vcenter.cluster.vm.count` | `{virtual_machines}` | Sum | Number of VMs in the cluster | `power_state` |
| `vcenter.cluster.vm_template.count` | `{virtual_machine_templates}` | Sum | Number of VM templates | |
| `vcenter.cluster.vsan.congestions` | `{congestions/s}` | Gauge | vSAN client IO congestions | |
| `vcenter.cluster.vsan.latency.avg` | `us` | Gauge | Cluster latency accessing vSAN storage | `type` (read/write) |
| `vcenter.cluster.vsan.operations` | `{operations/s}` | Gauge | vSAN IOPS of the cluster | `type` (read/write/unmap) |
| `vcenter.cluster.vsan.throughput` | `By/s` | Gauge | vSAN throughput of the cluster | `direction` |

### Host (ESXi) metrics (on by default)

| Metric | Unit | Type | Description | Attributes |
| --- | --- | --- | --- | --- |
| `vcenter.host.cpu.capacity` | `MHz` | Sum | Total CPU capacity of the host | |
| `vcenter.host.cpu.reserved` | `MHz` | Sum | CPU reserved for VMs | `cpu_reservation_type` |
| `vcenter.host.cpu.usage` | `MHz` | Sum | CPU used by the host | |
| `vcenter.host.cpu.utilization` | `%` | Gauge | Host CPU utilization | |
| `vcenter.host.memory.usage` | `MiBy` | Sum | Memory used by the host | |
| `vcenter.host.memory.utilization` | `%` | Gauge | Host memory utilization | |
| `vcenter.host.disk.latency.avg` | `ms` | Gauge | Device plus kernel read/write latency (needs Level 2) | `direction`, `object` |
| `vcenter.host.disk.latency.max` | `ms` | Gauge | Highest latency across host disks, 20s window (needs Level 3) | `object` |
| `vcenter.host.disk.throughput` | `{KiBy/s}` | Sum | Aggregated disk IO rate, 20s window (needs Level 4) | `direction`, `object` |
| `vcenter.host.network.usage` | `{KiBy/s}` | Sum | Sum of tx and rx across all NICs | `object` |
| `vcenter.host.network.throughput` | `{KiBy/s}` | Sum | Data transmitted and received, 20s window | `direction`, `object` |
| `vcenter.host.network.packet.rate` | `{packets/s}` | Gauge | Packet rate per physical NIC, 20s window | `direction`, `object` |
| `vcenter.host.network.packet.error.rate` | `{errors/s}` | Gauge | Packet error rate, 20s window | `direction`, `object` |
| `vcenter.host.network.packet.drop.rate` | `{packets/s}` | Gauge | Packet drop rate per physical NIC, 20s window | `direction`, `object` |
| `vcenter.host.vsan.cache.hit_rate` | `%` | Gauge | Read IOs served by local client cache, 5m window | |
| `vcenter.host.vsan.congestions` | `{congestions/s}` | Gauge | vSAN client IO congestions on the host, 5m window | |
| `vcenter.host.vsan.latency.avg` | `us` | Gauge | Host latency accessing vSAN storage, 5m window | `type` |
| `vcenter.host.vsan.operations` | `{operations/s}` | Gauge | vSAN IOPS of the host, 5m window | `type` |
| `vcenter.host.vsan.throughput` | `By/s` | Gauge | vSAN throughput of the host, 5m window | `direction` |

### Virtual machine metrics (on by default)

| Metric | Unit | Type | Description | Attributes |
| --- | --- | --- | --- | --- |
| `vcenter.vm.cpu.usage` | `MHz` | Sum | CPU used by the VM | |
| `vcenter.vm.cpu.utilization` | `%` | Gauge | VM CPU utilization | |
| `vcenter.vm.cpu.readiness` | `%` | Gauge | Time the VM was ready but not scheduled on a physical CPU | |
| `vcenter.vm.memory.usage` | `MiBy` | Sum | Memory used by the VM | |
| `vcenter.vm.memory.utilization` | `%` | Gauge | VM memory utilization | |
| `vcenter.vm.memory.ballooned` | `MiBy` | Sum | Memory ballooned due to virtualization | |
| `vcenter.vm.memory.swapped` | `MiBy` | Sum | Memory granted from host swap space | |
| `vcenter.vm.memory.swapped_ssd` | `KiBy` | Sum | Memory swapped to a fast device such as SSD | |
| `vcenter.vm.disk.usage` | `By` | Sum | Storage used by the VM | `disk_state` |
| `vcenter.vm.disk.utilization` | `%` | Gauge | Storage utilization on the VM | |
| `vcenter.vm.disk.latency.avg` | `ms` | Gauge | Disk operation latency, 20s window (needs Level 2) | `direction`, `disk_type`, `object` |
| `vcenter.vm.disk.latency.max` | `ms` | Gauge | Highest total latency over 20s | `object` |
| `vcenter.vm.disk.throughput` | `{KiBy/s}` | Gauge | Virtual disk KiB/s, 20s window (needs Level 2) | `direction`, `object` |
| `vcenter.vm.network.usage` | `{KiBy/s}` | Sum | Combined tx and rx rate, 20s window | `object` |
| `vcenter.vm.network.throughput` | `By/s` | Sum | Data transmitted and received, 20s window | `direction`, `object` |
| `vcenter.vm.network.packet.rate` | `{packets/s}` | Gauge | Packet rate per vNIC, 20s window | `direction`, `object` |
| `vcenter.vm.network.packet.drop.rate` | `{packets/s}` | Gauge | Dropped packet rate per vNIC, 20s window | `direction`, `object` |
| `vcenter.vm.vsan.latency.avg` | `us` | Gauge | VM latency accessing vSAN storage | `type` |
| `vcenter.vm.vsan.operations` | `{operations/s}` | Gauge | vSAN IOPS of the VM | `type` |
| `vcenter.vm.vsan.throughput` | `By/s` | Gauge | vSAN throughput of the VM | `direction` |

`vcenter.vm.cpu.readiness` deserves special mention. It is the single best early signal of CPU contention on an overcommitted cluster, and it shows up long before guest-level CPU metrics look unhealthy.

### Resource pool metrics (on by default)

| Metric | Unit | Type | Description | Attributes |
| --- | --- | --- | --- | --- |
| `vcenter.resource_pool.cpu.shares` | `{shares}` | Sum | CPU shares in the resource pool | |
| `vcenter.resource_pool.cpu.usage` | `MHz` | Sum | CPU used by the resource pool | |
| `vcenter.resource_pool.memory.shares` | `{shares}` | Sum | Memory shares in the resource pool | |
| `vcenter.resource_pool.memory.usage` | `MiBy` | Sum | Memory used by the resource pool | `type`, behind a feature gate |
| `vcenter.resource_pool.memory.ballooned` | `MiBy` | Sum | Ballooned memory in the pool | |
| `vcenter.resource_pool.memory.granted` | `MiBy` | Sum | Memory granted to VMs in the pool | `type` (private/shared) |
| `vcenter.resource_pool.memory.swapped` | `MiBy` | Sum | Memory granted from host swap space | |

### Datastore metrics (on by default)

| Metric | Unit | Type | Description | Attributes |
| --- | --- | --- | --- | --- |
| `vcenter.datastore.disk.usage` | `By` | Sum | Space in the datastore | `disk_state` (available/used) |
| `vcenter.datastore.disk.utilization` | `%` | Gauge | Datastore utilization | |

### Optional metrics (off by default)

| Metric | Unit | Type | Description | Attributes |
| --- | --- | --- | --- | --- |
| `vcenter.host.memory.capacity` | `MiBy` | Sum | Total memory capacity of the host | |
| `vcenter.vm.memory.granted` | `MiBy` | Sum | Memory granted to a VM | |
| `vcenter.vm.cpu.time` | `%` | Gauge | CPU time in idle, ready, or wait state, 20s window | `cpu_state`, `object` |
| `vcenter.vm.network.broadcast.packet.rate` | `{packets/s}` | Gauge | Broadcast packet rate per vNIC, 20s window | `direction`, `object` |
| `vcenter.vm.network.multicast.packet.rate` | `{packets/s}` | Gauge | Multicast packet rate per vNIC, 20s window | `direction`, `object` |

Enable any of these with:

```yaml
receivers:
  vcenter:
    metrics:
      vcenter.host.memory.capacity:
        enabled: true
```

Disable any default metric the same way with `enabled: false`. That is the main lever for cutting [cardinality](/glossary/what-is-high-cardinality/) on the object-attributed disk and network series in large fleets.

## Resource attributes

Every series carries resource attributes identifying its scope. These become labels and dimensions in OpenObserve.

| Attribute | Description |
| --- | --- |
| `vcenter.datacenter.name` | Datacenter name |
| `vcenter.cluster.name` | Cluster name |
| `vcenter.host.name` | ESXi host name |
| `vcenter.datastore.name` | Datastore name |
| `vcenter.resource_pool.name` | Resource pool name |
| `vcenter.resource_pool.inventory_path` | Full inventory path of the pool |
| `vcenter.virtual_app.name` | vApp name |
| `vcenter.virtual_app.inventory_path` | vApp inventory path |
| `vcenter.vm.name` | VM name |
| `vcenter.vm.id` | VM instance UUID |
| `vcenter.vm_template.name` | VM template name |
| `vcenter.vm_template.id` | Template instance UUID |

On top of those you get whatever you upsert in the `resource` processor, which in the config above is `service.name`, `deployment.environment`, and `vcenter.instance`.

One naming detail that trips people up in queries: in OpenObserve, dots in attribute names are normalized to underscores. Query them as `vcenter_vm_name` and `vcenter_host_name`, not `vcenter.vm.name`.

## How do you validate that metrics are actually arriving?

Work through these in order. The first three confirm the collector is healthy. The rest confirm OpenObserve is storing and displaying the data.

![Triage flow for missing VMware metrics, following the collector's own counters from the vcenter receiver through the exporter to OpenObserve](/assets/vmware_vsphere_otel_metrics_triage.png)

### Collector side

**1. Watch the foreground run, or the Event Log.** Lines like `Exporting failed` are bad. Silence after startup is good, because the `otlphttp` exporter says nothing on success. The `debug` exporter prints `Metrics {"resource metrics": N, "metrics": M, "data points": K}` per batch.

**2. Hit the health check endpoint.**

```powershell
Invoke-WebRequest http://127.0.0.1:13133/ | Select-Object StatusCode
```

**3. Read the collector's own self-metrics.** This is the single most useful check in the whole guide, because it sits upstream of OpenObserve and tells you which half of the pipeline to blame.

```powershell
(Invoke-WebRequest http://127.0.0.1:8888/metrics).Content |
  Select-String "otelcol_receiver_accepted_metric_points|otelcol_exporter_sent_metric_points|otelcol_exporter_send_failed_metric_points|otelcol_scraper_errored_metric_points"
```

Run it twice, roughly two minutes apart, and compare:

| Reading | Meaning |
| --- | --- |
| `otelcol_receiver_accepted_metric_points` climbing | vCenter scraping works |
| `otelcol_receiver_accepted_metric_points` flat at 0 | The problem is on the vCenter side: credentials, permissions, statistics levels, or powered-off inventory |
| `otelcol_exporter_sent_metric_points` tracking accepted | Shipping to OpenObserve works |
| `otelcol_exporter_send_failed_metric_points` above 0 | Endpoint, auth, or network problem. Check the Event Log for the HTTP status |
| `otelcol_scraper_errored_metric_points` above 0 | Partial vCenter scrape failures, often permissions on a subset of objects |

Port 8888 is the collector's own telemetry endpoint, not OpenObserve. It is defined under `service.telemetry.metrics` in the config. Port 5080 is OpenObserve. If accepted and sent are both climbing and failed is flat at zero, the collector is doing its job and anything still missing is a display or routing problem in OpenObserve.

### OpenObserve side

**4. Check the Streams page, with the stream type filter set to metrics, not logs.** You should see many streams, one per metric name: `vcenter_host_cpu_usage`, `vcenter_vm_memory_utilization`, and so on. OpenObserve converts OTLP metric names by replacing dots with underscores.

If instead you see a single stream, often named `vmware`, containing fields like `vcenter_host_name` and `vcenter_cluster_name`, your metrics are being written as one lumped stream. That is the `stream-name` header problem covered in the troubleshooting section below.

**5. Query in the Metrics explorer, not the Logs view.** Metrics and logs are separate areas of the UI. Browsing the Logs view will show you inventory attributes and never a metric chart. Set the time range to the last hour, then run:

```promql
vcenter_host_cpu_usage
```

**6. Confirm with an aggregate.**

```promql
topk(10, vcenter_vm_cpu_utilization)
```

## Dashboards and queries worth building

PromQL examples for OpenObserve dashboard panels.

Capacity overview:

```promql
# Cluster CPU headroom (MHz)
vcenter_cluster_cpu_effective - scalar(sum(vcenter_host_cpu_usage))

# Cluster memory utilization %
100 * sum(vcenter_host_memory_usage) by (vcenter_cluster_name)
  / (sum(vcenter_cluster_memory_effective) by (vcenter_cluster_name) / 1024 / 1024)
```

Noisy neighbours and contention:

```promql
# Top 10 VMs by CPU readiness. Anything sustained above 5 percent hurts.
topk(10, vcenter_vm_cpu_readiness)

# VMs actively ballooning or swapping, which means memory pressure
vcenter_vm_memory_ballooned > 0
vcenter_vm_memory_swapped > 0
```

Storage:

```promql
# Datastores over 80 percent
vcenter_datastore_disk_utilization > 80

# Worst VM disk latency (read)
topk(10, vcenter_vm_disk_latency_avg{direction="read"})
```

Network health:

```promql
# Host NIC packet errors
sum(vcenter_host_network_packet_error_rate) by (vcenter_host_name, direction)

# VM packet drops
topk(10, vcenter_vm_network_packet_drop_rate)
```

Inventory tiles for stat panels:

```promql
sum(vcenter_datacenter_vm_count{power_state="on"})
sum(vcenter_datacenter_host_count)
sum(vcenter_cluster_vm_template_count)
```

A layout that works well in practice:

1. Row 1: stat tiles for VMs on and off, hosts, clusters, datastores, templates.
2. Row 2: cluster CPU and memory, effective versus usage, as time series.
3. Row 3: top-N VM CPU readiness, VM CPU utilization, VM memory utilization.
4. Row 4: datastore utilization bar gauge, VM disk latency top-N.
5. Row 5: host network throughput, packet error and drop rates.
6. Row 6 for vSAN shops: cluster vSAN IOPS, latency, congestion.

Once the dashboards are in place, the datastore utilization and VM CPU readiness panels are the two best candidates for alerts, since both degrade gradually and give you real lead time.

## Troubleshooting

### Inventory appears but no metrics

This is the most common failure by a wide margin, so it gets its own section.

**Symptom.** OpenObserve shows hosts, clusters, and datacenters by name, so data is clearly arriving, but the Metrics explorer is empty and no `vcenter_*` metric streams exist.

**Cause.** The `stream-name` header is present on the exporter used by the metrics pipeline. OpenObserve treats it as a directive to write into that one named stream, so every metric collapses into a single stream instead of one stream per metric name. Resource attributes such as `vcenter_host_name` stay visible as fields, which is exactly why the inventory looks healthy.

**Fix.**

1. Open `C:\otelcol\config.yaml` and remove or comment out the `stream-name` line from the exporter used by `metrics/vmware`. Better still, split into the two exporters shown earlier.
2. Validate and restart:

```powershell
C:\otelcol\otelcol-contrib.exe validate --config C:\otelcol\config.yaml
Restart-Service otelcol-contrib
```

3. Wait one full `collection_interval`, which is 2 minutes with the config above.
4. Go to Streams, filter by stream type metrics, and confirm individual `vcenter_*` streams now appear.

The old lumped stream stays behind and can be deleted from the Streams page once the new ones are populating.

If that is not it, the other explanation for the same symptom is that inventory metrics are arriving while performance counters are not. Inventory and capacity come from the vCenter configuration API, whereas CPU, memory, disk, and network come from performance counters. Check statistics levels, confirm VMs are powered on, and confirm hosts are connected and out of maintenance mode.

### General reference

| Symptom | Cause | Fix |
| --- | --- | --- |
| Inventory names visible, Metrics explorer empty | `stream-name` header applied to metrics | See the section above |
| Data visible under Logs but nothing under Metrics | Looking in the wrong area of the UI | Metrics live in the Metrics explorer, and under stream type metrics on the Streams page |
| `x509: certificate signed by unknown authority` at startup | Self-signed vCenter certificate | Add `ca_file` with the vCenter CA, or `insecure_skip_verify: true` for testing |
| `ServerFaultCode: Cannot complete login due to an incorrect user name or password` | Bad credentials or SSO domain mismatch | Use the full UPN (`user@vsphere.local`), and verify the env var is visible to the service at machine scope, then restart |
| `ServerFaultCode: Permission to perform this operation was denied` | Role not propagated | Re-add Read-only at the top level with Propagate to children |
| Scrape works but disk latency and throughput metrics are absent | vCenter statistics level too low | Raise the level per the statistics table above |
| Some VMs have counts but no CPU or memory series | VMs powered off | Expected. Performance counters only exist for running VMs |
| A whole cluster shows names but no performance data | Hosts in maintenance mode or disconnected | Check host connection state in the vSphere Client |
| `context deadline exceeded` in scraper errors | Large inventory, scrape slower than the timeout | Raise `timeout`, raise `collection_interval` |
| Nothing in OpenObserve, exporter logs 401 | Wrong Basic token, or wrong org in the URL | Regenerate the token from the ingestion page, confirm the org segment in the endpoint |
| Exporter logs 404 | Endpoint path wrong, missing `/api/<org>` or `/v1/metrics` appended manually | Set only the base `/api/<org>` path. The exporter appends signal paths itself |
| Connection refused reaching OpenObserve | `localhost` used but OpenObserve is on another machine | Use the real hostname or IP in the exporter endpoint |
| Metrics visible but names differ from the docs | OpenObserve normalizes dots to underscores | Query `vcenter_vm_cpu_usage`, not `vcenter.vm.cpu.usage` |
| Metrics exist but charts are blank | Time range shorter than the collection interval | Widen the Metrics explorer range to at least the last hour |
| Service starts then stops immediately | Config parse error | Run `otelcol-contrib.exe validate --config C:\otelcol\config.yaml` |
| Env vars not resolving under the service | Set at user scope, or the service started before the vars existed | Set at machine scope and `Restart-Service otelcol-contrib` |
| High memory on the collector | Huge VM count with per-object disk and network attributes | Disable high-cardinality metrics, or filter with the `filter` processor |
| UDP syslog arrives but timestamps are wrong | ESXi sends local time, the receiver assumes UTC | Set `location` in the syslog receiver to the host timezone |

Validate any config change before restarting the service:

```powershell
C:\otelcol\otelcol-contrib.exe validate --config C:\otelcol\config.yaml
```

## Tuning for large environments

**Interval versus inventory size.** Each scrape enumerates every datacenter, cluster, host, resource pool, VM, and datastore. As a rough guide: under 500 VMs, `2m` is fine. Between 500 and 2000 VMs, use `3m` to `5m` with `timeout: 120s`. Beyond that, `5m` or more, and consider splitting per datacenter with multiple `vcenter/<name>` receiver instances, or one collector per vCenter.

**Cardinality control.** The biggest series multipliers are the `object` attribute, meaning per-disk and per-NIC, on the latency, throughput, and packet metrics. Disabling `vcenter.vm.disk.latency.avg`, `vcenter.vm.network.packet.rate`, and their host equivalents cuts datapoint volume dramatically if you only need capacity views.

**Batching.** Keep `send_batch_size` at 8192 or above. OpenObserve ingests OTLP efficiently in larger batches, and gzip compression on the exporter typically shrinks payloads by 8 to 10 times.

**Multiple vCenters.** Define one receiver per vCenter and tag each with the `resource` processor:

```yaml
receivers:
  vcenter/dc1:
    endpoint: https://vcenter-dc1.company.local
    username: otel-monitor@vsphere.local
    password: ${env:VCENTER_DC1_PASSWORD}
    collection_interval: 3m
  vcenter/dc2:
    endpoint: https://vcenter-dc2.company.local
    username: otel-monitor@vsphere.local
    password: ${env:VCENTER_DC2_PASSWORD}
    collection_interval: 3m

service:
  pipelines:
    metrics/vmware:
      receivers: [vcenter/dc1, vcenter/dc2]
      processors: [memory_limiter, resource, batch]
      exporters: [otlphttp/openobserve_metrics]
```

**Upgrades.** Pin the collector version in your download script, read the vcenter receiver changelog before jumping versions, since metric names and attributes have shifted between releases historically, and always run `validate` after upgrading.

## Quick reference

```text
Binary:        C:\otelcol\otelcol-contrib.exe
Config:        C:\otelcol\config.yaml
Service:       otelcol-contrib (Automatic)
Health:        http://127.0.0.1:13133/
Self-metrics:  http://127.0.0.1:8888/metrics   (the collector's own telemetry)
Validate:      otelcol-contrib.exe validate --config C:\otelcol\config.yaml
Logs:          Windows Event Log > Application > otelcol-contrib
vCenter user:  otel-monitor@vsphere.local (Read-only, propagated)
O2 endpoint:   https://api.openobserve.ai/api/<org>  (cloud)
               http://<host>:5080/api/<org>          (self-hosted, default org is "default")
Auth header:   Authorization: Basic base64(email:token)
Metric naming: vcenter.vm.cpu.usage becomes vcenter_vm_cpu_usage in OpenObserve
stream-name:   Logs exporter only. Never on the metrics exporter.
Where to look: Metrics explorer for metrics, Logs view for syslog. The Streams
               page has a stream type filter, set it to "metrics".
```

## Wrapping up

The VMware side of this is genuinely simple once the read-only user and statistics levels are right, and the collector side is a single YAML file. The part that costs people an afternoon is the exporter header, so if you take one thing away, take this: `stream-name` belongs on the logs exporter and nowhere near metrics.

OpenObserve is open source under AGPL-3.0, so you can run this whole pipeline self-hosted with no per-host licensing, which matters when you are monitoring a few thousand VMs. If you want to extend the same collector to other infrastructure, the [OpenTelemetry integration guides](/opentelemetry/) cover the other receivers, and [SNMP monitoring with OpenTelemetry](/blog/snmp-monitoring-opentelemetry/) is a good next step for the network gear sitting underneath your vSphere clusters.
